use std::collections::HashMap;

use proc_macro2::Span;
use syn::{
    spanned::Spanned, FnArg, PredicateType, Receiver, ReturnType, Type, TypePath, WherePredicate,
};
use syn::{Ident, Signature, TypeImplTrait};

use crate::syn_utils::{find_in_type, trait_bounds, TypeMatcher};
use crate::transform::{dynamize_function_bounds, AssocTypeConversions, TransformError};
use crate::AssocTypeMatcher;

#[derive(Debug, Clone)]
pub enum TypeTransform {
    NoOp,
    Into,
    Map(Box<TypeTransform>),
    Result(Box<TypeTransform>, Box<TypeTransform>),
}

#[derive(Debug)]
pub enum MethodParseError {
    NonDispatchableMethod,
    AssocTypeInInputs,
    ImplTraitInInputs,
    AssocTypeInUnsupportedReturnType,
    UnconvertibleAssocTypeInFnInput,
    UnconvertibleAssocTypeInTraitBound,
    UnconvertibleAssocType,
}

struct ImplTraitMatcher;
impl TypeMatcher<TypeImplTrait> for ImplTraitMatcher {
    fn match_type<'a>(&self, t: &'a Type) -> Option<&'a TypeImplTrait> {
        if let Type::ImplTrait(impltrait) = t {
            Some(impltrait)
        } else {
            None
        }
    }
}

pub struct SignatureChanges {
    pub return_type: TypeTransform,
    pub type_param_transforms: HashMap<Ident, Vec<TypeTransform>>,
}

pub fn parse_trait_signature(
    signature: &mut Signature,
    assoc_type_conversions: &AssocTypeConversions,
) -> Result<SignatureChanges, (Span, MethodParseError)> {
    if is_non_dispatchable(signature) {
        return Err((signature.span(), MethodParseError::NonDispatchableMethod));
    }

    // provide better error messages for associated types in params
    for input in &signature.inputs {
        if let FnArg::Typed(pattype) = input {
            if find_in_type(&pattype.ty, &AssocTypeMatcher).is_some() {
                return Err((pattype.ty.span(), MethodParseError::AssocTypeInInputs));
            }
            if let Some(impl_trait) = find_in_type(&pattype.ty, &ImplTraitMatcher) {
                return Err((impl_trait.span(), MethodParseError::ImplTraitInInputs));
            }
        }
    }

    let type_param_transforms =
        dynamize_function_bounds(&mut signature.generics, assoc_type_conversions)?;

    let return_type = match &mut signature.output {
        ReturnType::Type(_, og_type) => match assoc_type_conversions.parse_type_path(og_type) {
            Ok(ret_type) => ret_type,
            Err(TransformError::UnconvertibleAssocType(span)) => {
                return Err((span, MethodParseError::UnconvertibleAssocType));
            }
            Err(TransformError::AssocTypeInUnsupportedType(span)) => {
                return Err((span, MethodParseError::AssocTypeInUnsupportedReturnType));
            }
        },
        ReturnType::Default => TypeTransform::NoOp,
    };
    Ok(SignatureChanges {
        return_type,
        type_param_transforms,
    })
}

fn is_non_dispatchable(signature: &Signature) -> bool {
    // non-dispatchable: fn example(&self) where Self: Sized;
    if let Some(where_clause) = &signature.generics.where_clause {
        if where_clause
            .predicates
            .iter()
            .any(bounds_self_and_has_bound_sized)
        {
            return true;
        }
    }

    // non-dispatchable: fn example();
    if signature.inputs.is_empty() {
        return true;
    }

    // non-dispatchable: fn example(arg: Type);
    if matches!(signature.inputs.first(), Some(FnArg::Typed(_))) {
        return true;
    }

    // non-dispatchable: fn example(self);
    if matches!(
        signature.inputs.first(),
        Some(FnArg::Receiver(Receiver {
            reference: None,
            ..
        }))
    ) {
        return true;
    }
    false
}

