use syn::parse_quote;
use syn::punctuated::Punctuated;

use crate::ir::FnIR;
use crate::{ir::ModuleImplIR, utils};

#[derive(syn_derive::ToTokens)]
pub struct HasEntrypointsImplItem {
    impl_token: syn::token::Impl,
    has_ident_ty: syn::Type,
    for_token: syn::token::For,
    module_ident: syn::Ident,
    #[syn(braced)]
    brace_token: syn::token::Brace,
    #[syn(in = brace_token)]
    events_fn: EntrypointsFnItem
}

impl TryFrom<&'_ ModuleImplIR> for HasEntrypointsImplItem {
    type Error = syn::Error;

    fn try_from(struct_ir: &'_ ModuleImplIR) -> Result<Self, Self::Error> {
        Ok(Self {
            impl_token: Default::default(),
            has_ident_ty: utils::ty::has_entrypoints(),
            for_token: Default::default(),
            module_ident: struct_ir.module_ident()?,
            brace_token: Default::default(),
            events_fn: struct_ir.try_into()?
        })
    }
}

#[derive(syn_derive::ToTokens)]
pub struct EntrypointsFnItem {
    sig: syn::Signature,
    #[syn(braced)]
    brace_token: syn::token::Brace,
    #[syn(in = brace_token)]
    expr: syn::Expr
}

impl TryFrom<&'_ ModuleImplIR> for EntrypointsFnItem {
    type Error = syn::Error;

    fn try_from(struct_ir: &'_ ModuleImplIR) -> Result<Self, Self::Error> {
        let ident_entrypoints = utils::ident::entrypoints();
        let entrypoint_ty = utils::ty::entry_point_def();
        let expr = struct_entrypoints_expr(struct_ir)?;
        let vec_ty = utils::ty::vec_of(&entrypoint_ty);

        Ok(Self {
            sig: parse_quote!(fn #ident_entrypoints() -> #vec_ty),
            brace_token: Default::default(),
            expr
        })
    }
}

fn struct_entrypoints_expr(ir: &ModuleImplIR) -> syn::Result<syn::Expr> {
    let struct_entrypoints = ir
        .functions()?
        .iter()
        .map(|f| {
            let ident = f.name_str();
            let args = entrypoint_args(f)?;
            let is_mut = f.is_mut();
            let ret = match f.return_type() {
                syn::ReturnType::Default => utils::expr::unit_cl_type(),
                syn::ReturnType::Type(_, ty) => utils::expr::as_cl_type(&ty)
            };
            let ty = f
                .is_restricted()
                .then(utils::ty::entry_point_def_ty_constructor)
                .unwrap_or_else(utils::ty::entry_point_def_ty_public);
            let is_payable_attr = f.is_payable().then(utils::ty::entry_point_def_attr_payable);
            let is_non_reentrant = f
                .is_non_reentrant()
                .then(utils::ty::entry_point_def_attr_non_reentrant);
            let attributes = vec![is_payable_attr, is_non_reentrant]
                .into_iter()
                .flatten()
                .collect::<Punctuated<_, syn::token::Comma>>();
            let attributes = utils::expr::vec(attributes);
            let ty_entrypoint = utils::ty::entry_point_def();
            let name = utils::expr::string_from(ident);

            let expr: syn::Expr = parse_quote!(#ty_entrypoint {
                name: #name,
                args: #args,
                is_mutable: #is_mut,
                return_ty: #ret,
                ty: #ty,
                attributes: #attributes
            });
            Ok(expr)
        })
        .collect::<syn::Result<syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>>>()?;
    Ok(utils::expr::vec(struct_entrypoints))
}

fn entrypoint_args(f: &FnIR) -> syn::Result<syn::Expr> {
    let args = f
        .named_args()
        .iter()
        .map(|arg| {
            let ident = arg.name_str()?;
            let ty = utils::ty::unreferenced_ty(&arg.ty()?);
            Ok(utils::expr::into_arg(ty, ident))
        })
        .collect::<syn::Result<Punctuated<syn::Expr, syn::token::Comma>>>()?;
    Ok(utils::expr::vec(args))
}

#[cfg(test)]
mod test {
    use crate::test_utils;
    use quote::quote;

    use super::HasEntrypointsImplItem;

