use crate::*;
use crate::syntax::block::Item;
use crate::syntax::block::escape_string;
use crate::syntax::block::constructors::*;
use crate::gen::schema::*;

use convert_case::{Case, Casing};

use lazy_static::lazy_static;

use preserves::value::IOValue;
use preserves::value::Map;
use preserves::value::NestedValue;
use preserves::value::Value;

use super::CompilerConfig;
use super::names;
use super::types;
use super::types::Purpose;

pub struct BundleContext<'b> {
    pub config: &'b CompilerConfig,
    pub types: Map<Ref, types::TDefinition>,
    pub literals: Map<IOValue, String>,
}

#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub enum ModuleContextMode {
    TargetModule,
    TargetToplevel,
    TargetGeneric,
}

pub struct ModuleContext<'m, 'b> {
    pub bundle: &'m mut BundleContext<'b>,
    pub module_path: ModulePath,
    pub schema: &'m Schema,
    pub typedefs: Vec<Item>,
    pub functiondefs: Vec<Item>,
    pub mode: ModuleContextMode,
}

pub struct FunctionContext<'a, 'm, 'b> {
    pub error_context: String,
    pub m: &'a mut ModuleContext<'m, 'b>,
    pub temp_counter: usize,
    pub captures: Vec<Capture>,
    pub capture_mode: CaptureMode,
}

pub struct Capture {
    pub field_name: String,
    pub ty: types::TField,
    pub source_expr: String,
}

pub enum CaptureMode {
    Definite,
    Indefinite(Vec<Item>),
}

pub enum RefRenderStyle {
    Bare,
    Qualified,
}

lazy_static! {
    static ref ID_RE: regex::Regex = regex::Regex::new(r"^[a-zA-Z][a-zA-Z_0-9]*$").unwrap();
}

impl<'b> BundleContext<'b> {
    pub fn new(
        config: &'b CompilerConfig,
    ) -> Self {
        BundleContext {
            config,
            types: config.build_type_cache(),
            literals: Map::new(),
        }
    }

    pub fn any_type(&self) -> &'static str {
        "_Value"
    }

    pub fn lookup_definition(&self, r: &Ref) -> Option<(&Definition, Purpose)> {
        self.config.bundle.get(&r.module.0).and_then(
            |s| s.0.definitions.0.get(&r.name).map(|d| (d, s.1)))
    }

    pub fn type_for_name(&self, r: &Ref) -> Option<&types::TDefinition> {
        if r.module.0.is_empty() {
            panic!("BundleContext::type_for_name with module-relative ref {:?}", r);
        }
        let result = self.types.get(r);
        if result.is_none() && !self.config.external_modules.contains_key(&r.module.0) {
            panic!("Attempted to lookup unknown type {:?}", r)
        }
        result
    }

    pub fn define_literal(&mut self, v: &IOValue) -> String {
        let prefix = format!("LIT_{}", self.literals.len());
        let next_id = match v.value() {
            Value::Boolean(b) => prefix + "_" + &b.to_string(),
            Value::Symbol(s) => if ID_RE.is_match(&s) { prefix + "_" + s } else { prefix },
            Value::String(s) => if ID_RE.is_match(&s) { prefix + "_" + s } else { prefix },
            Value::SignedInteger(n) => prefix + "_" + &n.to_string(),
            _ => prefix
        };
        let next_id = next_id.to_case(Case::UpperSnake);
        format!("&<_L as Into<&'a {}>>::into(_ctxt).{}",
                self.language_type(),
                self.literals.entry(v.clone()).or_insert(next_id))
    }

    pub fn generate_module<F: FnOnce(&mut ModuleContext)>(
        &mut self,
        path: &Vec<String>,
        schema: &Schema,
        mode: ModuleContextMode,
        items: &mut Map<ModuleContextMode, Vec<Item>>,
        f: F,
    ) {
        let mut m = ModuleContext::new(self, &ModulePath(path.clone()), schema, mode);
        f(&mut m);
        items.entry(mode).or_default().extend(m.extract());
    }

    pub fn language_struct_name(&self) -> &'static str {
        "Language"
    }

    pub fn language_type_base(&self) -> String {
        format!("{}::{}",
                self.config.fully_qualified_module_prefix.clone(),
                self.language_struct_name())
    }

    pub fn language_type(&self) -> String {
        format!("{}<{}>", self.language_type_base(), self.any_type())
    }
}

impl<'m, 'b> ModuleContext<'m, 'b> {
    pub fn new(
        bundle: &'m mut BundleContext<'b>,
        module_path: &ModulePath,
        schema: &'m Schema,
        mode: ModuleContextMode,
    ) -> Self {
        ModuleContext {
            bundle,
            module_path: module_path.to_owned(),
            schema,
            typedefs: Vec::new(),
            functiondefs: Vec::new(),
            mode,
        }
    }

