use crate::*;
use crate::gen::schema::*;
use crate::syntax::block::constructors::*;
use crate::syntax::block::{Emittable, Item, escape_string};

use std::cell::Cell;
use std::rc::Rc;

use super::context::{ModuleContextMode, ModuleContext, FunctionContext};
use super::names;
use super::types::*;

#[derive(Debug)]
pub struct UnparserPlugin;

impl compiler::Plugin for UnparserPlugin {
    fn generate_definition(&self, module_ctxt: &mut ModuleContext, definition_name: &str, definition: &Definition) {
        if let ModuleContextMode::TargetGeneric = module_ctxt.mode {
            gen_definition_unparser(module_ctxt, definition_name, definition)
        }
    }
}

type ValueSource = Option<String>;

#[derive(Clone)]
struct FieldsSink {
    finish: Item,
    vec_expr: Item,
    body: Vec<Item>,
}

#[derive(Clone)]
enum ValueSink {
    Normal,
    Fields(Rc<Cell<Option<FieldsSink>>>),
}

impl std::fmt::Debug for ValueSink {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        match self {
            ValueSink::Normal => write!(f, "ValueSink::Normal"),
            ValueSink::Fields(_) => write!(f, "ValueSink::Normal"),
        }
    }
}

#[derive(Debug)]
struct ValueContext {
    src: ValueSource,
    sink: ValueSink,
    is_struct: bool,
}

fn normal_none(is_struct: bool) -> ValueContext {
    ValueContext { src: None, sink: ValueSink::Normal, is_struct }
}

fn normal_src(src: String, is_struct: bool) -> ValueContext {
    ValueContext { src: Some(src), sink: ValueSink::Normal, is_struct }
}

pub fn gen_definition_unparser(m: &mut ModuleContext, n: &str, d: &Definition) {
    let ty = definition_type(&m.module_path, Purpose::Codegen, n, d);

    m.define_function(
        n,
        |mut ctxt| {
            let mut body = vec![];

            match d {
                Definition::Or { pattern_0, pattern_1, pattern_n } => {
                    let mut ps = vec![&**pattern_0, &**pattern_1];
                    ps.extend(pattern_n);
                    body.push(item(seq!["match self ", codeblock(ps.iter().map(
                        | NamedAlternative { variant_label: name, pattern: pat } | ctxt.branch(|ctxt| {
                            let ctorname = item(name![names::render_constructor(n), names::render_constructor(name)]);
                            let (patpat, vc) = destruct(ctxt, ctorname, false, &pattern_type(pat));
                            item(seq![patpat, " => ", pattern_unparser(ctxt, pat, &vc), ","])
                        })).collect())]));
                }
                Definition::And { pattern_0, pattern_1, pattern_n } => {
                    let mut ps = vec![&**pattern_0, &**pattern_1];
                    ps.extend(pattern_n);
                    let (patpat, vc) = destruct(&mut ctxt, item(names::render_constructor(n)), true, &record_type(&ps));
                    body.push(item(seq!["let ", patpat, " = self;"]));
                    body.push(item(seq![
                        "preserves::value::merge(vec!", brackets(ps.iter().map(
                            |p| named_pattern_unparser(&mut ctxt, p, &vc)).collect()
                        ), ").expect", parens![escape_string(&(
                            "merge of ".to_owned() + &ctxt.fully_qualified_error_context()))]]));
                }
                Definition::Pattern(p) => {
                    let (patpat, vc) = destruct(&mut ctxt, item(names::render_constructor(n)), true, &pattern_type(p));
                    body.push(item(seq!["let ", patpat, " = self;"]));
                    body.push(pattern_unparser(&mut ctxt, p, &vc));
                }
            }

            item(seq!["impl",
                      ctxt.m.parse_unparse_generic_decls(&ty),
                      " _support::Unparse", anglebrackets!["_L", ctxt.m.any_type()], " for ",
                      names::render_constructor(n), ty.generic_arg(ctxt.m), " ",
                      codeblock![
                          seq!["fn unparse(&self, _ctxt: _L) -> ", ctxt.m.any_type(), " ",
                               codeblock(body)]]])
        });
}