    #[test]
    fn test_entrypoints() {
        let module = test_utils::mock::module_impl();
        let expected = quote!(
            impl odra::contract_def::HasEntrypoints for Erc20 {
                fn entrypoints() -> odra::prelude::vec::Vec<odra::contract_def::Entrypoint> {
                    odra::prelude::vec![
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("init"),
                            args: odra::prelude::vec![
                                odra::args::odra_argument::<Option<U256> >("total_supply")
                            ],
                            is_mutable: true,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Constructor,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("upgrade"),
                            args: odra::prelude::vec![
                                odra::args::odra_argument::<Option<U256> >("total_supply")
                            ],
                            is_mutable: true,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Constructor,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("total_supply"),
                            args: odra::prelude::vec![],
                            is_mutable: false,
                            return_ty: <U256 as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("pay_to_mint"),
                            args: odra::prelude::vec![],
                            is_mutable: true,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![odra::contract_def::EntrypointAttribute::Payable]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("approve"),
                            args: odra::prelude::vec![
                                odra::args::odra_argument::<Address>("to"),
                                odra::args::odra_argument::<U256>("amount"),
                                odra::args::odra_argument::<Maybe<String> >("msg")
                            ],
                            is_mutable: true,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![odra::contract_def::EntrypointAttribute::NonReentrant]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("airdrop"),
                            args: odra::prelude::vec![
                                odra::args::odra_argument::<odra::prelude::vec::Vec<Address> >("to"),
                                odra::args::odra_argument::<U256>("amount")
                            ],
                            is_mutable: false,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("swap"),
                            args: odra::prelude::vec![
                                odra::args::odra_argument::<Address>("to"),
                                odra::args::odra_argument::<U256>("amount")
                            ],
                            is_mutable: true,
                            return_ty: <U256 as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        }
                    ]
                }
            }
        );
        let actual = HasEntrypointsImplItem::try_from(&module).unwrap();
        test_utils::assert_eq(actual, expected);
    }

    #[test]
    fn test_trait_impl_entrypoints() {
        let module = test_utils::mock::module_trait_impl();
        let expected = quote!(
            impl odra::contract_def::HasEntrypoints for Erc20 {
                fn entrypoints() -> odra::prelude::vec::Vec<odra::contract_def::Entrypoint> {
                    odra::prelude::vec![
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("total_supply"),
                            args: odra::prelude::vec![],
                            is_mutable: false,
                            return_ty: <U256 as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("set_total_supply"),
                            args: odra::prelude::vec![],
                            is_mutable: true,
                            return_ty: <U256 as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("pay_to_mint"),
                            args: odra::prelude::vec![],
                            is_mutable: true,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![odra::contract_def::EntrypointAttribute::Payable]
                        }
                    ]
                }
            }
        );
        let actual = HasEntrypointsImplItem::try_from(&module).unwrap();
        test_utils::assert_eq(actual, expected);
    }

    #[test]
    fn test_delegated_entrypoints() {
        let module = test_utils::mock::module_delegation();
        let expected = quote!(
            impl odra::contract_def::HasEntrypoints for Erc20 {
                fn entrypoints() -> odra::prelude::vec::Vec<odra::contract_def::Entrypoint> {
                    odra::prelude::vec![
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("total_supply"),
                            args: odra::prelude::vec![],
                            is_mutable: false,
                            return_ty: <U256 as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("get_owner"),
                            args: odra::prelude::vec![],
                            is_mutable: false,
                            return_ty: <Address as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes : odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("set_owner"),
                            args: odra::prelude::vec![
                                odra::args::odra_argument::<Address>("new_owner")
                            ],
                            is_mutable: true,
                            return_ty: <() as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("name"),
                            args: odra::prelude::vec![],
                            is_mutable: false,
                            return_ty: <String as odra::casper_types::CLTyped >::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes : odra::prelude::vec![]
                        },
                        odra::contract_def::Entrypoint {
                            name: odra::prelude::string::String::from("symbol"),
                            args: odra::prelude::vec![],
                            is_mutable: false,
                            return_ty: <String as odra::casper_types::CLTyped>::cl_type(),
                            ty: odra::contract_def::EntrypointType::Public,
                            attributes: odra::prelude::vec![]
                        }
                    ]
                }
            }
        );
        let actual = HasEntrypointsImplItem::try_from(&module).unwrap();
        test_utils::assert_eq(actual, expected);
    }
}