/// Returns true if the bounded type is `Self` and the bounds contain `Sized`.
fn bounds_self_and_has_bound_sized(predicate: &WherePredicate) -> bool {
    matches!(
        predicate,
        WherePredicate::Type(PredicateType {
            bounded_ty: Type::Path(TypePath { path, .. }),
            bounds,
            ..
        })
        if path.is_ident("Self")
        && trait_bounds(bounds).any(|b| b.path.is_ident("Sized"))
    )
}

#[cfg(test)]
mod tests {
    use quote::{format_ident, quote};
    use syn::{TraitItemMethod, Type};

    use crate::{
        parse_assoc_type::DestType,
        parse_trait_sig::{
            parse_trait_signature, MethodParseError, SignatureChanges, TypeTransform,
        },
        transform::AssocTypeConversions,
    };

    #[test]
    fn ok_void() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Ok(SignatureChanges {
                return_type: TypeTransform::NoOp,
                ..
            })
        ));
    }

    #[test]
    fn ok_assoc_type() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) -> Self::A;
        })
        .unwrap();

        let mut assoc_type_map = AssocTypeConversions::default();
        let ident = format_ident!("A");
        let dest_inner = Type::Verbatim(quote! {Example});
        let dest = DestType::Into(&dest_inner);
        assoc_type_map.0.insert(ident, dest);

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &assoc_type_map),
            Ok(SignatureChanges {
                return_type: TypeTransform::Into,
                ..
            })
        ));
    }

    #[test]
    fn err_unconvertible_assoc_type() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) -> Self::A;
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::UnconvertibleAssocType))
        ));
    }

    #[test]
    fn err_non_dispatchable_assoc_function_no_args() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test();
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::NonDispatchableMethod))
        ));
    }

    #[test]
    fn err_non_dispatchable_assoc_function_with_args() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(arg: Type);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::NonDispatchableMethod))
        ));
    }

    #[test]
    fn err_non_dispatchable_consume_self() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(self);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::NonDispatchableMethod))
        ));
    }

    #[test]
    fn err_non_dispatchable_where_self_sized() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) where Self: Sized;
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::NonDispatchableMethod))
        ));
    }

    #[test]
    fn err_assoc_type_in_unsupported_return() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) -> Foo<Self::A>;
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::AssocTypeInUnsupportedReturnType))
        ));
    }

    #[test]
    fn err_assoc_type_in_unsupported_return_in_opt() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) -> Option<Foo<Self::A>>;
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::AssocTypeInUnsupportedReturnType))
        ));
    }

    #[test]
    fn err_assoc_type_in_unsupported_return_in_ok() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) -> Result<Foo<Self::A>, Error>;
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::AssocTypeInUnsupportedReturnType))
        ));
    }

    #[test]
    fn err_assoc_type_in_unsupported_return_in_err() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self) -> Result<Ok, Foo<Self::A>>;
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::AssocTypeInUnsupportedReturnType))
        ));
    }

    #[test]
    fn err_assoc_type_in_input() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self, x: Self::A);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::AssocTypeInInputs))
        ));
    }

    #[test]
    fn err_assoc_type_in_input_opt() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self, x: Option<Self::A>);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::AssocTypeInInputs))
        ));
    }

    #[test]
    fn err_impl_in_input() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test(&self, arg: Option<impl SomeTrait>);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::ImplTraitInInputs))
        ));
    }

    #[test]
    fn err_assoc_type_in_generic() {
        let mut type1: TraitItemMethod = syn::parse2(quote! {
            fn test<F: Fn(Foo<Self::A>)>(&self, fun: F);
        })
        .unwrap();

        assert!(matches!(
            parse_trait_signature(&mut type1.sig, &Default::default()),
            Err((_, MethodParseError::UnconvertibleAssocTypeInFnInput))
        ));
    }
}