use proc_macro2::TokenStream;
use quote::quote;
use syn::{ImplItem, ItemImpl, ReturnType, Type, FnArg, Pat};

use crate::val;

enum Receiver {
    RefSelf,
    Static,
}

struct ParsedMethod {
    name: syn::Ident,
    receiver: Receiver,
    params: Vec<(syn::Ident, Type)>,
    ret_type: Option<Type>,
}

fn is_scalar_type(ty: &Type) -> bool {
    let Type::Path(tp) = ty else { return false };
    let Some(seg) = tp.path.segments.last() else { return false };
    val::is_supported_scalar(seg)
}

fn returns_self(ty: &Type, self_ty: &Type) -> bool {
    match ty {
        Type::Path(tp) => {
            let last = tp.path.segments.last().map(|s| s.ident.to_string());
            last.as_deref() == Some("Self")
                || quote!(#ty).to_string() == quote!(#self_ty).to_string()
        }
        _ => false,
    }
}

pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
    let item: ItemImpl = syn::parse2(input)?;

    if item.trait_.is_some() {
        return Err(syn::Error::new_spanned(&item, "#[grw::repl] can only be used on inherent impl blocks"));
    }

    let self_ty = &item.self_ty;

    let mut parsed = Vec::new();

    for impl_item in &item.items {
        let ImplItem::Fn(method) = impl_item else { continue };

        if method.sig.asyncness.is_some() { continue }
        if !method.sig.generics.params.is_empty() { continue }

        let mut inputs = method.sig.inputs.iter();

        let receiver = match inputs.next() {
            Some(FnArg::Receiver(r)) => {
                if r.mutability.is_some() { continue }
                if r.reference.is_none() { continue }
                Receiver::RefSelf
            }
            Some(FnArg::Typed(_)) => Receiver::Static,
            None => Receiver::Static,
        };

        let extra_params: Vec<(syn::Ident, Type)> = match &receiver {
            Receiver::RefSelf => {
                method.sig.inputs.iter().skip(1).filter_map(|arg| {
                    let FnArg::Typed(pt) = arg else { return None };
                    let Pat::Ident(pi) = pt.pat.as_ref() else { return None };
                    Some((pi.ident.clone(), (*pt.ty).clone()))
                }).collect()
            }
            Receiver::Static => {
                method.sig.inputs.iter().filter_map(|arg| {
                    let FnArg::Typed(pt) = arg else { return None };
                    let Pat::Ident(pi) = pt.pat.as_ref() else { return None };
                    Some((pi.ident.clone(), (*pt.ty).clone()))
                }).collect()
            }
        };

        let ret_type = match &method.sig.output {
            ReturnType::Default => None,
            ReturnType::Type(_, ty) => Some((*ty.clone()).clone()),
        };

        parsed.push(ParsedMethod {
            name: method.sig.ident.clone(),
            receiver,
            params: extra_params,
            ret_type,
        });
    }

    let mut entries = Vec::new();

    for m in &parsed {
        let method_name = &m.name;
        let method_name_str = method_name.to_string();
        let is_static = matches!(m.receiver, Receiver::Static);

        let resolved_ret = m.ret_type.as_ref().map(|ty| {
            if returns_self(ty, self_ty) {
                (**self_ty).clone()
            } else {
                ty.clone()
            }
        });

        let ret_type_expr = match &resolved_ret {
            Some(ty) => val::type_to_field_type_pub(ty),
            None => quote! { grw::layout::FieldType::Bool },
        };

        let param_entries: Vec<TokenStream> = m.params.iter().map(|(pname, pty)| {
            let pname_str = pname.to_string();
            let pty_expr = val::type_to_field_type_pub(pty);
            quote! {
                grw::layout::MethodParam { name: #pname_str, ty: #pty_expr }
            }
        }).collect();
        let param_count = param_entries.len();

        let all_params_scalar = m.params.iter().all(|(_, ty)| is_scalar_type(ty));
        let ret_is_scalar = m.ret_type.as_ref().is_some_and(|ty| is_scalar_type(ty));
        let ret_is_self = m.ret_type.as_ref().is_some_and(|ty| returns_self(ty, self_ty));
        let has_return = m.ret_type.is_some();

        let can_wrap = all_params_scalar && has_return && (ret_is_scalar || (is_static && ret_is_self));

        let fn_ptr_expr = if can_wrap {
            let param_names: Vec<&syn::Ident> = m.params.iter().map(|(n, _)| n).collect();
            let param_types: Vec<&Type> = m.params.iter().map(|(_, t)| t).collect();
            let ret_ty = resolved_ret.as_ref().unwrap();

            if is_static && ret_is_self {
                quote! {
                    {
                        extern "C" fn __grw_w(#(#param_names: #param_types,)* __out: *mut u8) {
                            let __val = #self_ty::#method_name(#(#param_names),*);
                            unsafe { std::ptr::write(__out as *mut #self_ty, __val) };
                        }
                        __grw_w as usize as *const u8
                    }
                }
            } else if is_static {
                quote! {
                    {
                        extern "C" fn __grw_w(#(#param_names: #param_types),*) -> #ret_ty {
                            #self_ty::#method_name(#(#param_names),*)
                        }
                        __grw_w as usize as *const u8
                    }
                }
            } else {
                quote! {
                    {
                        extern "C" fn __grw_w(__p: *const u8, #(#param_names: #param_types),*) -> #ret_ty {
                            unsafe { &*(__p as *const #self_ty) }.#method_name(#(#param_names),*)
                        }
                        __grw_w as usize as *const u8
                    }
                }
            }
        } else {
            quote! { std::ptr::null::<u8>() }
        };

        let params_expr = if param_entries.is_empty() {
            quote! { &[] }
        } else {
            quote! {
                {
                    static PARAMS: std::sync::LazyLock<[grw::layout::MethodParam; #param_count]> =
                        std::sync::LazyLock::new(|| [#(#param_entries),*]);
                    &*PARAMS as &'static [grw::layout::MethodParam]
                }
            }
        };

        entries.push(quote! {
            {
                grw::layout::MethodMeta {
                    name: #method_name_str,
                    ret_type: #ret_type_expr,
                    params: #params_expr,
                    is_static: #is_static,
                    fn_ptr: #fn_ptr_expr,
                }
            }
        });
    }

    let method_count = entries.len();

    Ok(quote! {
        #item

        impl #self_ty {
            #[doc(hidden)]
            pub fn __grw_method_table() -> &'static [grw::layout::MethodMeta] {
                static METHODS: std::sync::LazyLock<[grw::layout::MethodMeta; #method_count]> =
                    std::sync::LazyLock::new(|| [
                        #(#entries),*
                    ]);
                &*METHODS
            }
        }
    })
}