fn destruct(
    ctxt: &mut FunctionContext,
    ctorname: Item,
    is_struct: bool,
    ty: &TSimple,
) -> (impl Emittable, ValueContext) {
    match ty {
        TSimple::Field(TField::Unit) => (seq![ctorname], normal_none(is_struct)),
        TSimple::Field(_) => {
            let src = ctxt.gentempname();
            (seq![ctorname, parens![src.to_owned()]], normal_src(src, is_struct))
        }
        TSimple::Record(TRecord(fs)) => {
            for (fname, fty) in fs {
                let fsrc = ctxt.gentempname();
                ctxt.capture(names::render_fieldname(fname), fty.clone(), fsrc);
            }
            (seq![
                ctorname, " ", braces(ctxt.captures.iter().map(
                    |c| item(seq![c.field_name.clone(), ": ", c.source_expr.clone()])).collect())],
             normal_none(is_struct))
        }
    }
}

fn simple_pattern_unparser(
    ctxt: &mut FunctionContext,
    p: &SimplePattern,
    vc: &ValueContext,
) -> Item {
    let src = &vc.src;
    match p {
        SimplePattern::Any =>
            item(seq![src.as_ref().unwrap().to_owned(), ".clone()"]),
        SimplePattern::Atom { atom_kind: k } => {
            match &**k {
                AtomKind::Symbol =>
                    item(seq!["preserves::value::Value::symbol(", src.as_ref().unwrap().to_owned(), ").wrap()"]),
                AtomKind::ByteString =>
                    item(seq!["preserves::value::Value::ByteString(", src.as_ref().unwrap().to_owned(), ".clone()).wrap()"]),
                _ =>
                    item(seq!["preserves::value::Value::from(", src.as_ref().unwrap().to_owned(), ").wrap()"])
            }
        }
        SimplePattern::Embedded { .. } =>
            item(seq!["preserves::value::Value::Embedded(", src.as_ref().unwrap().to_owned(), ".clone()).wrap()"]),
        SimplePattern::Lit { value } =>
            item(seq![parens![ctxt.m.define_literal(value)], ".clone()"]),
        SimplePattern::Seqof { pattern } => {
            let mut fields_sink = sequenceify(ctxt, vc);
            let tmp = ctxt.gentempname();
            fields_sink.body.push(item(seq![
                "for ", tmp.to_owned(), " in ",
                src.as_ref().unwrap().to_owned(), " ", codeblock![
                    seq![fields_sink.vec_expr.clone(), ".push(",
                         simple_pattern_unparser(ctxt,
                                                 pattern,
                                                 &normal_src(tmp, true)), ");"]]]));
            finish(fields_sink)
        }
        SimplePattern::Setof { pattern } => {
            let tmp = ctxt.gentempname();
            item(seq!["preserves::value::Value::Set(",
                      src.as_ref().unwrap().to_owned(), ".iter().map(|", tmp.to_owned(), "| ",
                      simple_pattern_unparser(ctxt, pattern, &normal_src(tmp, true)),
                      ").collect()).wrap()"])
        }
        SimplePattern::Dictof { key, value } => {
            let tmp_key = ctxt.gentempname();
            let tmp_value = ctxt.gentempname();
            item(seq!["preserves::value::Value::Dictionary(",
                      src.as_ref().unwrap().to_owned(),
                      ".iter().map(|(", tmp_key.to_owned(), ", ", tmp_value.to_owned(), ")| ",
                      parens![simple_pattern_unparser(ctxt, key, &normal_src(tmp_key, true)),
                              simple_pattern_unparser(ctxt, value, &normal_src(tmp_value, true))],
                      ").collect()).wrap()"])
        }
        SimplePattern::Ref(_r) =>
            item(seq![src.as_ref().unwrap().to_owned(),
                      if vc.is_struct { "" } else { ".as_ref()" },
                      ".unparse(_ctxt)"]),
    }
}

fn named_pattern_unparser(
    ctxt: &mut FunctionContext,
    p: &NamedPattern,
    vc: &ValueContext,
) -> Item {
    match p {
        NamedPattern::Anonymous(p) =>
            pattern_unparser(ctxt, p, vc),
        NamedPattern::Named(b) => {
            let Binding { name, pattern} = &**b;
            let src = ctxt.lookup_capture(&names::render_fieldname(name)).source_expr.to_owned();
            simple_pattern_unparser(ctxt, pattern, &ValueContext {
                src: Some(src),
                sink: vc.sink.clone(),
                is_struct: vc.is_struct,
            })
        }
    }
}

