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

use crate::*;
use crate::compiler::context::*;
use crate::compiler::types::Purpose;
use crate::gen::Language;
use crate::gen::schema;
use crate::gen::schema::*;
use crate::syntax::block::{Formatter, Item};
use crate::syntax::block::constructors::*;

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

use std::fs::DirBuilder;
use std::fs::File;
use std::io;
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_module(&self, _module_ctxt: &mut ModuleContext) {}

    fn generate_definition(
        &self,
        module_ctxt: &mut ModuleContext,
        definition_name: &str,
        definition: &Definition,
    );
}

pub struct LanguageTypes {
    pub fallback: Option<Box<dyn Fn(&str) -> Set<String>>>,
    pub definitions: Map<String, Box<dyn Fn(&str) -> Set<String>>>,
}

impl std::fmt::Debug for LanguageTypes {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        f.debug_struct("LanguageTypes")
            .field("fallback", &self.fallback.as_ref().map(|f| f("_")))
            .field("definitions", &self.definitions.iter().map(
                |(k, f)| (k.clone(), f("_"))).collect::<Map<String, Set<String>>>())
            .finish()
    }
}

#[derive(Debug)]
pub struct ExternalModule {
    pub path: ModulePath,
    pub rust_namespace: String,
    pub rust_language_types: LanguageTypes,
}

impl ExternalModule {
    pub fn new(path: ModulePath, rust_namespace: &str) -> Self {
        ExternalModule {
            path,
            rust_namespace: rust_namespace.to_owned(),
            rust_language_types: LanguageTypes {
                fallback: None,
                definitions: Map::new(),
            },
        }
    }

    pub fn set_fallback_language_types<F: 'static + Fn(&str) -> Set<String>>(
        mut self,
        f: F,
    ) -> Self {
        self.rust_language_types.fallback = Some(Box::new(f));
        self
    }

    pub fn set_definition_language_types<F: 'static + Fn(&str) -> Set<String>>(
        mut self,
        d: &str,
        f: F,
    ) -> Self {
        if self.rust_language_types.definitions.insert(d.to_owned(), Box::new(f)).is_some() {
            panic!("Duplicate language types definition installed: {:?} {:?}", &self.path, d);
        }
        self
    }
}

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

pub fn load_schema_or_bundle_with_purpose(
    bundle: &mut Map<ModulePath, (Schema, Purpose)>,
    i: &PathBuf,
    purpose: Purpose,
) -> io::Result<()> {
    let mut inserted = Map::<ModulePath, Schema>::new();
    load_schema_or_bundle(&mut inserted, i)?;
    for (k, v) in inserted.into_iter() { bundle.insert(k, (v, purpose)); }
    Ok(())
}

pub fn load_schema_or_bundle_bin_with_purpose(
    bundle: &mut Map<ModulePath, (Schema, Purpose)>,
    prefix: &str,
    input: &[u8],
    purpose: Purpose,
) -> io::Result<()> {
    let mut inserted = Map::<ModulePath, Schema>::new();
    load_schema_or_bundle_bin(&mut inserted, prefix, input)?;
    for (k, v) in inserted.into_iter() { bundle.insert(k, (v, purpose)); }
    Ok(())
}

fn bundle_prefix(i: &PathBuf) -> io::Result<&str> {
    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)))
}

pub fn load_schema_or_bundle(
    bundle: &mut Map<ModulePath, Schema>,
    i: &PathBuf,
) -> io::Result<()> {
    let mut f = File::open(&i)?;
    let mut bs = vec![];
    f.read_to_end(&mut bs)?;
    load_schema_or_bundle_bin(bundle, bundle_prefix(i)?, &bs[..])
}

pub fn load_schema_or_bundle_bin(
    bundle: &mut Map<ModulePath, Schema>,
    prefix: &str,
    input: &[u8],
) -> io::Result<()> {
    let mut src = BytesBinarySource::new(input);
    let mut reader = src.packed_iovalues();
    let blob = reader.demand_next(false)?;
    let language = Language::default();

    if let Ok(s) = language.parse(&blob) {
        bundle.insert(vec![prefix.to_owned()], s);
    } else if let Ok(Bundle { modules }) = language.parse(&blob) {
        for (ModulePath(k), v) in modules.0 {
            bundle.insert(k, v);
        }
    } else {
        return Err(io::Error::new(io::ErrorKind::InvalidData,
                                  format!("Invalid schema binary blob {:?}", prefix)));
    }

    Ok(())
}

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

    pub fn add_external_module(&mut self, m: ExternalModule) {
        let path = m.path.clone();
        if self.external_modules.insert(path.clone(), m).is_some() {
            panic!("Duplicate external module installed: {:?}", path)
        }
    }

    pub fn load_schemas_and_bundles(&mut self, inputs: &Vec<PathBuf>, xrefs: &Vec<PathBuf>) -> io::Result<()> {
        for i in inputs {
            load_schema_or_bundle_with_purpose(&mut self.bundle, i, Purpose::Codegen)?;
        }
        for i in xrefs {
            load_schema_or_bundle_with_purpose(&mut self.bundle, i, Purpose::Xref)?;
        }
        Ok(())
    }

    pub fn load_xref_bin(&mut self, prefix: &str, bundle_or_schema: &[u8]) -> io::Result<()> {
        load_schema_or_bundle_bin_with_purpose(
            &mut self.bundle, prefix, bundle_or_schema, Purpose::Xref)
    }

    fn build_type_cache(&self) -> Map<Ref, types::TDefinition> {
        self.bundle.iter().flat_map(|(modpath, s)| {
            let modpath = ModulePath(modpath.clone());
            s.0.definitions.0.iter().map(move |(name, def)| {
                let ty = types::definition_type(&modpath, s.1, name, def);
                (ty.self_ref.clone(), ty)
            })
        }).collect()
    }

    fn generate_definition(
        &self,
        b: &mut BundleContext,
        k: &ModulePath,
        v: &Schema,
        n: &str,
        d: &Definition,
        mode: ModuleContextMode,
        generated: &mut Map<ModuleContextMode, Vec<Item>>,
    ) {
        b.generate_module(k, v, mode, generated, |m| {
            for plugin in self.plugins.iter() {
                plugin.generate_definition(m, n, d);
            }
        });
    }
}

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)
}

