use crate::idl::*;
use crate::parser::{self, accounts, error, program};
use crate::{AccountsStruct, StateIx};
use anyhow::Result;
use heck::MixedCase;
use quote::ToTokens;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Read;
use std::path::Path;

const DERIVE_NAME: &str = "Accounts";

// Parse an entire interface file.
pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
    let mut file = File::open(&filename)?;

    let mut src = String::new();
    file.read_to_string(&mut src).expect("Unable to read file");

    let f = syn::parse_file(&src).expect("Unable to parse file");

    let p = program::parse(parse_program_mod(&f));

    let accs = parse_accounts(&f);
    let acc_names = {
        let mut acc_names = HashSet::new();
        for accs_strct in accs.values() {
            for a in accs_strct.account_tys(&accs)? {
                acc_names.insert(a);
            }
        }
        acc_names
    };

    let state = match p.state {
        None => None,
        Some(state) => match state.ctor_and_anchor {
            None => None, // State struct defined but no implementation
            Some((ctor, anchor_ident)) => {
                let mut methods = state
                    .impl_block_and_methods
                    .map(|(_impl_block, methods)| {
                        methods
                            .iter()
                            .map(|method: &StateIx| {
                                let name = method.ident.to_string().to_mixed_case();
                                let args = method
                                    .args
                                    .iter()
                                    .map(|arg| {
                                        let mut tts = proc_macro2::TokenStream::new();
                                        arg.raw_arg.ty.to_tokens(&mut tts);
                                        let ty = tts.to_string().parse().unwrap();
                                        IdlField {
                                            name: arg.name.to_string().to_mixed_case(),
                                            ty,
                                        }
                                    })
                                    .collect::<Vec<_>>();
                                let accounts_strct =
                                    accs.get(&method.anchor_ident.to_string()).unwrap();
                                let accounts = accounts_strct.idl_accounts(&accs);
                                IdlStateMethod {
                                    name,
                                    args,
                                    accounts,
                                }
                            })
                            .collect::<Vec<_>>()
                    })
                    .unwrap_or_default();
                let ctor = {
                    let name = "new".to_string();
                    let args = ctor
                        .sig
                        .inputs
                        .iter()
                        .filter(|arg| match arg {
                            syn::FnArg::Typed(pat_ty) => {
                                // TODO: this filtering should be donein the parser.
                                let mut arg_str = parser::tts_to_string(&pat_ty.ty);
                                arg_str.retain(|c| !c.is_whitespace());
                                !arg_str.starts_with("Context<")
                            }
                            _ => false,
                        })
                        .map(|arg: &syn::FnArg| match arg {
                            syn::FnArg::Typed(arg_typed) => {
                                let mut tts = proc_macro2::TokenStream::new();
                                arg_typed.ty.to_tokens(&mut tts);
                                let ty = tts.to_string().parse().unwrap();
                                IdlField {
                                    name: parser::tts_to_string(&arg_typed.pat).to_mixed_case(),
                                    ty,
                                }
                            }
                            _ => panic!("Invalid syntax"),
                        })
                        .collect();
                    let accounts_strct = accs.get(&anchor_ident.to_string()).unwrap();
                    let accounts = accounts_strct.idl_accounts(&accs);
                    IdlStateMethod {
                        name,
                        args,
                        accounts,
                    }
                };

                methods.insert(0, ctor);

                let strct = {
                    let fields = match state.strct.fields {
                        syn::Fields::Named(f_named) => f_named
                            .named
                            .iter()
                            .map(|f: &syn::Field| {
                                let mut tts = proc_macro2::TokenStream::new();
                                f.ty.to_tokens(&mut tts);
                                let ty = tts.to_string().parse().unwrap();
                                IdlField {
                                    name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
                                    ty,
                                }
                            })
                            .collect::<Vec<IdlField>>(),
                        _ => panic!("State must be a struct"),
                    };
                    IdlTypeDef {
                        name: state.name,
                        ty: IdlTypeDefTy::Struct { fields },
                    }
                };

                Some(IdlState { strct, methods })
            }
        },
    };
    let error = parse_error_enum(&f).map(|mut e| error::parse(&mut e));
    let error_codes = error.as_ref().map(|e| {
        e.codes
            .iter()
            .map(|code| IdlErrorCode {
                code: 100 + code.id,
                name: code.ident.to_string(),
                msg: code.msg.clone(),
            })
            .collect::<Vec<IdlErrorCode>>()
    });

    let instructions = p
        .ixs
        .iter()
        .map(|ix| {
            let args = ix
                .args
                .iter()
                .map(|arg| {
                    let mut tts = proc_macro2::TokenStream::new();
                    arg.raw_arg.ty.to_tokens(&mut tts);
                    let ty = tts.to_string().parse().unwrap();
                    IdlField {
                        name: arg.name.to_string().to_mixed_case(),
                        ty,
                    }
                })
                .collect::<Vec<_>>();
            // todo: don't unwrap
            let accounts_strct = accs.get(&ix.anchor_ident.to_string()).unwrap();
            let accounts = accounts_strct.idl_accounts(&accs);
            IdlIx {
                name: ix.ident.to_string().to_mixed_case(),
                accounts,
                args,
            }
        })
        .collect::<Vec<_>>();

    let events = parse_events(&f)
        .iter()
        .map(|e: &&syn::ItemStruct| {
            let fields = match &e.fields {
                syn::Fields::Named(n) => n,
                _ => panic!("Event fields must be named"),
            };
            let fields = fields
                .named
                .iter()
                .map(|f: &syn::Field| {
                    let index = match f.attrs.iter().next() {
                        None => false,
                        Some(i) => parser::tts_to_string(&i.path) == "index",
                    };
                    IdlEventField {
                        name: f.ident.clone().unwrap().to_string(),
                        ty: parser::tts_to_string(&f.ty).to_string().parse().unwrap(),
                        index,
                    }
                })
                .collect::<Vec<IdlEventField>>();

            IdlEvent {
                name: e.ident.to_string(),
                fields,
            }
        })
        .collect::<Vec<IdlEvent>>();

    // All user defined types.
    let mut accounts = vec![];
    let mut types = vec![];
    let ty_defs = parse_ty_defs(&f)?;

    let error_name = error.map(|e| e.name).unwrap_or_else(|| "".to_string());

    for ty_def in ty_defs {
        // Don't add the error type to the types or accounts sections.
        if ty_def.name != error_name {
            if acc_names.contains(&ty_def.name) {
                accounts.push(ty_def);
            } else if events.iter().position(|e| e.name == ty_def.name).is_none() {
                types.push(ty_def);
            }
        }
    }

    Ok(Idl {
        version: "0.0.0".to_string(),
        name: p.name.to_string(),
        state,
        instructions,
        types,
        accounts,
        events: if events.is_empty() {
            None
        } else {
            Some(events)
        },
        errors: error_codes,
        metadata: None,
    })
}