fn pattern_unparser(
    ctxt: &mut FunctionContext,
    p: &Pattern,
    vc: &ValueContext,
) -> Item {
    match p {
        Pattern::SimplePattern(s) =>
            simple_pattern_unparser(ctxt, s, vc),
        Pattern::CompoundPattern(c) => {
            match &**c {
                CompoundPattern::Rec { label, fields } => {
                    let rtmp = ctxt.gentempname();
                    let mut body = Vec::new();
                    let init_expr = item(seq![
                        "preserves::value::Record(vec![",
                        named_pattern_unparser(ctxt, label, &normal_none(vc.is_struct)),
                        "])"]);
                    ctxt.declare_compound(&mut body, &rtmp, init_expr);
                    named_pattern_unparser(ctxt, fields, &ValueContext {
                        src: None,
                        sink: ValueSink::Fields(Rc::new(Cell::new(Some(FieldsSink {
                            finish: item(seq![rtmp.clone(), ".finish().wrap()"]),
                            vec_expr: item(seq![rtmp.clone(), ".fields_vec_mut()"]),
                            body,
                        })))),
                        is_struct: vc.is_struct,
                    })
                },
                CompoundPattern::Tuple { patterns } => {
                    let mut fields_sink = sequenceify(ctxt, vc);
                    fixed_sequence_parser(ctxt, patterns, &mut fields_sink, vc.is_struct);
                    finish(fields_sink)
                },
                CompoundPattern::TuplePrefix { fixed, variable } => {
                    let mut fields_sink = sequenceify(ctxt, vc);
                    fixed_sequence_parser(ctxt, fixed, &mut fields_sink, vc.is_struct);
                    named_pattern_unparser(ctxt, &promote(variable), &ValueContext {
                        src: vc.src.clone(),
                        sink: ValueSink::Fields(Rc::new(Cell::new(Some(fields_sink)))),
                        is_struct: true,
                    })
                },
                CompoundPattern::Dict { entries } => {
                    let dtmp = ctxt.gentempname();
                    let mut body = Vec::new();
                    ctxt.declare_compound(&mut body, &dtmp, item("preserves::value::Map::new()"));
                    for (key_lit, value_pat) in entries.0.iter() {
                        body.push(item(seq![dtmp.clone(), ".insert", parens![
                            seq![parens![ctxt.m.define_literal(key_lit)], ".clone()"],
                            named_pattern_unparser(ctxt, &promote(value_pat), &normal_none(vc.is_struct))], ";"]));
                    }
                    body.push(item(seq!["preserves::value::Value::Dictionary(", dtmp, ").wrap()"]));
                    item(codeblock(body))
                }
            }
        },
    }
}

fn sequenceify<'a>(
    ctxt: &mut FunctionContext,
    vc: &'a ValueContext,
) -> FieldsSink {
    match vc {
        ValueContext { sink: ValueSink::Fields(fields_sink), .. } =>
            (**fields_sink).take().unwrap(),
        _ => {
            let rtmp = ctxt.gentempname();
            let mut body = Vec::new();
            ctxt.declare_compound(&mut body, &rtmp, item("std::vec::Vec::new()"));
            FieldsSink {
                finish: item(seq![
                    "preserves::value::Value::Sequence", parens![rtmp.clone()], ".wrap()"]),
                vec_expr: item(rtmp),
                body,
            }
        }
    }
}

fn finish(mut fields_sink: FieldsSink) -> Item {
    fields_sink.body.push(fields_sink.finish);
    item(codeblock(fields_sink.body))
}

fn fixed_sequence_parser(
    ctxt: &mut FunctionContext,
    patterns: &Vec<NamedPattern>,
    fields_sink: &mut FieldsSink,
    is_struct: bool,
) {
    for p in patterns {
        fields_sink.body.push(item(seq![fields_sink.vec_expr.clone(), ".push", parens![
            named_pattern_unparser(ctxt, p, &normal_none(is_struct))], ";"]));
    }
}