impl Ref {
    pub fn qualify(&self, default_module_path: &schema::ModulePath) -> Ref {
        if self.module.0.is_empty() {
            Ref { module: default_module_path.clone(), name: self.name.clone() }
        } else {
            self.clone()
        }
    }
}

impl Schema {
    pub fn has_embedded_type(&self) -> bool {
        self.embedded_type != EmbeddedTypeName::False
    }
}

pub fn compile(config: &CompilerConfig) -> io::Result<()> {
    let mut b = BundleContext::new(config);

    for (k, (v, module_purpose)) in config.bundle.iter() {
        if *module_purpose != Purpose::Codegen {
            continue;
        }

        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 generated = Map::new();

        b.generate_module(k, v, ModuleContextMode::TargetModule, &mut generated, |m| {
            for plugin in config.plugins.iter() {
                plugin.generate_module(m);
            }
        });

        for (n, d) in &v.definitions.0 {
            use ModuleContextMode::*;
            config.generate_definition(&mut b, k, v, n, d, TargetToplevel, &mut generated);
            config.generate_definition(&mut b, k, v, n, d, TargetGeneric, &mut generated);
        }

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

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

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

        let mut emit_items = |items: Vec<Item>| {
            if !items.is_empty() {
                lines.push(Formatter::to_string(vertical(true, seq(items))));
                lines.push("".to_owned());
            }
        };
        emit_items(generated.remove(&ModuleContextMode::TargetModule).unwrap());
        emit_items(generated.remove(&ModuleContextMode::TargetToplevel).unwrap());
        emit_items(generated.remove(&ModuleContextMode::TargetGeneric).unwrap());

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

    {
        let mut mod_rs = config.output_dir.clone();
        mod_rs.extend(vec!["mod.rs"]);
        let mut lines = Vec::new();

        for (modpath, (_, module_purpose)) in config.bundle.iter() {
            if *module_purpose != Purpose::Codegen {
                continue;
            }
            lines.push(format!("pub mod {};", names::render_modname(modpath.last().unwrap())));
        }
        lines.push("".to_owned());

        lines.push(format!("use {}::support as _support;", &config.support_crate));
        lines.push("use _support::preserves;".to_owned());
        lines.push("".to_owned());

        lines.push("#[allow(non_snake_case)]".to_owned());
        lines.push(Formatter::to_string(item(seq![
            "pub struct ", b.language_struct_name(), anglebrackets!["N: preserves::value::NestedValue"], " ",
            vertical(false, braces(b.literals.iter().map(
                |(value, name)| item(format!("pub {}: N /* {:?} */", name, value))).collect()))
        ])));
        lines.push("".to_owned());
        lines.push(Formatter::to_string(item(seq![
            "impl", anglebrackets!["N: preserves::value::NestedValue"],
            " Default for ", b.language_struct_name(), "<N> ", codeblock![
                seq!["fn default() -> Self ", codeblock![
                    seq![b.language_struct_name(), " ", vertical(false, braces(b.literals.iter().map(|(value, name)| {
                        let bs = preserves::value::PackedWriter::encode_iovalue(&value).unwrap();
                        item(format!("{}: /* {:?} */ _support::decode_lit(&vec!{:?}).unwrap()",
                                     name,
                                     value,
                                     bs))
                    }).collect()))]
                ]]
            ]
        ])));
        lines.push("".to_owned());
        {
            let mut b = Bundle { modules: Modules(Map::new()) };
            for (modpath, (schema, purpose)) in config.bundle.iter() {
                if *purpose == Purpose::Codegen {
                    b.modules.0.insert(ModulePath(modpath.clone()), schema.clone());
                }
            }
            let b_value = Language::default().unparse(&b);
            let b_bin = preserves::value::PackedWriter::encode_iovalue(&b_value).unwrap();
            let mut hex_encoded_bundle = String::new();
            let mut count = 0;
            for b in b_bin {
                if count % 16 == 0 {
                    hex_encoded_bundle.push_str("\\\n        ");
                }
                count += 1;
                hex_encoded_bundle.push_str(&format!("\\x{:02x}", b));
            }
            lines.push(Formatter::to_string(item(seq![
                "pub fn _bundle() -> &'static [u8] ", codeblock![
                    seq!["b\"", hex_encoded_bundle, "\""]
                ]
            ])));
        }
        lines.push("".to_owned());

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

    Ok(())
}
