use crate::{parser::docs, AccountField, AccountsStruct, Error, Program};
use heck::MixedCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
pub use serde_json;
use syn::{Ident, ItemEnum, ItemStruct};

#[inline(always)]
fn get_module_paths() -> (TokenStream, TokenStream) {
    (
        quote!(anchor_lang::anchor_syn::idl::types),
        quote!(anchor_lang::anchor_syn::idl::build::serde_json),
    )
}

#[inline(always)]
pub fn get_no_docs() -> bool {
    std::option_env!("ANCHOR_IDL_BUILD_NO_DOCS")
        .map(|val| val == "TRUE")
        .unwrap_or(false)
}

#[inline(always)]
pub fn get_seeds_feature() -> bool {
    std::option_env!("ANCHOR_IDL_BUILD_SEEDS_FEATURE")
        .map(|val| val == "TRUE")
        .unwrap_or(false)
}

// Returns TokenStream for IdlType enum and the syn::TypePath for the defined
// type if any.
// Returns Err when the type wasn't parsed successfully.
#[allow(clippy::result_unit_err)]
pub fn idl_type_ts_from_syn_type(
    ty: &syn::Type,
    type_params: &Vec<Ident>,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
    let (idl, _) = get_module_paths();

    fn the_only_segment_is(path: &syn::TypePath, cmp: &str) -> bool {
        if path.path.segments.len() != 1 {
            return false;
        };
        return path.path.segments.first().unwrap().ident == cmp;
    }

    // Foo<first::path, second::path> -> first::path
    fn get_first_angle_bracketed_path_arg(segment: &syn::PathSegment) -> Option<&syn::Type> {
        match &segment.arguments {
            syn::PathArguments::AngleBracketed(arguments) => match arguments.args.first() {
                Some(syn::GenericArgument::Type(ty)) => Some(ty),
                _ => None,
            },
            _ => None,
        }
    }

    match ty {
        syn::Type::Path(path) if the_only_segment_is(path, "bool") => {
            Ok((quote! { #idl::IdlType::Bool }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "u8") => {
            Ok((quote! { #idl::IdlType::U8 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "i8") => {
            Ok((quote! { #idl::IdlType::I8 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "u16") => {
            Ok((quote! { #idl::IdlType::U16 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "i16") => {
            Ok((quote! { #idl::IdlType::I16 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "u32") => {
            Ok((quote! { #idl::IdlType::U32 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "i32") => {
            Ok((quote! { #idl::IdlType::I32 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "f32") => {
            Ok((quote! { #idl::IdlType::F32 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "u64") => {
            Ok((quote! { #idl::IdlType::U64 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "i64") => {
            Ok((quote! { #idl::IdlType::I64 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "f64") => {
            Ok((quote! { #idl::IdlType::F64 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "u128") => {
            Ok((quote! { #idl::IdlType::U128 }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "i128") => {
            Ok((quote! { #idl::IdlType::I128 }, vec![]))
        }
        syn::Type::Path(path)
            if the_only_segment_is(path, "String") || the_only_segment_is(path, "&str") =>
        {
            Ok((quote! { #idl::IdlType::String }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "Pubkey") => {
            Ok((quote! { #idl::IdlType::PublicKey }, vec![]))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "Vec") => {
            let segment = path.path.segments.first().unwrap();
            let arg = match get_first_angle_bracketed_path_arg(segment) {
                Some(arg) => arg,
                None => unreachable!("Vec arguments can only be of AngleBracketed variant"),
            };
            match arg {
                syn::Type::Path(path) if the_only_segment_is(path, "u8") => {
                    return Ok((quote! {#idl::IdlType::Bytes}, vec![]));
                }
                _ => (),
            };
            let (inner, defined) = idl_type_ts_from_syn_type(arg, type_params)?;
            Ok((quote! { #idl::IdlType::Vec(Box::new(#inner)) }, defined))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "Option") => {
            let segment = path.path.segments.first().unwrap();
            let arg = match get_first_angle_bracketed_path_arg(segment) {
                Some(arg) => arg,
                None => unreachable!("Option arguments can only be of AngleBracketed variant"),
            };
            let (inner, defined) = idl_type_ts_from_syn_type(arg, type_params)?;
            Ok((quote! { #idl::IdlType::Option(Box::new(#inner)) }, defined))
        }
        syn::Type::Path(path) if the_only_segment_is(path, "Box") => {
            let segment = path.path.segments.first().unwrap();
            let arg = match get_first_angle_bracketed_path_arg(segment) {
                Some(arg) => arg,
                None => unreachable!("Box arguments can only be of AngleBracketed variant"),
            };
            let (ts, defined) = idl_type_ts_from_syn_type(arg, type_params)?;
            Ok((quote! { #ts }, defined))
        }
        syn::Type::Array(arr) => {
            let len = arr.len.clone();
            let len_is_generic = type_params.iter().any(|param| match len {
                syn::Expr::Path(ref path) => path.path.is_ident(param),
                _ => false,
            });

            let (inner, defined) = idl_type_ts_from_syn_type(&arr.elem, type_params)?;

            if len_is_generic {
                match len {
                    syn::Expr::Path(ref len) => {
                        let len = len.path.get_ident().unwrap().to_string();
                        Ok((
                            quote! { #idl::IdlType::GenericLenArray(Box::new(#inner), #len.into()) },
                            defined,
                        ))
                    }
                    _ => unreachable!("Array length can only be a generic parameter"),
                }
            } else {
                Ok((
                    quote! { #idl::IdlType::Array(Box::new(#inner), #len) },
                    defined,
                ))
            }
        }
        syn::Type::Path(path) => {
            let is_generic_param = type_params.iter().any(|param| path.path.is_ident(param));

            if is_generic_param {
                let generic = format!("{}", path.path.get_ident().unwrap());
                Ok((quote! { #idl::IdlType::Generic(#generic.into()) }, vec![]))
            } else {
                let mut params = vec![];
                let mut defined = vec![path.clone()];

                if let Some(segment) = &path.path.segments.last() {
                    if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments {
                        for arg in &args.args {
                            match arg {
                                syn::GenericArgument::Type(ty) => {
                                    let (ts, def) = idl_type_ts_from_syn_type(ty, type_params)?;
                                    params.push(quote! { #idl::IdlDefinedTypeArg::Type(#ts) });
                                    defined.extend(def);
                                }
                                syn::GenericArgument::Const(c) => params.push(
                                    quote! { #idl::IdlDefinedTypeArg::Value(format!("{}", #c))},
                                ),
                                _ => (),
                            }
                        }
                    }
                }

                if !params.is_empty() {
                    let params = quote! { vec![#(#params),*] };
                    Ok((
                        quote! { #idl::IdlType::DefinedWithTypeArgs {
                            name: <#path>::__anchor_private_full_path(),
                            args: #params
                        } },
                        defined,
                    ))
                } else {
                    Ok((
                        quote! { #idl::IdlType::Defined(<#path>::__anchor_private_full_path()) },
                        vec![path.clone()],
                    ))
                }
            }
        }
        syn::Type::Reference(reference) => match reference.elem.as_ref() {
            syn::Type::Slice(slice) if matches!(&*slice.elem, syn::Type::Path(path) if the_only_segment_is(path, "u8")) =>
            {
                return Ok((quote! {#idl::IdlType::Bytes}, vec![]));
            }
            _ => panic!("Reference types other than byte slice(`&[u8]`) are not allowed"),
        },
        _ => Err(()),
    }
}

// Returns TokenStream for IdlField struct and the syn::TypePath for the defined
// type if any.
// Returns Err when the type wasn't parsed successfully
#[allow(clippy::result_unit_err)]
pub fn idl_field_ts_from_syn_field(
    field: &syn::Field,
    no_docs: bool,
    type_params: &Vec<syn::Ident>,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
    let (idl, _) = get_module_paths();

    let name = field.ident.as_ref().unwrap().to_string().to_mixed_case();
    let docs = match docs::parse(&field.attrs) {
        Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
        _ => quote! {None},
    };
    let (ty, defined) = idl_type_ts_from_syn_type(&field.ty, type_params)?;

    Ok((
        quote! {
            #idl::IdlField {
                name: #name.into(),
                docs: #docs,
                ty: #ty,
            }
        },
        defined,
    ))
}

// Returns TokenStream for IdlEventField struct and the syn::TypePath for the defined
// type if any.
// Returns Err when the type wasn't parsed successfully
#[allow(clippy::result_unit_err)]
pub fn idl_event_field_ts_from_syn_field(
    field: &syn::Field,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
    let (idl, _) = get_module_paths();

    let name = field.ident.as_ref().unwrap().to_string().to_mixed_case();
    let (ty, defined) = idl_type_ts_from_syn_type(&field.ty, &vec![])?;

    let index: bool = field
        .attrs
        .get(0)
        .and_then(|attr| attr.path.segments.first())
        .map(|segment| segment.ident == "index")
        .unwrap_or(false);

    Ok((
        quote! {
            #idl::IdlEventField {
                name: #name.into(),
                ty: #ty,
                index: #index,
            }
        },
        defined,
    ))
}

// Returns TokenStream for IdlTypeDefinitionTy::Struct and Vec<&syn::TypePath>
// for the defined types if any.
// Returns Err if any of the fields weren't parsed successfully.
#[allow(clippy::result_unit_err)]
pub fn idl_type_definition_ts_from_syn_struct(
    item_strct: &syn::ItemStruct,
    no_docs: bool,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
    let (idl, _) = get_module_paths();

    let docs = match docs::parse(&item_strct.attrs) {
        Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
        _ => quote! {None},
    };

    let type_params = item_strct
        .generics
        .params
        .iter()
        .filter_map(|p| match p {
            syn::GenericParam::Type(ty) => Some(ty.ident.clone()),
            syn::GenericParam::Const(c) => Some(c.ident.clone()),
            _ => None,
        })
        .collect::<Vec<_>>();
    let (fields, defined): (Vec<TokenStream>, Vec<Vec<syn::TypePath>>) = match &item_strct.fields {
        syn::Fields::Named(fields) => fields
            .named
            .iter()
            .map(|f: &syn::Field| idl_field_ts_from_syn_field(f, no_docs, &type_params))
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .unzip::<_, _, Vec<_>, Vec<_>>(),
        _ => return Err(()),
    };
    let defined = defined
        .into_iter()
        .flatten()
        .collect::<Vec<syn::TypePath>>();

    let generics = if !type_params.is_empty() {
        let g: Vec<String> = type_params.iter().map(|id| id.to_string()).collect();
        quote! { Some(vec![#(#g.into()),*]) }
    } else {
        quote! { None }
    };

    Ok((
        quote! {
            #idl::IdlTypeDefinition {
                name: Self::__anchor_private_full_path(),
                generics: #generics,
                docs: #docs,
                ty: #idl::IdlTypeDefinitionTy::Struct{
                    fields: vec![
                        #(#fields),*
                    ]
                }
            },
        },
        defined,
    ))
}

// Returns TokenStream for IdlTypeDefinitionTy::Enum and the Vec<&syn::TypePath>
// for the defined types if any.
// Returns Err if any of the fields didn't parse successfully.
#[allow(clippy::result_unit_err)]
pub fn idl_type_definition_ts_from_syn_enum(
    enum_item: &syn::ItemEnum,
    no_docs: bool,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
    let (idl, _) = get_module_paths();

    let docs = match docs::parse(&enum_item.attrs) {
        Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
        _ => quote! {None},
    };

    let type_params = enum_item
        .generics
        .params
        .iter()
        .filter_map(|p| match p {
            syn::GenericParam::Type(ty) => Some(ty.ident.clone()),
            syn::GenericParam::Const(c) => Some(c.ident.clone()),
            _ => None,
        })
        .collect::<Vec<_>>();

    let (variants, defined): (Vec<TokenStream>, Vec<Vec<syn::TypePath>>) = enum_item.variants.iter().map(|variant: &syn::Variant| {
        let name = variant.ident.to_string();
        let (fields, defined): (TokenStream, Vec<syn::TypePath>) = match &variant.fields {
            syn::Fields::Unit => (quote!{None}, vec![]),
            syn::Fields::Unnamed(fields) => {
                let (types, defined) = fields.unnamed
                    .iter()
                    .map(|f| idl_type_ts_from_syn_type(&f.ty, &type_params))
                    .collect::<Result<Vec<_>, _>>()?
                    .into_iter()
                    .unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();
                let defined = defined
                    .into_iter()
                    .flatten()
                    .collect::<Vec<_>>();

                (quote!{ Some(#idl::EnumFields::Tuple(vec![#(#types),*]))}, defined)
            }
            syn::Fields::Named(fields) => {
                let (fields, defined) = fields.named
                    .iter()
                    .map(|f| idl_field_ts_from_syn_field(f, no_docs, &type_params))
                    .collect::<Result<Vec<_>, _>>()?
                    .into_iter()
                    .unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();
                let defined = defined
                    .into_iter()
                    .flatten()
                    .collect::<Vec<_>>();

                (quote!{ Some(#idl::EnumFields::Named(vec![#(#fields),*]))}, defined)
            }
        };

        Ok((quote!{ #idl::IdlEnumVariant{ name: #name.into(), fields: #fields }}, defined))
    })
    .collect::<Result<Vec<_>, _>>()?
    .into_iter()
    .unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();

    let defined = defined
        .into_iter()
        .flatten()
        .collect::<Vec<syn::TypePath>>();

    let generics = if !type_params.is_empty() {
        let g: Vec<String> = type_params.iter().map(|id| id.to_string()).collect();
        quote! { Some(vec![#(#g.into()),*]) }
    } else {
        quote! { None }
    };

    Ok((
        quote! {
            #idl::IdlTypeDefinition {
                name: Self::__anchor_private_full_path(),
                generics: #generics,
                docs: #docs,
                ty: #idl::IdlTypeDefinitionTy::Enum{
                    variants: vec![
                        #(#variants),*
                    ]
                }
            }
        },
        defined,
    ))
}

pub fn idl_gen_impl_skeleton(
    idl_type_definition_ts: TokenStream,
    insert_defined_ts: TokenStream,
    ident: &Ident,
    input_generics: &syn::Generics,
) -> TokenStream {
    let (idl, _) = get_module_paths();
    let name = ident.to_string();
    let (impl_generics, ty_generics, where_clause) = input_generics.split_for_impl();

    quote! {
        impl #impl_generics #ident #ty_generics #where_clause {
            pub fn __anchor_private_full_path() -> String {
                format!("{}::{}", std::module_path!(), #name)
            }

            pub fn __anchor_private_gen_idl_type() -> Option<#idl::IdlTypeDefinition> {
                #idl_type_definition_ts
            }

            pub fn __anchor_private_insert_idl_defined(
                defined_types: &mut std::collections::HashMap<String, #idl::IdlTypeDefinition>
            ) {
                #insert_defined_ts
            }
        }
    }
}

// generates the IDL generation impl for for a struct
pub fn gen_idl_gen_impl_for_struct(strct: &ItemStruct, no_docs: bool) -> TokenStream {
    let idl_type_definition_ts: TokenStream;
    let insert_defined_ts: TokenStream;

    if let Ok((ts, defined)) = idl_type_definition_ts_from_syn_struct(strct, no_docs) {
        idl_type_definition_ts = quote! {Some(#ts)};
        insert_defined_ts = quote! {
            #({
                <#defined>::__anchor_private_insert_idl_defined(defined_types);

                let path = <#defined>::__anchor_private_full_path();
                <#defined>::__anchor_private_gen_idl_type()
                    .and_then(|ty| defined_types.insert(path, ty));
            });*
        };
    } else {
        idl_type_definition_ts = quote! {None};
        insert_defined_ts = quote! {};
    }

    let ident = &strct.ident;
    let input_generics = &strct.generics;

    idl_gen_impl_skeleton(
        idl_type_definition_ts,
        insert_defined_ts,
        ident,
        input_generics,
    )
}

// generates the IDL generation impl for for an enum
pub fn gen_idl_gen_impl_for_enum(enm: ItemEnum, no_docs: bool) -> TokenStream {
    let idl_type_definition_ts: TokenStream;
    let insert_defined_ts: TokenStream;

    if let Ok((ts, defined)) = idl_type_definition_ts_from_syn_enum(&enm, no_docs) {
        idl_type_definition_ts = quote! {Some(#ts)};
        insert_defined_ts = quote! {
            #({
                <#defined>::__anchor_private_insert_idl_defined(defined_types);

                let path = <#defined>::__anchor_private_full_path();
                <#defined>::__anchor_private_gen_idl_type()
                    .and_then(|ty| defined_types.insert(path, ty));
            });*
        };
    } else {
        idl_type_definition_ts = quote! {None};
        insert_defined_ts = quote! {};
    }

    let ident = &enm.ident;
    let input_generics = &enm.generics;

    idl_gen_impl_skeleton(
        idl_type_definition_ts,
        insert_defined_ts,
        ident,
        input_generics,
    )
}

// generates the IDL generation impl for for an event
pub fn gen_idl_gen_impl_for_event(event_strct: &ItemStruct) -> TokenStream {
    fn parse_fields(
        fields: &syn::FieldsNamed,
    ) -> Result<(Vec<TokenStream>, Vec<syn::TypePath>), ()> {
        let (fields, defined) = fields
            .named
            .iter()
            .map(idl_event_field_ts_from_syn_field)
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .unzip::<_, _, Vec<_>, Vec<_>>();
        let defined = defined
            .into_iter()
            .flatten()
            .collect::<Vec<syn::TypePath>>();

        Ok((fields, defined))
    }

    let res = match &event_strct.fields {
        syn::Fields::Named(fields) => parse_fields(fields),
        _ => Err(()),
    };

    let (idl, _) = get_module_paths();
    let name = event_strct.ident.to_string();

    let (ret_ts, types_ts) = match res {
        Ok((fields, defined)) => {
            let ret_ts = quote! {
                Some(
                    #idl::IdlEvent {
                        name: #name.into(),
                        fields: vec![#(#fields),*],
                    }
                )
            };
            let types_ts = quote! {
                #({
                    <#defined>::__anchor_private_insert_idl_defined(defined_types);

                    let path = <#defined>::__anchor_private_full_path();
                    <#defined>::__anchor_private_gen_idl_type()
                        .and_then(|ty| defined_types.insert(path, ty));
                });*
            };
            (ret_ts, types_ts)
        }
        Err(()) => (quote! { None }, quote! {}),
    };

    let ident = &event_strct.ident;
    let input_generics = &event_strct.generics;
    let (impl_generics, ty_generics, where_clause) = input_generics.split_for_impl();

    quote! {
        impl #impl_generics #ident #ty_generics #where_clause {
            pub fn __anchor_private_gen_idl_event(
                defined_types: &mut std::collections::HashMap<String, #idl::IdlTypeDefinition>,
            ) -> Option<#idl::IdlEvent> {
                #types_ts
                #ret_ts
            }
        }
    }
}

// generates the IDL generation impl for the Accounts struct
pub fn gen_idl_gen_impl_for_accounts_strct(
    accs_strct: &AccountsStruct,
    no_docs: bool,
) -> TokenStream {
    let (idl, _) = get_module_paths();

    let ident = &accs_strct.ident;
    let (impl_generics, ty_generics, where_clause) = accs_strct.generics.split_for_impl();

    let (accounts, acc_types): (Vec<TokenStream>, Vec<Option<&syn::TypePath>>) = accs_strct
        .fields
        .iter()
        .map(|acc: &AccountField| match acc {
            AccountField::CompositeField(comp_f) => {
                let ty = if let syn::Type::Path(path) = &comp_f.raw_field.ty {
                    // some::path::Foo<'info> -> some::path::Foo
                    let mut res = syn::Path {
                        leading_colon: path.path.leading_colon,
                        segments: syn::punctuated::Punctuated::new(),
                    };
                    for segment in &path.path.segments {
                        let s = syn::PathSegment {
                            ident: segment.ident.clone(),
                            arguments: syn::PathArguments::None,
                        };
                        res.segments.push(s);
                    };
                    res
                } else {
                    panic!("expecting path")
                };
                let name = comp_f.ident.to_string().to_mixed_case();
                (quote!{
                    #idl::IdlAccountItem::IdlAccounts(#idl::IdlAccounts {
                        name: #name.into(),
                        accounts: <#ty>::__anchor_private_gen_idl_accounts(accounts, defined_types),
                    })
                }, None)
            }
            AccountField::Field(acc) => {
                let name = acc.ident.to_string().to_mixed_case();
                let is_mut = acc.constraints.is_mutable();
                let is_signer = match acc.ty {
                    crate::Ty::Signer => true,
                    _ => acc.constraints.is_signer()
                };
                let is_optional = if acc.is_optional { quote!{Some(true)} } else { quote!{None} };
                let docs = match &acc.docs {
                    Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
                    _ => quote! {None},
                };
                let pda = quote!{None}; // TODO
                let relations = super::parse::relations::parse(acc, get_seeds_feature());

                let acc_type_path = match &acc.ty {
                    crate::Ty::Account(ty) => Some(&ty.account_type_path),
                    crate::Ty::AccountLoader(ty) => Some(&ty.account_type_path),
                    crate::Ty::InterfaceAccount(ty) => Some(&ty.account_type_path),
                    _ => None,
                };

                (quote!{
                    #idl::IdlAccountItem::IdlAccount(#idl::IdlAccount{
                        name: #name.into(),
                        is_mut: #is_mut,
                        is_signer: #is_signer,
                        is_optional: #is_optional,
                        docs: #docs,
                        pda: #pda,
                        relations: vec![#(#relations.into()),*],
                    })
                }, acc_type_path)
            }
        })
        .unzip::<TokenStream, Option<&syn::TypePath>, Vec<TokenStream>, Vec<Option<&syn::TypePath>>>();
    let acc_types = acc_types
        .into_iter()
        .flatten()
        .collect::<Vec<&syn::TypePath>>();

    quote! {
        impl #impl_generics #ident #ty_generics #where_clause {
            pub fn __anchor_private_gen_idl_accounts(
                accounts: &mut std::collections::HashMap<String, #idl::IdlTypeDefinition>,
                defined_types: &mut std::collections::HashMap<String, #idl::IdlTypeDefinition>,
            ) -> Vec<#idl::IdlAccountItem> {
                #({
                    <#acc_types>::__anchor_private_insert_idl_defined(defined_types);

                    let path = <#acc_types>::__anchor_private_full_path();
                    <#acc_types>::__anchor_private_gen_idl_type()
                        .and_then(|ty| accounts.insert(path, ty));

                });*

                vec![#(#accounts),*]
            }
        }
    }
}

// generates the IDL generation print function for the program module
pub fn gen_idl_print_function_for_program(program: &Program, no_docs: bool) -> TokenStream {
    let (idl, serde_json) = get_module_paths();

    let (instructions, defined) = program
        .ixs
        .iter()
        .flat_map(|ix| -> Result<_, ()> {
            let name = ix.ident.to_string().to_mixed_case();
            let docs = match &ix.docs {
                Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
                _ => quote! {None},
            };
            let ctx_ident = &ix.anchor_ident;

            let (args, mut defined) = ix
                .args
                .iter()
                .map(|arg| {
                    let arg_name = arg.name.to_string().to_mixed_case();
                    let docs = match docs::parse(&arg.raw_arg.attrs) {
                        Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
                        _ => quote! {None},
                    };
                    let (ty, defined) = idl_type_ts_from_syn_type(&arg.raw_arg.ty, &vec![])?;

                    Ok((quote! {
                        #idl::IdlField {
                            name: #arg_name.into(),
                            docs: #docs,
                            ty: #ty,
                        }
                    }, defined))
                })
                .collect::<Result<Vec<_>, ()>>()?
                .into_iter()
                .unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();

            let returns = match idl_type_ts_from_syn_type(&ix.returns.ty, &vec![]) {
                Ok((ty, def)) => {
                    defined.push(def);
                    quote!{ Some(#ty) }
                },
                Err(()) => quote!{ None }
            };

            Ok((quote! {
                #idl::IdlInstruction {
                    name: #name.into(),
                    docs: #docs,
                    accounts: #ctx_ident::__anchor_private_gen_idl_accounts(
                        &mut accounts,
                        &mut defined_types,
                    ),
                    args: vec![#(#args),*],
                    returns: #returns,
                }
            }, defined))
        })
        .unzip::<TokenStream, Vec<Vec<syn::TypePath>>, Vec<TokenStream>, Vec<Vec<Vec<syn::TypePath>>>>();
    let defined = defined
        .into_iter()
        .flatten()
        .flatten()
        .collect::<Vec<syn::TypePath>>();

    let name = program.name.to_string();
    let docs = match &program.docs {
        Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])},
        _ => quote! {None},
    };

    quote! {
        #[test]
        pub fn __anchor_private_print_idl_program() {
            let mut accounts: std::collections::HashMap<String, #idl::IdlTypeDefinition> =
                std::collections::HashMap::new();
            let mut defined_types: std::collections::HashMap<String, #idl::IdlTypeDefinition> =
                std::collections::HashMap::new();

            #({
                <#defined>::__anchor_private_insert_idl_defined(&mut defined_types);

                let path = <#defined>::__anchor_private_full_path();
                <#defined>::__anchor_private_gen_idl_type()
                    .and_then(|ty| defined_types.insert(path, ty));
            });*

            let instructions = vec![#(#instructions),*];

            let idl = #idl::Idl {
                version: env!("CARGO_PKG_VERSION").into(),
                name: #name.into(),
                docs: #docs,
                constants: vec![],
                instructions,
                accounts: accounts.into_values().collect(),
                types: defined_types.into_values().collect(),
                events: None,
                errors: None,
                metadata: None,
            };

            println!("---- IDL begin program ----");
            println!("{}", #serde_json::to_string_pretty(&idl).unwrap());
            println!("---- IDL end program ----");
        }
    }
}

pub fn gen_idl_print_function_for_event(event: &ItemStruct) -> TokenStream {
    let (idl, serde_json) = get_module_paths();

    let ident = &event.ident;
    let fn_name = format_ident!("__anchor_private_print_idl_event_{}", ident.to_string());
    let impl_gen = gen_idl_gen_impl_for_event(event);

    quote! {
        #impl_gen

        #[test]
        pub fn #fn_name() {
            let mut defined_types: std::collections::HashMap<String, #idl::IdlTypeDefinition> = std::collections::HashMap::new();
            let event = #ident::__anchor_private_gen_idl_event(&mut defined_types);

            if let Some(event) = event {
                let json = #serde_json::json!({
                    "event": event,
                    "defined_types": defined_types.into_values().collect::<Vec<#idl::IdlTypeDefinition>>()
                });

                println!("---- IDL begin event ----");
                println!("{}", #serde_json::to_string_pretty(&json).unwrap());
                println!("---- IDL end event ----");
            }
        }
    }
}

pub fn gen_idl_print_function_for_constant(item: &syn::ItemConst) -> TokenStream {
    let fn_name = format_ident!(
        "__anchor_private_print_idl_const_{}",
        item.ident.to_string()
    );
    let (idl, serde_json) = get_module_paths();

    let name = item.ident.to_string();
    let expr = &item.expr;

    let impl_ts = match idl_type_ts_from_syn_type(&item.ty, &vec![]) {
        Ok((ty, _)) => quote! {
            let value = format!("{:?}", #expr);

            let idl = #idl::IdlConst {
                name: #name.into(),
                ty: #ty,
                value,
            };

            println!("---- IDL begin const ----");
            println!("{}", #serde_json::to_string_pretty(&idl).unwrap());
            println!("---- IDL end const ----");
        },
        Err(()) => quote! {},
    };

    quote! {
        #[test]
        pub fn #fn_name() {
            #impl_ts
        }
    }
}

pub fn gen_idl_print_function_for_error(error: &Error) -> TokenStream {
    let fn_name = format_ident!(
        "__anchor_private_print_idl_error_{}",
        error.ident.to_string()
    );
    let (idl, serde_json) = get_module_paths();

    let error_codes = error
        .codes
        .iter()
        .map(|code| {
            let id = code.id;
            let name = code.ident.to_string();

            let msg = match code.msg.clone() {
                Some(msg) => quote! { Some(#msg.to_string()) },
                None => quote! { None },
            };

            quote! {
                #idl::IdlErrorCode {
                    code: anchor_lang::error::ERROR_CODE_OFFSET + #id,
                    name: #name.into(),
                    msg: #msg,
                }
            }
        })
        .collect::<Vec<TokenStream>>();

    quote! {
        #[test]
        pub fn #fn_name() {
            let errors = vec![#(#error_codes),*];

            println!("---- IDL begin errors ----");
            println!("{}", #serde_json::to_string_pretty(&errors).unwrap());
            println!("---- IDL end errors ----");
        }
    }
}
