use std::fmt::{self, Display, Formatter};
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::Path;

mod xml;
use xml::XMLWriter;

const MAX_OUTPUT_FILES: u32 = 500;

/// An SVG document, which has a fixed size (computed in inches) as
/// well as a collection of drawn figures.
pub struct SVG {
    stuff: Vec<Box<AsSVG>>,
    size: (f64, f64),
}

#[derive(Copy, Clone)]
struct Inches { amt: f64 }

impl Display for Inches {
    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
        self.amt.fmt(f)?;
        write!(f, "in")
    }
}

fn inches(amt: f64) -> Inches { Inches { amt } }

/// Create a new empty SVG document of the specified width and height
pub fn svg(w: f64, h: f64) -> SVG {
    SVG {
        stuff: Vec::new(),
        size: (w, h),
    }
}

impl SVG {
    /// Print this SVG document to stdout
    pub fn to_stdout(self) -> io::Result<()> {
        self.write_svg(&mut std::io::stdout())
    }

    pub fn to_bytes(self) -> io::Result<Vec<u8>> {
        let mut buf = Vec::new();
        self.write_svg(&mut buf)?;
        Ok(buf)
    }

    /// Print this SVG document to stdout
    pub fn output(self, p: &str) -> io::Result<()> {
        let output_dir = Path::new("output");
        if !output_dir.is_dir() {
            fs::create_dir(output_dir)?;
        }

        let mut file = {
            let mut n = 0;
            let mut path = format!("output/{}{:05}.svg", p, n);
            while Path::new(&path).exists() {
                if n > MAX_OUTPUT_FILES {
                    return Err(io::Error::new(
                        io::ErrorKind::Other,
                        "gunpowder_treason: too many output files",
                    ));
                }
                n += 1;
                path = format!("output/{}{:05}.svg", p, n);
            }
            eprintln!("writing to {}", path);
            OpenOptions::new().write(true).create_new(true).open(&path)?
        };
        self.write_svg(&mut file)
    }


   pub fn write_svg<W: Write>(self, buf: &mut W) -> io::Result<()> {
       let (w, h) = self.size;
       let mut xml = XMLWriter::start(buf)?;
       xml.block(
           "svg",
           &[("xmlns", &"http://www.w3.org/2000/svg"),
             ("version", &"1.1"),
             ("width", &inches(w)),
             ("height", &inches(h)),
             ("viewBox", &format!("0 0 {} {}", w, h)),
             ("stroke-width", &"0.0001in"),
           ],
           |mut b| {
               for elem in self.stuff {
                   elem.draw_svg(&mut b)?;
               }
               Ok(())
           }
       )
    }

    /// Add a new drawable thing to this SVG document
    pub fn add<T: AsSVG>(&mut self, t: T) {
        self.stuff.push(t.consume())
    }
}



/// The AsSVG trait represents things which can be rendered to an SVG
/// canvas.
pub trait AsSVG {
    fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()>;
    // This is a bit of a hack to make sure that all of our types live
    // long enough: it's now on the implementer of AsSVG to box it up
    // and make sure that we can keep a trait object around.
    fn consume(self) -> Box<AsSVG>;
}

/// A Line is a sequence of points. It'll be represented as a single
/// dot if it has only one point, and otherwise will be drawn as a
/// line between them.
pub struct Line {
    start: (f64, f64),
    points: Vec<(f64, f64)>,
}

impl AsSVG for Line {
    fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()> {
        if self.points.len() == 0 {
            // if there are no later points, then we just draw a
            // 'point' at the starting position, which is just a tiny
            // circle
            let (x, y) = self.start;
            buf.tag("circle",
                &[("cx", &x),
                  ("cy", &y),
                  ("r", &"0.01"),
                  ("fill", &"black"),
                ],
            )
        } else {
            // Otherwise, we draw a path, which mean assembling this
            // somewhat wonky path field

            let path = {
                let mut path = Vec::new();
                let (x, y) = self.start;
                write!(&mut path, "M{} {}", x, y)?;
                for &(x, y) in self.points.iter() {
                    write!(&mut path, " L{} {}", x, y)?;
                }
                String::from_utf8(path).unwrap()
            };

            buf.tag("path",
                &[("d", &path),
                  ("stroke", &"black"),
                  ("fill", &"none"),
                ],
            )
        }
    }

    fn consume(self) -> Box<AsSVG> {
        Box::new(self)
    }
}

/// Create a new line at the given starting point
pub fn line(x: f64, y: f64) -> Line {
    Line::new(x, y)
}

impl Line {
    /// Create a new line at the given starting point
    pub fn new(x: f64, y: f64) -> Line {
        Line {
            start: (x, y),
            points: Vec::new(),
        }
    }

    /// Draw a line segment from this point to another point
    pub fn to(mut self, x: f64, y: f64) -> Line {
        self.points.push((x, y));
        self
    }
}

pub struct Rect {
    position: (f64, f64),
    size: (f64, f64),
}

pub fn rect(position: (f64, f64), size: (f64, f64)) -> Rect {
    Rect { position, size }
}

impl AsSVG for Rect {
    fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()> {
        let (x, y) = self.position;
        let (w, h) = self.size;
        buf.tag("rect",
            &[("x", &x),
              ("y", &y),
              ("width", &w),
              ("height", &h),
              ("stroke", &"black"),
              ("fill", &"none"),
            ]
        )
    }

    fn consume(self) -> Box<AsSVG> {
        Box::new(self)
    }
}


pub struct Circle {
    position: (f64, f64),
    radius: f64,
}

pub fn circle(position: (f64, f64), radius: f64) -> Circle {
    Circle { position, radius }
}

impl AsSVG for Circle {
    fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()> {
        let (x, y) = self.position;
        let r = self.radius;
        buf.tag(
            "circle",
            &[("cx", &x),
              ("cy", &y),
              ("r", &r),
              ("stroke", &"black"),
              ("fill", &"none"),
            ]
        )
    }

    fn consume(self) -> Box<AsSVG> {
        Box::new(self)
    }
}
