//! Generates a companion module with per-field checker functions.
//!
//! Both `#[derive(Defaulted)]` and `#[defaulted::skip_serializing_defaults]`
//! use this same code path so the generated functions can serve double duty:
//!   - called by the `Defaulted::is_defaulted()` body
//!   - referenced by `#[serde(skip_serializing_if = "...")]`
//!
//! All checker (and optional default-value) functions live as associated
//! functions on a `Helper` struct inside the companion module.  For generic
//! structs, `Helper` carries `PhantomData` so the struct's type/lifetime
//! parameters remain in scope.

use std::collections::HashSet;

use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
#[cfg(feature = "serde")]
use quote::format_ident;
use syn::spanned::Spanned;

use crate::ParseAttrs;
use crate::field_parser::{FieldAttrs, FieldChecker};

/// Name of the companion module for a given struct.
#[cfg(feature = "serde")]
pub fn module_ident(struct_name: &syn::Ident) -> syn::Ident
{
    format_ident!("__defaulted_check_{struct_name}")
}

/// String path to a `Helper` associated function, for use in serde
/// attribute values like `skip_serializing_if` and `default`.
///
/// Non-generic: `__defaulted_check_Foo::Helper::field_name`
/// Generic:     `__defaulted_check_Foo::Helper::<'a, T>::field_name`
#[cfg(feature = "serde")]
pub fn helper_fn_path(
    struct_name: &syn::Ident,
    generics: &syn::Generics,
    fn_name: &syn::Ident,
) -> String
{
    let mod_name = module_ident(struct_name);
    let suffix = helper_suffix(generics);
    format!("{mod_name}::{suffix}::{fn_name}")
}

// Private helpers

/// Generic predicate inference: for each type parameter that appears in a
/// field matching `field_needs_bound`, emit a where predicate via `make_pred`.
fn infer_predicates(
    generics: &syn::Generics,
    fields: &syn::Fields,
    field_needs_bound: impl Fn(&FieldChecker) -> bool,
    make_pred: impl Fn(&syn::Ident) -> syn::WherePredicate,
) -> syn::Result<Vec<syn::WherePredicate>>
{
    let type_params: Vec<_> = generics.type_params().map(|tp| tp.ident.clone()).collect();
    if type_params.is_empty() {
        return Ok(Vec::new());
    }

    let mut needed = HashSet::new();
    for field in fields {
        let attrs: FieldAttrs = field.parse_attrs()?;
        if !field_needs_bound(&attrs.checker) {
            continue;
        }
        for param in &type_params {
            if type_contains_param(&field.ty, param) {
                needed.insert(param.clone());
            }
        }
    }
    // Maintain declaration order for deterministic output.
    Ok(type_params
        .iter()
        .filter(|p| needed.contains(p))
        .map(&make_pred)
        .collect())
}

/// Check whether a type parameter ident appears anywhere in a type.
fn type_contains_param(ty: &syn::Type, param: &syn::Ident) -> bool
{
    match ty {
        syn::Type::Path(tp) => {
            // Bare type param (e.g., `T`).
            if tp.qself.is_none() && tp.path.segments.len() == 1 {
                let seg = &tp.path.segments[0];
                if seg.ident == *param && matches!(seg.arguments, syn::PathArguments::None) {
                    return true;
                }
            }
            // Recurse into generic args (e.g., `Vec<T>`, `HashMap<K, V>`).
            tp.path.segments.iter().any(|seg| match &seg.arguments {
                syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| {
                    matches!(arg, syn::GenericArgument::Type(inner) if type_contains_param(inner, param))
                }),
                syn::PathArguments::Parenthesized(args) => {
                    args.inputs.iter().any(|t| type_contains_param(t, param))
                        || matches!(
                            &args.output,
                            syn::ReturnType::Type(_, ret) if type_contains_param(ret, param)
                        )
                }
                syn::PathArguments::None => false,
            })
        }
        syn::Type::Reference(r) => type_contains_param(&r.elem, param),
        syn::Type::Tuple(t) => t.elems.iter().any(|e| type_contains_param(e, param)),
        syn::Type::Array(a) => type_contains_param(&a.elem, param),
        syn::Type::Slice(s) => type_contains_param(&s.elem, param),
        syn::Type::Paren(p) => type_contains_param(&p.elem, param),
        syn::Type::Group(g) => type_contains_param(&g.elem, param),
        _ => false,
    }
}

/// `"Helper"` or `"Helper::<'a, T>"` as a string.
#[cfg(feature = "serde")]
fn helper_suffix(generics: &syn::Generics) -> String
{
    if generics.params.is_empty() {
        return "Helper".to_owned();
    }
    let params: Vec<String> = generics
        .lifetimes()
        .map(|lt| lt.lifetime.to_string())
        .chain(generics.type_params().map(|tp| tp.ident.to_string()))
        .collect();
    format!("Helper::<{}>", params.join(", "))
}

