pub mod context;
pub mod names;
pub mod parsers;
pub mod readers;
pub mod types;
pub mod unparsers;

use crate::*;
use crate::gen::schema::*;
use crate::syntax::block::Formatter;
use crate::syntax::block::constructors::*;

use glob::glob;
use preserves::value::BinarySource;
use preserves::value::IOBinarySource;
use preserves::value::Map;
use preserves::value::Reader;
use preserves::value::Set;

use std::convert::TryFrom;
use std::fs::DirBuilder;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::io::Read;
use std::io::Write;
use std::path::PathBuf;

pub type ModulePath = Vec<String>;

pub trait Plugin: std::fmt::Debug {
    fn generate(
        &self,
        module_ctxt: &mut context::ModuleContext,
        definition_name: &str,
        definition: &Definition,
    );
}

#[derive(Debug)]
pub struct CompilerConfig {
    pub bundle: Map<ModulePath, Schema>,
    pub output_dir: PathBuf,
    pub fully_qualified_module_prefix: String,
    pub support_crate: String,
    pub module_aliases: Map<ModulePath, String>,
    pub plugins: Vec<Box<dyn Plugin>>,
}

impl CompilerConfig {
    pub fn new(
        output_dir: PathBuf,
        fully_qualified_module_prefix: String,
    ) -> Self {
        CompilerConfig {
            bundle: Map::new(),
            output_dir: output_dir,
            fully_qualified_module_prefix: fully_qualified_module_prefix,
            support_crate: "preserves_schema".to_owned(),
            module_aliases: Map::new(),
            plugins: vec![
                Box::new(types::TypePlugin),
                Box::new(readers::ReaderPlugin),
                Box::new(parsers::ParserPlugin),
                Box::new(unparsers::UnparserPlugin),
            ],
        }
    }

    pub fn load_schemas_and_bundles(&mut self, inputs: &Vec<PathBuf>) -> io::Result<()> {
        for i in inputs {
            let mut f = File::open(&i)?;
            let mut src = IOBinarySource::new(&mut f);
            let mut reader = src.packed_iovalues();
            let blob = reader.demand_next(false)?;

            if let Ok(s) = Schema::try_from(&blob) {
                let prefix = i.file_stem().ok_or_else(
                    || io::Error::new(io::ErrorKind::InvalidData,
                                      format!("Bad schema file stem: {:?}", i)))?
                    .to_str().ok_or_else(
                        || io::Error::new(io::ErrorKind::InvalidData,
                                          format!("Invalid UTF-8 in schema file name: {:?}", i)))?;
                self.bundle.insert(vec![prefix.to_owned()], s);
                continue;
            }

            if let Ok(Bundle { modules }) = Bundle::try_from(&blob) {
                for (ModulePath(k), v) in modules.0 {
                    self.bundle.insert(k, v);
                }
                continue;
            }

            return Err(io::Error::new(io::ErrorKind::InvalidData,
                                      format!("Invalid schema binary blob {:?}", i)));
        }
        Ok(())
    }
}

pub fn expand_inputs(globs: &Vec<String>) -> io::Result<Vec<PathBuf>> {
    let mut result = Vec::new();
    for g in globs.iter() {
        for p in glob(g).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))? {
            result.push(p.map_err(glob::GlobError::into_error)?)
        }
    }
    Ok(result)
}

fn write_if_changed(output_path: &PathBuf, contents: &[u8]) -> io::Result<()> {
    if output_path.exists() {
        if let Ok(mut f) = File::open(output_path) {
            let mut existing_contents = Vec::new();
            f.read_to_end(&mut existing_contents)?;
            if existing_contents == contents {
                return Ok(());
            }
        }
    }
    let mut f = File::create(output_path)?;
    f.write_all(contents)
}

