use crate::context::{Ty, TyKind};

use super::*;

impl Context {
    pub(super) fn generate_to_json(
        &self,
        buf: &mut String,
        class: &Class,
        superclass: Option<&Union>,
    ) -> std::fmt::Result {
        if superclass.is_some() {
            writeln!(buf, "@override")?;
        }
        writeln!(buf, "Map<String, dynamic> toJson() => {{")?;

        for field in &class.fields {
            let json_key = self.library.json_key_for(class, field);
            let field_name = &field.name;

            if let Some(to_json) = &field.to_json {
                writeln!(buf, "\"{json_key}\": ")?;
                writeln!(buf, "// ignore: unnecessary_parenthesis")?;
                write!(buf, "({to_json})({field_name}),")?;
            } else {
                write!(buf, "\"{json_key}\": ")?;
                let ty = self.parse_ty(&field.ty).0.unwrap();
                self.write_expr_to_json(buf, field_name, &ty)?;
                writeln!(buf, ",")?;
            }
        }

        if let Some(union) = superclass {
            let discriminant_key = self.library.discriminant_key_for(union);
            let discriminant_value = self.library.discriminant_value_for(union, class);

            writeln!(buf, "\"{discriminant_key}\": {discriminant_value}",)?;
        }

        writeln!(buf, "}};")?;

        Ok(())
    }

    pub(super) fn generate_from_json(&self, buf: &mut String, class: &Class) -> std::fmt::Result {
        writeln!(
            buf,
            "factory {0}.fromJson(Map<String, dynamic> json) => {0}(",
            class.name
        )?;

        for field in &class.fields {
            let json_key = self.library.json_key_for(class, field);
            let field_ty = &field.ty;
            let field_name = &field.name;
            let expr = format!("json[\"{json_key}\"]");
            let ty = self.parse_ty(field_ty).0.unwrap();

            write!(buf, "{field_name}: ")?;

            // if a field has a default, we always check for null and then return the default if
            // null
            if let Some(defaults_to) = &field.defaults_to {
                let default = format_dart_literal_const(defaults_to);
                write!(buf, "{expr} == null ? {default} : ")?;
            } else if let Some(defaults_to_dart) = &field.defaults_to_dart {
                write!(buf, "{expr} == null ? {defaults_to_dart} : ")?;
            }

            if let Some(from_json) = &field.from_json {
                writeln!(buf)?;
                writeln!(buf, "// ignore: unnecessary_parenthesis")?;
                writeln!(buf, "({from_json})({expr})")?;
            } else {
                self.write_expr_from_json(buf, &expr, &ty)?;
            }
            writeln!(buf, ",")?;
        }

        writeln!(buf, ");")?;

        Ok(())
    }

    /// Some types need special handling to convert to JSON. The important cases are:
    /// - trivial values - Strings, ints, etc, - these are just `expr`
    /// - types we have generated - these are {expr}.toJson()
    /// - lists and sets are iterated over, then this function is applied to the inner expr, then
    ///   collected
    fn write_expr_to_json(&self, buf: &mut String, expr: &str, ty: &Ty) -> std::fmt::Result {
        match &ty.kind {
            // types generated by us
            TyKind::Simple(ident) if self.type_generated_by_us(ident) => {
                write!(buf, "{expr}.toJson()")?
            }
            // trivial types
            TyKind::Simple(_) => write!(buf, "{expr}")?,
            TyKind::List(inner) | TyKind::Set(inner) => {
                write!(buf, "{expr}.map((inner) => ")?;
                self.write_expr_to_json(buf, "inner", inner)?;
                write!(buf, ").toList()")?
            }
            TyKind::Map { value, .. } => {
                // we've already checked that `key` is a string
                //
                write!(buf, "{expr}.map((key, value) => MapEntry(key, ")?;
                self.write_expr_to_json(buf, "value", value)?;
                write!(buf, "))")?;
            }
            // TODO(cameron): this isn't really correct, but it works in common cases (e.g.
            // nullable strings, etc.)
            TyKind::Nullable(inner) => {
                match &inner.kind {
                    TyKind::Simple(ident) => match self.type_generated_by_us(ident) {
                        true => write!(buf, "{expr}?.toJson()")?,
                        false => write!(buf, "{expr}")?,
                    },
                    _ => self.write_expr_to_json(buf, expr, inner)?,
                };
            }
        }

        Ok(())
    }

    fn write_expr_from_json(&self, buf: &mut String, expr: &str, ty: &Ty) -> std::fmt::Result {
        match &ty.kind {
            // types generated by us
            TyKind::Simple(ident) if self.type_generated_by_us(ident) => {
                if self.library.enums.iter().any(|e| e.name.as_str() == ident) {
                    write!(buf, "{ident}.fromJson({expr})")?
                } else {
                    write!(buf, "{ident}.fromJson({expr} as Map<String, dynamic>)")?
                }
            }
            // trivial types
            TyKind::Simple(ident) => write!(buf, "{expr} as {ident}")?,
            TyKind::List(inner) => {
                write!(buf, "({expr} as List<dynamic>).map<{inner}>((inner) => ")?;
                self.write_expr_from_json(buf, "inner", inner)?;
                write!(buf, ").toList()")?
            }
            TyKind::Set(inner) => {
                write!(buf, "({expr} as List<dynamic>).map<{inner}>((inner) => ")?;
                self.write_expr_from_json(buf, "inner", inner)?;
                write!(buf, ").toSet()")?
            }
            TyKind::Map { value, .. } => {
                // we've already checked that `key` is a string
                //
                write!(
                    buf,
                    "({expr} as Map).map<String, {value}>((key, value) => MapEntry(key as String, "
                )?;
                self.write_expr_from_json(buf, "value", value)?;
                write!(buf, "))")?;
            }
            // TODO(cameron): rethink this - there is special handling for top-level nulls in the
            // fromJson code
            TyKind::Nullable(inner) => {
                write!(buf, "{expr} == null ? null : ")?;
                self.write_expr_from_json(buf, expr, inner)?;
            }
        }

        Ok(())
    }

    /// All types generated by this tool have `toJson` and `fromJson` functions.
    fn type_generated_by_us(&self, type_name: &str) -> bool {
        self.library.type_names().any(|s| **s == type_name)
    }
}