    pub fn any_type(&self) -> &'static str {
        self.bundle.any_type()
    }

    pub fn reset_mode(&mut self) {
        self.mode = ModuleContextMode::TargetToplevel;
    }

    pub fn define_literal(&mut self, v: &IOValue) -> String {
        self.bundle.define_literal(v)
    }

    pub fn define_type(&mut self, i: Item) {
        self.typedefs.push(i)
    }

    pub fn define_function<F: FnOnce(FunctionContext) -> Item>(&mut self, error_context: &str, f: F) {
        let i = f(FunctionContext::new(self, error_context));
        self.functiondefs.push(i)
    }

    pub fn render_ref(&self, r: &Ref, style: RefRenderStyle) -> Item {
        let base = match self.bundle.config.external_modules.get(&r.module.0) {
            None =>
                if r.module.0.is_empty() {
                    item(names::render_constructor(&r.name))
                } else {
                    let mut items = Vec::new();
                    items.push(item(self.bundle.config.fully_qualified_module_prefix.to_owned()));
                    for p in &r.module.0 { items.push(item(names::render_modname(p))) }
                    items.push(item(names::render_constructor(&r.name)));
                    item(name(items))
                }
            Some(xm) =>
                item(name![xm.rust_namespace.clone(), names::render_constructor(&r.name)])
        };
        let q = self.ref_has_embedded(r);
        match style {
            RefRenderStyle::Bare =>
                base,
            RefRenderStyle::Qualified =>
                if q { item(seq![base, anglebrackets![self.any_type()]]) } else { base },
        }
    }

    pub fn ref_has_embedded(&self, r: &Ref) -> bool {
        let r = r.qualify(&self.module_path);
        self.bundle.type_for_name(&r).map(|ty| ty.has_embedded(self.bundle)).unwrap_or(false)
        // ^ TODO: should the "false" be configurable?
    }

    pub fn parse_unparse_generic_decls(&self, ty: &types::TDefinition) -> Item {
        let mut lts = ty.language_types(self.bundle);
        lts.insert(self.bundle.language_type());
        item(anglebrackets![
            "'a",
            seq!["_L: Copy",
                 seq(lts.into_iter().map(|t| item(seq![" + Into<&'a ", t, ">"])).collect())],
            seq![self.any_type(), ": preserves::value::NestedValue + 'a"]])
    }

    pub fn extract(&mut self) -> Vec<Item> {
        let mut items = std::mem::take(&mut self.typedefs);
        items.extend(std::mem::take(&mut self.functiondefs));
        items
    }
}

impl<'a, 'm, 'b> FunctionContext<'a, 'm, 'b> {
    pub fn new(m: &'a mut ModuleContext<'m, 'b>, error_context: &str) -> Self {
        FunctionContext {
            error_context: error_context.to_owned(),
            m,
            temp_counter: 0,
            captures: Vec::new(),
            capture_mode: CaptureMode::Definite,
        }
    }

    pub fn capture(&mut self, field_name: String, ty: types::TField, source_expr: String) {
        self.captures.push(Capture {
            field_name,
            ty,
            source_expr: match self.capture_mode {
                CaptureMode::Definite =>
                    source_expr,
                CaptureMode::Indefinite(_) =>
                    format!("{}.ok_or_else(|| {:?})?", source_expr, self.conformance_err_code()),
            }
        })
    }

    pub fn lookup_capture(&self, field_name: &str) -> &Capture {
        for c in &self.captures {
            if c.field_name == field_name {
                return c;
            }
        }
        panic!("No capture for field {:?} available", field_name)
    }

    pub fn gentempname(&mut self) -> String {
        let i = self.temp_counter;
        self.temp_counter += 1;
        format!("_tmp{}", i)
    }

    pub fn declare_compound(&self, body: &mut Vec<Item>, name: &str, init_expr: Item) {
        body.push(item(seq!["let mut ", name.to_owned(), " = ", init_expr, ";"]));
    }

    pub fn define_atom(&mut self, body: &mut Vec<Item>, name: &str, val_expr: Item) {
        body.push(match &mut self.capture_mode {
            CaptureMode::Definite => item(seq!["let ", name.to_owned(), " = ", val_expr, ";"]),
            CaptureMode::Indefinite(items) => {
                items.push(item(seq!["let mut ", name.to_owned(), " = None;"]));
                item(seq![name.to_owned(), " = Some(", val_expr, ");"])
            }
        })
    }

    pub fn with_definite_mode<R, F: FnOnce(&mut Self) -> R>(&mut self, f: F) -> R {
        let saved_mode = std::mem::replace(&mut self.capture_mode, CaptureMode::Definite);
        let result = f(self);
        match std::mem::replace(&mut self.capture_mode, saved_mode) {
            CaptureMode::Definite => (),
            CaptureMode::Indefinite(_) => panic!("corrupt capture_mode"),
        }
        result
    }

    pub fn with_indefinite_mode<F: FnOnce(&mut Self) -> ()>(&mut self, f: F) -> Vec<Item> {
        let saved_mode = std::mem::replace(&mut self.capture_mode, CaptureMode::Indefinite(Vec::new()));
        f(self);
        match std::mem::replace(&mut self.capture_mode, saved_mode) {
            CaptureMode::Definite => panic!("corrupt capture_mode"),
            CaptureMode::Indefinite(declarations) => declarations,
        }
    }

    pub fn branch<R, F: FnOnce(&mut Self) -> R>(&mut self, f: F) -> R {
        let saved_temp_counter = self.temp_counter;
        let saved_capture_count = self.captures.len();
        let result = f(self);
        self.temp_counter = saved_temp_counter;
        self.captures.truncate(saved_capture_count);
        result
    }

    pub fn err_code(&self) -> Item {
        return item(seq!["Err", parens![self.conformance_err_code()]]);
    }

    pub fn fully_qualified_error_context(&self) -> String {
        self.m.module_path.0.join(".") + "." + &self.error_context
    }

    pub fn conformance_err_code(&self) -> Item {
        return item(seq!["_support::ParseError::conformance_error", parens![
            escape_string(&self.fully_qualified_error_context())]]);
    }
}