// Parse the main program mod.
fn parse_program_mod(f: &syn::File) -> syn::ItemMod {
    let mods = f
        .items
        .iter()
        .filter_map(|i| match i {
            syn::Item::Mod(item_mod) => {
                let mod_count = item_mod
                    .attrs
                    .iter()
                    .filter(|attr| attr.path.segments.last().unwrap().ident == "program")
                    .count();
                if mod_count != 1 {
                    return None;
                }
                Some(item_mod)
            }
            _ => None,
        })
        .collect::<Vec<_>>();
    if mods.len() != 1 {
        panic!("Did not find program attribute");
    }
    mods[0].clone()
}

fn parse_error_enum(f: &syn::File) -> Option<syn::ItemEnum> {
    f.items
        .iter()
        .filter_map(|i| match i {
            syn::Item::Enum(item_enum) => {
                let attrs_count = item_enum
                    .attrs
                    .iter()
                    .filter(|attr| {
                        let segment = attr.path.segments.last().unwrap();
                        segment.ident == "error"
                    })
                    .count();
                match attrs_count {
                    0 => None,
                    1 => Some(item_enum),
                    _ => panic!("Invalid syntax: one error attribute allowed"),
                }
            }
            _ => None,
        })
        .next()
        .cloned()
}

fn parse_events(f: &syn::File) -> Vec<&syn::ItemStruct> {
    f.items
        .iter()
        .filter_map(|i| match i {
            syn::Item::Struct(item_strct) => {
                let attrs_count = item_strct
                    .attrs
                    .iter()
                    .filter(|attr| {
                        let segment = attr.path.segments.last().unwrap();
                        segment.ident == "event"
                    })
                    .count();
                match attrs_count {
                    0 => None,
                    1 => Some(item_strct),
                    _ => panic!("Invalid syntax: one event attribute allowed"),
                }
            }
            _ => None,
        })
        .collect()
}