pub fn compile(config: &CompilerConfig) -> io::Result<()> {
    for (k, v) in config.bundle.iter() {
        let mut output_path = config.output_dir.clone();
        output_path.extend(k);
        let module_name = output_path.file_stem().unwrap().to_str().unwrap().to_owned();
        let module_name = names::render_modname(&module_name);
        output_path.set_file_name(format!("{}.rs", module_name));
        DirBuilder::new().recursive(true).create(output_path.parent().unwrap())?;
        let mut m = context::ModuleContext::new(config, &ModulePath(k.clone()));

        let mut modes: Vec<context::ModuleContextMode> =
            vec![context::ModuleContextMode::TargetAny];

        match &v.embedded_type {
            EmbeddedTypeName::False => {
                m.define_type(item(seq!["pub type _Ptr = preserves::value::IOValue;"]));
                m.define_type(item(seq!["pub type _Any = preserves::value::IOValue;"]));
            }
            EmbeddedTypeName::Ref(r) => {
                modes.push(context::ModuleContextMode::TargetIOValue);
                m.define_type(item(seq!["pub type _Dom = ", m.render_ref(&**r), ";"]));
                m.define_type(item(seq!["pub type _Ptr = std::sync::Arc<_Dom>;"]));
                m.define_type(item(seq!["pub type _Any = preserves::value::ArcValue<_Ptr>;"]));
            }
        }

        let modes = modes; // rebind as non-mutable

        for (n, d) in &v.definitions.0 {
            for plugin in config.plugins.iter() {
                plugin.generate(&mut m, n, d);
            }
            for mode in &modes {
                m.mode = Some(*mode);
                for plugin in config.plugins.iter() {
                    plugin.generate(&mut m, n, d);
                }
            }
            m.mode = None;
        }

        //---------------------------------------------------------------------------

        let mut lines: Vec<String> = Vec::new();

        lines.push("#![allow(unused_parens)]".to_owned());
        lines.push("#![allow(unused_imports)]".to_owned());
        lines.push("".to_owned());
        lines.push("use std::convert::TryFrom;".to_owned());
        lines.push("use preserves::value::Domain;".to_owned());
        lines.push("use preserves::value::NestedValue;".to_owned());
        lines.push(format!("use {}::support as _support;", &config.support_crate));
        lines.push("use _support::Deserialize;".to_owned());
        lines.push("".to_owned());

        for mode in &modes {
            m.mode = Some(*mode);
            lines.push(format!("mod {} {{", m.target_prefix()));
            lines.push("    use super::_Any;".to_owned());
            lines.push(format!("    use {}::support as _support;", &config.support_crate));
            lines.push("    _support::lazy_static! {".to_owned());
            for (value, name) in &m.literals {
                let bs = preserves::value::PackedWriter::encode_iovalue(&value).unwrap();
                lines.push(format!(
                    "        pub static ref {}: {} = /* {:?} */ _support::decode_lit(&vec!{:?}).unwrap();",
                    name,
                    m.target(),
                    value,
                    bs));
            }
            lines.push("    }".to_owned());
            lines.push("}".to_owned());
            lines.push("".to_owned());
        }
        m.mode = None;

        for i in m.typedefs {
            lines.push(Formatter::to_string(i));
            lines.push("".to_owned());
        }
        for i in m.functiondefs {
            lines.push(Formatter::to_string(i));
            lines.push("".to_owned());
        }

        {
            let contents = lines.join("\n");
            write_if_changed(&output_path, contents.as_bytes())?;
        }

        {
            let mut mod_rs = output_path.clone();
            mod_rs.set_file_name("mod.rs");
            let mut lines = if !mod_rs.exists() {
                Set::new()
            } else {
                io::BufReader::new(File::open(&mod_rs)?)
                    .lines().collect::<Result<Set<String>, _>>()?
            };
            lines.insert(format!("pub mod {};", &module_name));
            let contents = lines.into_iter().collect::<Vec<String>>().join("\n");
            write_if_changed(&mod_rs, contents.as_bytes())?;
        }
    }
    Ok(())
}
