use failure::Error;
use image;
use serde_json;
use std::io::{Read, Seek, Write};
use zip::{ZipArchive, ZipWriter};

use crate::constants;

/// This value represents both the current document in-memory as well
/// as the entirety of the values that we will want to both save and
/// restore.
pub struct Document {
    pub tilesheet: image::DynamicImage,
    pub metadata: Metadata,
    pub rules: Vec<RuleSpec>,
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuleSpec {
    pub rule_name: String,
    pub lhs: Vec<((u16, u16), (u16, u16))>,
    pub rhs: Vec<((u16, u16), (u16, u16))>,
}

/// This should be renamed probably, but it's the configuration-level
/// info about a document (e.g. the tile size)
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Metadata {
    pub tile_width: u16,
    pub tile_height: u16,
    pub config_loop_forever: bool,
}

impl Document {
    /// Attempt to read a `Document` from anything which implements
    /// `Read` and `Seek`. The file format for Palladio files is
    /// documented externally.
    pub fn open_from_file<R: Read + Seek>(r: &mut R) -> Result<Document, Error> {
        let mut archive = ZipArchive::new(r)?;
        let tilesheet = {
            let mut buf = Vec::new();
            archive.by_name("tilesheet.png")?.read_to_end(&mut buf)?;
            image::load_from_memory(&buf)?
        };
        let metadata = {
            let file = archive.by_name("metadata.json")?;
            serde_json::from_reader(file)?
        };
        let rules = {
            let file = archive.by_name("rules.json")?;
            serde_json::from_reader(file)?
        };
        Ok(Document {
            tilesheet,
            metadata,
            rules,
        })
    }

    /// Attempt to write a `Document` from anything which implements
    /// `Write` and `Seek`. The file format for Palladio files is
    /// documented externally.
    pub fn save_to_file<W: Write + Seek>(&self, w: &mut W) -> Result<(), Error> {
        use zip::write::FileOptions;

        let mut zip = ZipWriter::new(w);
        zip.start_file("tilesheet.png", FileOptions::default())?;
        self.tilesheet
            .write_to(&mut zip, image::ImageOutputFormat::PNG)?;

        zip.start_file("metadata.json", FileOptions::default())?;
        serde_json::to_writer(&mut zip, &self.metadata)?;

        zip.start_file("rules.json", FileOptions::default())?;
        serde_json::to_writer(&mut zip, &self.rules)?;

        zip.start_file("info.txt", FileOptions::default())?;
        writeln!(
            &mut zip,
            "Created by {} v{}",
            constants::PROGRAM_NAME,
            constants::PROGRAM_VERSION,
        )?;

        zip.finish()?;
        Ok(())
    }

    /// Create a new fresh document with an empty 32x32 image, a
    /// configured tile size of 16x16, and no rules
    pub fn default() -> Document {
        Document {
            tilesheet: image::DynamicImage::new_rgb8(32, 32),
            metadata: Metadata {
                tile_width: 16,
                tile_height: 16,
                config_loop_forever: false,
            },
            rules: Vec::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Document;
    use std::io::{BufReader, Cursor};

    #[test]
    fn round_trip() {
        // First, save our dummy `Document` to an in-memory buffer
        let mut buf = Cursor::new(Vec::new());
        let doc1 = Document::default();
        doc1.save_to_file(&mut buf).unwrap();

        // then take that buffer back, and reparse it
        let buf = buf.into_inner();
        let doc2 = Document::open_from_file(&mut BufReader::new(Cursor::new(buf))).unwrap();
        // we can't assert equality over the image itself, so let's
        // just assert that the other parts are equal
        assert!(doc1.metadata == doc2.metadata);
        assert!(doc1.rules == doc2.rules);
    }
}