// Parse all structs implementing the `Accounts` trait.
fn parse_accounts(f: &syn::File) -> HashMap<String, AccountsStruct> {
    f.items
        .iter()
        .filter_map(|i| match i {
            syn::Item::Struct(i_strct) => {
                for attr in &i_strct.attrs {
                    if attr.tokens.to_string().contains(DERIVE_NAME) {
                        let strct = accounts::parse(i_strct);
                        return Some((strct.ident.to_string(), strct));
                    }
                }
                None
            }
            // TODO: parse manual implementations. Currently we only look
            //       for derives.
            _ => None,
        })
        .collect()
}

// Parse all user defined types in the file.
fn parse_ty_defs(f: &syn::File) -> Result<Vec<IdlTypeDef>> {
    f.items
        .iter()
        .filter_map(|i| match i {
            syn::Item::Struct(item_strct) => {
                for attr in &item_strct.attrs {
                    if attr.tokens.to_string().contains(DERIVE_NAME) {
                        return None;
                    }
                }
                if let syn::Visibility::Public(_) = &item_strct.vis {
                    let name = item_strct.ident.to_string();
                    let fields = match &item_strct.fields {
                        syn::Fields::Named(fields) => fields
                            .named
                            .iter()
                            .map(|f: &syn::Field| {
                                let mut tts = proc_macro2::TokenStream::new();
                                f.ty.to_tokens(&mut tts);
                                Ok(IdlField {
                                    name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
                                    ty: tts.to_string().parse()?,
                                })
                            })
                            .collect::<Result<Vec<IdlField>>>(),
                        _ => panic!("Only named structs are allowed."),
                    };

                    return Some(fields.map(|fields| IdlTypeDef {
                        name,
                        ty: IdlTypeDefTy::Struct { fields },
                    }));
                }
                None
            }
            syn::Item::Enum(enm) => {
                let name = enm.ident.to_string();
                let variants = enm
                    .variants
                    .iter()
                    .map(|variant: &syn::Variant| {
                        let name = variant.ident.to_string();
                        let fields = match &variant.fields {
                            syn::Fields::Unit => None,
                            syn::Fields::Unnamed(fields) => {
                                let fields: Vec<IdlType> =
                                    fields.unnamed.iter().map(to_idl_type).collect();
                                Some(EnumFields::Tuple(fields))
                            }
                            syn::Fields::Named(fields) => {
                                let fields: Vec<IdlField> = fields
                                    .named
                                    .iter()
                                    .map(|f: &syn::Field| {
                                        let name = f.ident.as_ref().unwrap().to_string();
                                        let ty = to_idl_type(f);
                                        IdlField { name, ty }
                                    })
                                    .collect();
                                Some(EnumFields::Named(fields))
                            }
                        };
                        EnumVariant { name, fields }
                    })
                    .collect::<Vec<EnumVariant>>();
                Some(Ok(IdlTypeDef {
                    name,
                    ty: IdlTypeDefTy::Enum { variants },
                }))
            }
            _ => None,
        })
        .collect()
}

fn to_idl_type(f: &syn::Field) -> IdlType {
    let mut tts = proc_macro2::TokenStream::new();
    f.ty.to_tokens(&mut tts);
    tts.to_string().parse().unwrap()
}
