use syn::{
    Item, ItemMod, ItemImpl, Type, TypePath,
    Ident, ItemStruct, ItemEnum, ItemFn,
    GenericArgument, parse_quote
};
use quote::quote;
use proc_macro2::Span;
use crate::{
    attr::{MsgAttr, Entry, CONTRACT},
    err::{ErrorSink, CompileErrors},
    generate::{self, MsgType, ErrorEnum},
    method::{Method, item_impl_methods}
};
pub fn derive(mut item_mod: ItemMod) -> Result<proc_macro2::TokenStream, CompileErrors> {
    let Some((_, items)) = &mut item_mod.content else {
        return Err(vec![
            syn::Error::new_spanned(
                &item_mod,
                "Contract mod definition must contain a block."
            )
        ]);
    };
    let mut sink = ErrorSink::default();
    let contract = Contract::parse(&mut sink, item_mod.ident.span(), items);
    let g = contract.generate(&mut sink);
    items.push(Item::Struct(g.boilerplate.contract_struct));
    items.push(Item::Enum(g.boilerplate.error_enum.enum_def));
    items.push(Item::Impl(g.boilerplate.error_enum.display_impl));
    items.push(Item::Impl(g.boilerplate.error_enum.err_impl));
    if let Some(i) = g.interfaces {
        items.push(Item::Struct(i.init_msg));
        items.push(Item::Enum(i.execute_msg));
        items.push(Item::Enum(i.query_msg));
    
        items.push(Item::Fn(i.entry.init));
        items.push(Item::Fn(i.entry.execute));
        items.push(Item::Fn(i.entry.query));
        if let Some(wasm) = i.entry.wasm_ffi {
            items.push(Item::Mod(wasm));
        }
    }
    sink.check()?;
    Ok(quote!(#item_mod))
}
struct Contract<'a> {
    contract_impl: Option<&'a ItemImpl>,
    interfaces: Vec<&'a ItemImpl>
}
struct Generated {
    interfaces: Option<Interfaces>,
    boilerplate: Boilerplate
}
struct Interfaces {
    init_msg: ItemStruct,
    execute_msg: ItemEnum,
    query_msg: ItemEnum,
    entry: Entrypoints
}
struct Entrypoints {
    init: ItemFn,
    execute: ItemFn,
    query: ItemFn,
    wasm_ffi: Option<ItemMod>
}
struct Boilerplate {
    contract_struct: ItemStruct,
    error_enum: ErrorEnum
}
impl<'a> Contract<'a> {
    fn parse(sink: &mut ErrorSink, mod_span: Span, items: &'a [Item]) -> Self {
        let mut contract_impl = None;
        let mut interfaces = vec![];
        for item in items {
            match item {
                Item::Impl(item) if is_interface_impl(item) => {
                    interfaces.push(item);
                }
                Item::Impl(item) if is_contract_impl(item) => {
                    if contract_impl.is_some() {
                        sink.push(
                            mod_span,
                            format!("Can only have a single \"impl {}\" item.", CONTRACT)
                        );
                    } else {
                        contract_impl = Some(item);
                    }
                }
                _ => { }
            }
        }
        Self {
            contract_impl,
            interfaces
        }
    }
    fn generate(self, sink: &mut ErrorSink) -> Generated {
        let mut init: Option<Method> = None;
        let mut execute: Vec<Method> = vec![];
        let mut query: Vec<Method> = vec![];
        let mut reply: Option<Method> = None;
        let mut execute_guard: Option<Method> = None;
        let mut contract_err_ty: Option<GenericArgument> = None;
        if let Some(contract_impl) = self.contract_impl {
            for method in item_impl_methods(sink, &contract_impl) {
                match &contract_err_ty {
                    Some(err_ty) => {
                        if err_ty != method.return_ty().error {
                            sink.push_spanned(
                                &method.sig().output,
                                format!(
                                    "All methods in the \"impl {}\" block must have the same error type.",
                                    CONTRACT
                                )
                            );
                        }
                    },
                    None => contract_err_ty = Some(method.return_ty().error.clone())
                }
                let ty = method.ty();
                match ty {
                    MsgAttr::Init { .. } if init.is_some() =>
                        sink.duplicate_annotation(&contract_impl.self_ty, ty),
                    MsgAttr::Init { entry } => {
                        if entry.is_some() {
                            init = Some(method);
                        } else {
                            sink.push_spanned(
                                &contract_impl.self_ty,
                                format!(
                                    "Init methods in {} implementation must have one of the following meta list parameters: {:?}",
                                    CONTRACT,
                                    [MsgAttr::ENTRY_META, MsgAttr::ENTRY_WASM_META]
                                )
                            )
                        }
                    }
                    MsgAttr::Execute => execute.push(method),
                    MsgAttr::Query => query.push(method),
                    MsgAttr::Reply => {
                        if reply.is_some() {
                            sink.duplicate_annotation(&contract_impl.self_ty, ty);
                        } else {
                            reply = Some(method);
                        }
                    }
                    MsgAttr::ExecuteGuard => {
                        if execute_guard.is_some() {
                            sink.duplicate_annotation(&contract_impl.self_ty, ty);
                        } else {
                            execute_guard = Some(method);
                        }
                    }
                };
            }
        }
        for interface in &self.interfaces {
            let mut has_init = false;
            for method in item_impl_methods(sink, interface) {
                let ty = method.ty();
                match method.ty() {
                    MsgAttr::Init { .. } if has_init =>
                        sink.duplicate_annotation(interface, ty),
                    MsgAttr::Init { entry } if entry.is_some() && init.is_some() =>
                        sink.push_spanned(
                            method.sig(),
                            "Entry point already defined."
                        ),
                    MsgAttr::Init { entry } => {
                        if entry.is_some() {
                            init = Some(method);
                            has_init = true;
                        }
                    }
                    MsgAttr::Execute => execute.push(method),
                    MsgAttr::Query => query.push(method),
                    unsupported => sink.unsupported_interface_attr(
                        &method.sig().ident,
                        unsupported
                    )
                }
            }
        }
        let interfaces = if let Some(init) = init {
            let entry = Entrypoints {
                init: generate::init_fn(
                    sink,
                    &init
                ),
                execute: generate::execute_fn(
                    sink,
                    &execute,
                    execute_guard
                ),
                query: generate::query_fn(
                    sink,
                    &query
                ),
                wasm_ffi: if matches!(
                    init.ty(),
                    MsgAttr::Init { entry } if matches!(entry, Some(Entry::Wasm))
                ) {
                    Some(generate::wasm_entry(&reply))
                } else {
                    None
                }
            };
    
            Some(Interfaces {
                init_msg: generate::init_msg(sink, &init),
                execute_msg: generate::messages(
                    sink,
                    MsgType::Execute,
                    &execute
                ),
                query_msg: generate::messages(
                    sink,
                    MsgType::Query,
                    &query
                ),
                entry
            })
        } else {
            if let Some(guard) = execute_guard {
                sink.attr_no_effect(guard.sig(), guard.ty());
            }
            if let Some(reply) = reply {
                sink.attr_no_effect(reply.sig(), reply.ty());
            }
            None
        };
        let boilerplate = Boilerplate {
            contract_struct: create_contract_struct(),
            error_enum: generate::error_enum(sink, contract_err_ty, &self.interfaces)
        };
        Generated {
            interfaces,
            boilerplate
        }
    }
}
fn create_contract_struct() -> ItemStruct {
    let ident = Ident::new(CONTRACT, Span::call_site());
    parse_quote! {
        #[derive(Clone, Copy, Debug)]
        pub struct #ident;
    }
}
#[inline]
fn is_contract_impl(item: &ItemImpl) -> bool {
    if let Type::Path(path) = &*item.self_ty {
        let ident = Ident::new(CONTRACT, Span::call_site());
        let expected: TypePath = parse_quote!(#ident);
    
        path.qself.is_none() && (path.path == expected.path)
    } else {
        false
    }
}
#[inline]
fn is_interface_impl(item: &ItemImpl) -> bool {
    is_contract_impl(item) && item.trait_.is_some()
}