use crate::{
    helpers::{doc_tokens, push_doc},
    naming::escape_ident,
};
use genco::prelude::*;
use js::Tokens;
use sails_idl_parser_v2::ast;

pub(crate) struct TypeGenerator;

impl TypeGenerator {
    pub(crate) fn new() -> Self {
        Self
    }

    pub(crate) fn render_all(&self, tokens: &mut Tokens, types: &[ast::Type]) {
        for ty in types {
            self.render_type(tokens, ty);
        }
    }

    pub(crate) fn render_type(&self, tokens: &mut Tokens, ty: &ast::Type) {
        let name = &ty.name;
        if name == "NonZeroU8"
            || name == "NonZeroU16"
            || name == "NonZeroU32"
            || name == "NonZeroU64"
            || name == "NonZeroU128"
            || name == "NonZeroU256"
        {
            return;
        }

        push_doc(tokens, &ty.docs);
        match &ty.def {
            ast::TypeDef::Struct(def) => {
                tokens.append(self.render_struct(&ty.name, &ty.type_params, def))
            }
            ast::TypeDef::Enum(def) => {
                tokens.append(self.render_enum(&ty.name, &ty.type_params, def))
            }
            ast::TypeDef::Alias(def) => {
                let generics = render_type_params(&ty.type_params);
                let target = self.ts_type_decl(&def.target);
                quote_in! { *tokens =>
                    export type $(&ty.name)$(generics) = $target;
                }
            }
        }
        tokens.line();
    }

    fn render_struct(
        &self,
        name: &str,
        type_params: &[ast::TypeParameter],
        def: &ast::StructDef,
    ) -> Tokens {
        let generics = render_type_params(type_params);
        let payload = self.ts_struct_def_tokens(def);
        if def.is_unit() || def.is_tuple() {
            quote! {
                export type $(name)$(generics) = $payload;
            }
        } else {
            quote! {
                export interface $(name)$(generics) $payload
            }
        }
    }

    fn render_enum(
        &self,
        name: &str,
        type_params: &[ast::TypeParameter],
        def: &ast::EnumDef,
    ) -> Tokens {
        let generics = render_type_params(type_params);
        let is_unit_only = def.variants.iter().all(|v| v.def.is_unit());

        if is_unit_only {
            quote! {
                export type $(name)$(generics) = $(for v in &def.variants join( | ) => $(quoted(&v.name)));
            }
        } else {
            let variant_tokens = def.variants.iter().map(|variant| {
                let docs = doc_tokens(&variant.docs);
                let payload = self.ts_struct_def_tokens(&variant.def);
                quote!({ $docs$(&variant.name): $payload })
            });

            quote! {
                export type $(name)$(generics) = $(for v in variant_tokens join ( | ) => $v);
            }
        }
    }

    pub(crate) fn ts_struct_def_tokens(&self, def: &ast::StructDef) -> Tokens {
        if def.is_unit() {
            quote! { null }
        } else if def.is_tuple() {
            if def.fields.len() == 1 {
                self.ts_type_decl(&def.fields[0].type_decl)
            } else {
                let tuple_types = def
                    .fields
                    .iter()
                    .map(|field| self.ts_type_decl(&field.type_decl));
                quote! { [$(for ty in tuple_types join (, ) => $ty)] }
            }
        } else {
            let fields = def.fields.iter().map(|field| {
                let field_name =
                    escape_ident(field.name.as_deref().expect("field name should be present"));
                let field_ty = self.ts_type_decl(&field.type_decl);
                quote! { $(field_name): $field_ty }
            });
            quote! { { $(for f in fields join (; ) => $f) } }
        }
    }

    pub(crate) fn ts_type_decl(&self, ty: &ast::TypeDecl) -> Tokens {
        match ty {
            ast::TypeDecl::Primitive(p) => match p {
                ast::PrimitiveType::Void => quote! { null },
                ast::PrimitiveType::Bool => quote! { boolean },
                ast::PrimitiveType::Char | ast::PrimitiveType::String => quote! { string },
                ast::PrimitiveType::I8
                | ast::PrimitiveType::I16
                | ast::PrimitiveType::I32
                | ast::PrimitiveType::I64
                | ast::PrimitiveType::U8
                | ast::PrimitiveType::U16
                | ast::PrimitiveType::U32
                | ast::PrimitiveType::U64 => quote! { number },
                ast::PrimitiveType::I128 | ast::PrimitiveType::U128 | ast::PrimitiveType::U256 => {
                    quote! { bigint }
                }
                ast::PrimitiveType::ActorId => {
                    let import = js::import("sails-js", "ActorId");
                    quote! { $import }
                }
                ast::PrimitiveType::CodeId => {
                    let import = js::import("sails-js", "CodeId");
                    quote! { $import }
                }
                ast::PrimitiveType::MessageId => {
                    let import = js::import("sails-js", "MessageId");
                    quote! { $import }
                }
                ast::PrimitiveType::H160 => {
                    let import = js::import("sails-js", "H160");
                    quote! { $import }
                }
                ast::PrimitiveType::H256 => {
                    let import = js::import("sails-js", "H256");
                    quote! { $import }
                }
            },
            ast::TypeDecl::Slice { item } => {
                let ty = self.ts_type_decl(item);
                quote! { $ty[] }
            }
            ast::TypeDecl::Array { item, len } => {
                if *len == 32
                    && matches!(
                        item.as_ref(),
                        ast::TypeDecl::Primitive(ast::PrimitiveType::U8)
                    )
                {
                    let rendered = "`0x${string}`".to_string();
                    quote! { $rendered }
                } else {
                    let ty = self.ts_type_decl(item);
                    quote! { $ty[] }
                }
            }
            ast::TypeDecl::Tuple { types } => {
                if types.is_empty() {
                    quote! { null }
                } else {
                    let tuple_types = types.iter().map(|t| self.ts_type_decl(t));
                    quote! { [$(for ty in tuple_types join (, ) => $ty)] }
                }
            }
            ast::TypeDecl::Generic { name } => quote! { $name },
            ast::TypeDecl::Named { name, generics } => {
                if name == "Option" && generics.len() == 1 {
                    let ty = self.ts_type_decl(&generics[0]);
                    return quote!($ty | null);
                }
                if name == "Result" && generics.len() == 2 {
                    let ok = self.ts_type_decl(&generics[0]);
                    let err = self.ts_type_decl(&generics[1]);
                    return quote!({ ok: $ok } | { err: $err });
                }
                if name == "NonZeroU8"
                    || name == "NonZeroU16"
                    || name == "NonZeroU32"
                    || name == "NonZeroU64"
                    || name == "NonZeroU128"
                    || name == "NonZeroU256"
                {
                    let import = js::import("sails-js", name);
                    return quote! { $import };
                }

                if generics.is_empty() {
                    quote! { $name }
                } else {
                    let generic_tokens = generics.iter().map(|g| self.ts_type_decl(g));
                    quote! { $name<$(for g in generic_tokens join (, ) => $g)> }
                }
            }
        }
    }
}

fn render_type_params(params: &[ast::TypeParameter]) -> String {
    if params.is_empty() {
        String::new()
    } else {
        format!(
            "<{}>",
            params
                .iter()
                .map(|p| p.name.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        )
    }
}
