use crate::ir::{FnType, ModuleImplIR};
use crate::utils::syn::visibility_default;
use crate::utils::ty;
use crate::{
    ast::fn_utils,
    ir::{FnArgIR, FnIR},
    utils::{self, syn::visibility_pub}
};
use quote::{ToTokens, format_ident};
use syn::{parse_quote, Attribute, Visibility};

pub fn host_try_function_item(fun: &FnIR) -> syn::ItemFn {
    let signature = try_function_signature(fun);
    let call_def_expr = call_def_with_amount(fun);
    let mut attrs = function_filtered_attrs(fun);
    attrs.push(parse_quote!(#[doc = " Does not fail in case of error, returns `odra::OdraResult` instead."]));

    env_call(signature, call_def_expr, attrs, visibility_pub())
}

pub fn factory_try_function_item(fun: &FnIR) -> syn::ItemFn {
    let signature = try_function_signature(fun);

    let call_def_expr = factory_call_def_with_amount(fun);
    let mut attrs = function_filtered_attrs(fun);
    attrs.push(parse_quote!(#[doc = " Does not fail in case of error, returns `odra::OdraResult` instead."]));

    env_call(signature, call_def_expr, attrs, visibility_pub())
}

pub fn host_function_item(fun: &FnIR, is_trait_impl: bool) -> syn::ItemFn {
    let pub_vis = match is_trait_impl {
        true => None,
        false => Some(visibility_pub())
    };
    let attrs = function_filtered_attrs(fun);
    let signature = function_signature(fun);
    let try_func_name = fun.try_name();
    let args = fun.arg_names();
    syn::parse_quote!(
        #(#attrs)*
        #pub_vis #signature {
            self.#try_func_name(#(#args),*).unwrap()
        }
    )
}

pub fn host_mut_no_ret_function_item(fun: &FnIR) -> syn::ItemFn {
    let mut attrs = function_filtered_attrs(fun);
    attrs.push(parse_quote!(#[doc = " Ignores the result of the call."]));
    let signature = function_signature(fun);
    let signature = syn::Signature {
        ident: format_ident!("{}_no_ret", fun.name()),
        output: syn::ReturnType::Default,
        ..signature
    };
    let try_func_name = fun.try_no_ret_name();
    let args = fun.arg_names();
    syn::parse_quote!(
        #(#attrs)*
        pub #signature {
            self.#try_func_name(#(#args),*).unwrap()
        }
    )
}

pub fn host_mut_try_no_ret_function_item(fun: &FnIR) -> syn::ItemFn {
    let mut attrs = function_filtered_attrs(fun);
    attrs.push(parse_quote!(#[doc = " Ignores the result of the call."]));
    let ret_ty = utils::ty::odra_result_unit();
    let signature = function_signature(fun);
    let signature = syn::Signature {
        ident: fun.try_no_ret_name(),
        output: syn::parse_quote!(-> #ret_ty),
        ..signature
    };
    let call_def_expr = call_def_with_amount(fun);
    env_call(signature, call_def_expr, attrs, visibility_pub())
}

pub fn contract_function_item(fun: &FnIR, is_trait_impl: bool) -> syn::ItemFn {
    let vis = match is_trait_impl {
        true => visibility_default(),
        false => visibility_pub()
    };
    let signature = function_signature(fun);
    let call_def_expr = call_def_with_amount(fun);
    let attrs = function_filtered_attrs(fun);

    env_call(signature, call_def_expr, attrs, vis)
}

pub fn factory_contract_function_item(fun: &FnIR, is_trait_impl: bool) -> syn::ItemFn {
    let vis = match is_trait_impl {
        true => visibility_default(),
        false => visibility_pub()
    };
    let signature = function_signature(fun);
    let call_def_expr = factory_call_def_with_amount(fun);
    let attrs = function_filtered_attrs(fun);

    env_call(signature, call_def_expr, attrs, vis)
}

fn env_call(
    sig: syn::Signature,
    call_def_expr: syn::Expr,
    docs: Vec<Attribute>,
    vis: Visibility
) -> syn::ItemFn {
    let m_env = utils::member::env();
    let m_address = utils::member::address();

    syn::parse_quote!(
        #(#docs)*
        #vis #sig {
            #m_env.call_contract(
                #m_address,
                #call_def_expr
            )
        }
    )
}

fn call_def_with_amount(fun: &FnIR) -> syn::Expr {
    let ty_call_def = utils::ty::call_def();
    let fun_name_str = fun.name_str();
    let args_block = runtime_args_with_amount_block(fun, insert_arg_stmt);
    let is_mut = fun.is_mut();
    let attached_value = utils::member::attached_value();
    let fun_name = utils::expr::string_from(fun_name_str);

    syn::parse_quote!(#ty_call_def::new(#fun_name, #is_mut, #args_block).with_amount(#attached_value))
}

fn factory_call_def_with_amount(fun: &FnIR) -> syn::Expr {
    let ty_call_def = utils::ty::call_def();
    let fun_name_str = fun.name_str();
    let new_runtime_args = utils::expr::new_runtime_args();
    let args = utils::ident::named_args();
    let is_mut = fun.is_mut();
    let fun_name = utils::expr::string_from(fun_name_str);
    let ty_ep_arg = utils::ty::entry_point_arg();
    let fn_args = fun
        .named_args()
        .iter()
        .map(|arg| {
            let ident = arg.name().unwrap();
            let name = ident.to_string();
            quote::quote!(#ty_ep_arg::insert_runtime_arg(#ident, #name, &mut #args);)
        })
        .collect::<Vec<_>>();
    let ty_args = utils::ty::batch_upgrade_args();
    match fun.fn_type() {
        FnType::FactoryUpgrader => syn::parse_quote!(#ty_call_def::new(#fun_name, #is_mut, {
            let mut #args = #new_runtime_args;
            #(#fn_args)*
            #ty_ep_arg::insert_runtime_arg(true, "odra_cfg_is_factory_upgrade", &mut #args);
            #ty_ep_arg::insert_runtime_arg(true, "odra_cfg_allow_key_override", &mut #args);
            #ty_ep_arg::insert_runtime_arg(false, "odra_cfg_create_upgrade_group", &mut #args);
            #args
        })),
        FnType::FactoryBatchUpgrader => syn::parse_quote!(#ty_call_def::new(#fun_name, #is_mut, {
            let mut #args = #new_runtime_args;
            #ty_ep_arg::insert_runtime_arg(#ty_args::from(args), "args", &mut #args);
            #ty_ep_arg::insert_runtime_arg(true, "odra_cfg_is_factory_upgrade", &mut #args);
            #ty_ep_arg::insert_runtime_arg(true, "odra_cfg_allow_key_override", &mut #args);
            #ty_ep_arg::insert_runtime_arg(false, "odra_cfg_create_upgrade_group", &mut #args);
            #args
        })),
        _ => syn::parse_quote!(#ty_call_def::new(#fun_name, #is_mut, {
            let mut #args = #new_runtime_args;
            #ty_ep_arg::insert_runtime_arg(true, "odra_cfg_is_upgradable", &mut #args);
            #ty_ep_arg::insert_runtime_arg(false, "odra_cfg_is_upgrade", &mut #args);
            #ty_ep_arg::insert_runtime_arg(true, "odra_cfg_allow_key_override", &mut #args);
            #ty_ep_arg::insert_runtime_arg(contract_name.clone(), "odra_cfg_package_hash_key_name", &mut #args);
            #(#fn_args)*
            #args
        }))
    }
}

fn function_signature(fun: &FnIR) -> syn::Signature {
    let fun_name = fun.name();
    let args = fun.typed_args();
    let return_type = fun.return_type();
    let generics = fun.generics();
    let mutability = fun.is_mut().then(|| quote::quote!(mut));

    syn::parse_quote!(fn #fun_name #generics(& #mutability self #(, #args)*) #return_type)
}

fn function_filtered_attrs(fun: &FnIR) -> Vec<syn::Attribute> {
    fun.attributes()
        .iter()
        .filter(|attr| !attr.path().is_ident("odra"))
        .cloned()
        .collect()
}

fn try_function_signature(fun: &FnIR) -> syn::Signature {
    let fun_name = fun.try_name();
    let args = fun.typed_args();
    let return_type = fun.try_return_type();
    let generics = fun.generics();
    let mutability = fun.is_mut().then(|| quote::quote!(mut));

    syn::parse_quote!(
        fn #fun_name #generics(& #mutability self #(, #args)*) #return_type)
}

fn runtime_args_with_amount_block<F: FnMut(&FnArgIR) -> syn::Stmt>(
    fun: &FnIR,
    insert_arg_fn: F
) -> syn::Block {
    let runtime_args = utils::expr::new_runtime_args();
    let args = utils::ident::named_args();
    let insert_amount = insert_amount_arg_stmt();
    let ty = utils::ty::entry_point_arg();

    let insert_args = match fun.fn_type() {
        FnType::FactoryBatchUpgrader => {
            let args_ty = utils::ty::batch_upgrade_args();
            vec![parse_quote!(#ty::insert_runtime_arg(#args_ty::from(args), "args", &mut #args);)]
        }
        FnType::Factory => fn_utils::insert_args_stmts(fun, insert_arg_fn),
        _ => fn_utils::insert_args_stmts(fun, insert_arg_fn)
    };

    syn::parse_quote!({
        let mut #args = #runtime_args;
        #insert_amount
        #(#insert_args)*
        #args
    })
}

fn insert_amount_arg_stmt() -> syn::Stmt {
    let ident = utils::ident::named_args();
    let zero = utils::expr::u512_zero();
    let attached_value = utils::member::attached_value();

    syn::parse_quote!(
        if #attached_value > #zero {
            let _ = #ident.insert("amount", #attached_value);
        }
    )
}

pub fn insert_arg_stmt(arg: &FnArgIR) -> syn::Stmt {
    let ident = arg.name().unwrap();
    let name = ident.to_string();
    let args = utils::ident::named_args();
    let ty = ty::entry_point_arg();
    syn::parse_quote!(#ty::insert_runtime_arg(#ident, #name, &mut #args);)
}

pub struct SchemaErrorsItem {
    ident: syn::Ident
}

impl TryFrom<&ModuleImplIR> for SchemaErrorsItem {
    type Error = syn::Error;

    fn try_from(ir: &ModuleImplIR) -> Result<Self, Self::Error> {
        Ok(Self {
            ident: ir.contract_ref_ident()?
        })
    }
}

impl ToTokens for SchemaErrorsItem {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let ident = &self.ident;
        let item = quote::quote!(
            #[automatically_derived]
            #[cfg(not(target_arch = "wasm32"))]
            impl odra::schema::SchemaErrors for #ident {}
        );
        item.to_tokens(tokens);
    }
}

pub struct SchemaEventsItem {
    ident: syn::Ident
}

impl TryFrom<&ModuleImplIR> for SchemaEventsItem {
    type Error = syn::Error;

    fn try_from(ir: &ModuleImplIR) -> Result<Self, Self::Error> {
        Ok(Self {
            ident: ir.contract_ref_ident()?
        })
    }
}

impl ToTokens for SchemaEventsItem {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let ident = &self.ident;
        let item = quote::quote!(
            #[automatically_derived]
            #[cfg(not(target_arch = "wasm32"))]
            impl odra::schema::SchemaEvents for #ident {}
        );
        item.to_tokens(tokens);
    }
}