/// `mod_name::Helper` or `mod_name::Helper::<'a, T>` as tokens.
#[cfg(feature = "serde")]
fn helper_tokens(mod_name: &syn::Ident, generics: &syn::Generics) -> TokenStream2
{
    if generics.params.is_empty() {
        return quote! { #mod_name::Helper };
    }
    let lifetime_params: Vec<_> = generics.lifetimes().map(|lt| &lt.lifetime).collect();
    let type_params: Vec<_> = generics.type_params().map(|tp| &tp.ident).collect();
    quote! { #mod_name::Helper::<#(#lifetime_params,)* #(#type_params),*> }
}

/// A single field's checker function definition,
/// plus an optional default-value function for `EqualsValue` fields.
#[cfg(feature = "serde")]
struct FieldCheck
{
    fn_name: syn::Ident,
    ty: syn::Type,
    body: TokenStream2,
    default_body: Option<TokenStream2>,
    is_closure: bool,
}

/// Build checker function definitions for all non-ignored fields.
#[cfg(feature = "serde")]
#[expect(clippy::match_bool, reason = "makes logic clearer here")]
#[expect(clippy::single_match_else, reason = "makes logic clearer here")]
fn build_field_checks(fields: &syn::Fields) -> syn::Result<Vec<FieldCheck>>
{
    let mut checks = Vec::new();
    for field in fields {
        let attrs: FieldAttrs = field.parse_attrs()?;
        let field_name = field
            .ident
            .as_ref()
            .ok_or_else(|| syn::Error::new_spanned(field, "check_module requires named fields"))?;
        let ty = &field.ty;

        let is_closure = matches!(attrs.checker, FieldChecker::Closure(_));

        let body = match attrs.checker {
            FieldChecker::Ignore => continue,
            FieldChecker::Delegate => quote_spanned! {field.span()=>
                ::defaulted::Defaulted::is_defaulted(v)
            },
            FieldChecker::EqualsValue(ref expr) => match expr {
                syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Bool(b),
                    ..
                }) => match b.value() {
                    true => quote_spanned! {field.span()=> *v },
                    false => quote_spanned! {field.span()=> !*v },
                },
                _ => quote_spanned! {field.span()=> *v == #expr },
            },
            FieldChecker::EqualsRef(ref expr) => quote_spanned! {field.span()=> v == #expr },
            FieldChecker::Func(ref func) => quote_spanned! {field.span()=>
                #func(v)
            },
            FieldChecker::Closure(ref clos) => quote_spanned! {field.span()=>
                (#clos)(v)
            },
        };

        let default_body = attrs.checker.default_value_expr(field.span());

        checks.push(FieldCheck {
            fn_name: field_name.clone(),
            ty: ty.clone(),
            body,
            default_body,
            is_closure,
        });
    }
    Ok(checks)
}

/// Generate tokens for all checker (and default-value) functions.
#[cfg(feature = "serde")]
fn generate_fns(checks: &[FieldCheck]) -> Vec<TokenStream2>
{
    checks
        .iter()
        .flat_map(|c| {
            let fn_name = &c.fn_name;
            let ty = &c.ty;
            let body = &c.body;

            let allow_closure = c.is_closure.then(|| {
                quote! { #[allow(clippy::redundant_closure_call)] }
            });

            let checker = quote! {
                #[inline]
                #[allow(clippy::float_cmp)]
                #allow_closure
                pub fn #fn_name(v: &#ty) -> bool { #body }
            };

            let default_fn = c.default_body.as_ref().map(|default_body| {
                let default_name = format_ident!("{fn_name}__default");
                quote! {
                    #[inline]
                    pub fn #default_name() -> #ty { #default_body }
                }
            });

            std::iter::once(checker).chain(default_fn)
        })
        .collect()
}

/// Generate the `Helper` struct definition and its `impl` block.
///
/// Non-generic structs get a unit struct.  Generic structs get a struct
/// carrying `PhantomData` so type/lifetime parameters stay in scope.
#[cfg(feature = "serde")]
fn generate_helper(generics: &syn::Generics, fns: &[TokenStream2]) -> (TokenStream2, TokenStream2)
{
    if generics.params.is_empty() {
        return (
            quote! { pub struct Helper; },
            quote! { impl Helper { #(#fns)* } },
        );
    }

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

    let lifetime_params: Vec<_> = generics.lifetimes().map(|lt| &lt.lifetime).collect();
    let type_params: Vec<_> = generics.type_params().map(|tp| &tp.ident).collect();
    let phantom = quote! {
        core::marker::PhantomData<
            fn(#(&#lifetime_params (),)*) -> (#(#type_params,)*)
        >
    };

    (
        quote! { pub struct Helper #ty_generics (#phantom); },
        quote! {
            impl #impl_generics Helper #ty_generics #where_clause {
                #(#fns)*
            }
        },
    )
}

// Public API

/// Compute `where` predicates that auto-bind type parameters to `Defaulted`.
///
/// For each type parameter that appears in a field delegating to
/// `Defaulted::is_defaulted()`, emits `T: ::defaulted::Defaulted`.
/// Fields using `with`, `default`, or `ignore` do not contribute bounds.
pub fn defaulted_predicates(
    generics: &syn::Generics,
    fields: &syn::Fields,
) -> syn::Result<Vec<syn::WherePredicate>>
{
    infer_predicates(
        generics,
        fields,
        |c| matches!(c, FieldChecker::Delegate),
        |p| syn::parse_quote! { #p: ::defaulted::Defaulted },
    )
}

/// Compute `where` predicates that auto-bind type parameters to `Default`.
///
/// For each type parameter that appears in a field *without* an explicit
/// `#[defaulted(default = ...)]` value, emits `T: ::core::default::Default`.
pub fn default_predicates(
    generics: &syn::Generics,
    fields: &syn::Fields,
) -> syn::Result<Vec<syn::WherePredicate>>
{
    infer_predicates(
        generics,
        fields,
        |c| !matches!(c, FieldChecker::EqualsValue(_)),
        |p| syn::parse_quote! { #p: ::core::default::Default },
    )
}

/// Clone generics and append extra where predicates.
pub fn augment_generics(generics: &syn::Generics, extra: &[syn::WherePredicate]) -> syn::Generics
{
    let mut result = generics.clone();
    if !extra.is_empty() {
        result
            .make_where_clause()
            .predicates
            .extend(extra.iter().cloned());
    }
    result
}

/// Generate the companion module containing per-field checker functions.
#[cfg(feature = "serde")]
pub fn generate(
    struct_name: &syn::Ident,
    generics: &syn::Generics,
    fields: &syn::Fields,
) -> syn::Result<TokenStream2>
{
    if let Some(cp) = generics.const_params().next() {
        return Err(syn::Error::new_spanned(
            cp,
            "Defaulted does not support const generic parameters",
        ));
    }

    let mod_name = module_ident(struct_name);
    let checks = build_field_checks(fields)?;
    let fns = generate_fns(&checks);

    // Augment Helper impl with inferred Defaulted bounds so the struct
    // definition itself does not need trait bounds on its type parameters.
    let predicates = defaulted_predicates(generics, fields)?;
    let augmented = augment_generics(generics, &predicates);
    let (helper_def, helper_impl) = generate_helper(&augmented, &fns);

    Ok(quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        mod #mod_name {
            use super::*;
            #helper_def
            #helper_impl
        }
    })
}

/// Generate the conjunction expression for `Defaulted::is_defaulted()`,
/// calling each checker function from the companion module's `Helper`.
#[cfg(feature = "serde")]
pub fn generate_defaulted_body(
    struct_name: &syn::Ident,
    generics: &syn::Generics,
    fields: &syn::Fields,
) -> syn::Result<TokenStream2>
{
    let mod_name = module_ident(struct_name);
    let helper = helper_tokens(&mod_name, generics);
    let checks = build_field_checks(fields)?;

    if checks.is_empty() {
        return Ok(quote! { true });
    }

    let terms: Vec<TokenStream2> = checks
        .iter()
        .map(|c| {
            let fn_name = &c.fn_name;
            quote! { #helper::#fn_name(&self.#fn_name) }
        })
        .collect();

    let mut body = terms[0].clone();
    for term in &terms[1..] {
        body = quote! { #body && #term };
    }
    Ok(body)
}

/// Generate an inline conjunction expression for `Defaulted::is_defaulted()`
/// that does not depend on a companion module.
///
/// This avoids the `proc_macro_derive_resolution_fallback` problem where
/// `use super::*` inside a generated module cannot see names imported in
/// an enclosing function scope.
pub fn generate_inline_defaulted_body(fields: &syn::Fields) -> syn::Result<TokenStream2>
{
    let mut terms = Vec::new();
    for field in fields {
        let attrs: FieldAttrs = field.parse_attrs()?;
        let field_name = field
            .ident
            .as_ref()
            .ok_or_else(|| syn::Error::new_spanned(field, "inline check requires named fields"))?;
        let span = field.span();

        let term = match attrs.checker {
            FieldChecker::Ignore => continue,
            FieldChecker::Delegate => {
                quote_spanned! {span=> ::defaulted::Defaulted::is_defaulted(&self.#field_name) }
            }
            FieldChecker::EqualsValue(ref expr) => match expr {
                syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Bool(b),
                    ..
                }) => match b.value() {
                    true => quote_spanned! {span=> self.#field_name },
                    false => quote_spanned! {span=> !self.#field_name },
                },
                _ => {
                    quote_spanned! {span=> self.#field_name == #expr }
                }
            },
            FieldChecker::EqualsRef(ref expr) => {
                quote_spanned! {span=> self.#field_name == #expr }
            }
            FieldChecker::Func(ref func) => {
                quote_spanned! {span=> #func(&self.#field_name) }
            }
            FieldChecker::Closure(ref clos) => {
                quote_spanned! {span=> (#clos)(&self.#field_name) }
            }
        };
        terms.push(term);
    }

    if terms.is_empty() {
        return Ok(quote! { true });
    }

    let mut body = terms[0].clone();
    for term in &terms[1..] {
        body = quote! { #body && #term };
    }
    Ok(body)
}
