use std::collections::HashMap;

use proc_macro2::Span;
use quote::quote;
use syn::{
    spanned::Spanned, GenericArgument, Generics, Ident, PathArguments, TraitBound, Type,
    TypeParamBound, TypePath, TypeReference, TypeTraitObject, WherePredicate,
};

use crate::{
    filter_map_assoc_paths, match_assoc_type,
    parse_assoc_type::{BoxType, DestType},
    parse_trait_sig::{MethodParseError, TypeTransform},
    syn_utils::{iter_path, iter_type},
};

#[derive(Default)]
pub struct AssocTypeConversions<'a>(pub HashMap<Ident, DestType<'a>>);

pub enum TransformError {
    UnconvertibleAssocType(Span),
    AssocTypeInUnsupportedType(Span),
}

fn is_supported_collection(ident: &Ident) -> bool {
    // collections added here must implement IntoIterator & FromIterator
    // FromIterator must not require bounds like Eq or Ord since these are Self-referential
    ident == "Vec" || ident == "VecDeque" || ident == "LinkedList"
}

impl AssocTypeConversions<'_> {
    pub fn parse_type_path(&self, type_: &mut Type) -> Result<TypeTransform, TransformError> {
        if !iter_type(type_).any(match_assoc_type) {
            return Ok(TypeTransform::NoOp);
        }

        if let Type::Reference(TypeReference {
            lifetime: None,
            mutability: Some(_),
            elem,
            ..
        }) = type_
        {
            if let Type::TraitObject(TypeTraitObject {
                dyn_token: Some(_),
                bounds,
            }) = elem.as_mut()
            {
                if bounds.len() == 1 {
                    if let TypeParamBound::Trait(bound) = bounds.first_mut().unwrap() {
                        if bound.path.segments.len() == 1 {
                            let first = bound.path.segments.first_mut().unwrap();
                            if first.ident == "Iterator" {
                                if let PathArguments::AngleBracketed(args) = &mut first.arguments {
                                    if args.args.len() == 1 {
                                        if let GenericArgument::Binding(binding) =
                                            args.args.first_mut().unwrap()
                                        {
                                            if binding.ident == "Item"
                                                && iter_type(&binding.ty).any(match_assoc_type)
                                            {
                                                let inner =
                                                    self.parse_type_path(&mut binding.ty)?;
                                                let box_type = BoxType {
                                                    inner: quote! {#elem},
                                                    placeholder_lifetime: true,
                                                };
                                                *type_ = Type::Verbatim(quote! {#box_type});
                                                return Ok(TypeTransform::Iterator(
                                                    box_type,
                                                    inner.into(),
                                                ));
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if let Type::Path(TypePath { path, qself: None }) = type_ {
            let ident = &path.segments.first().unwrap().ident;

            if ident == "Self" && path.segments.len() == 2 {
                let ident = &path.segments.last().unwrap().ident;
                let dest_type = self
                    .0
                    .get(ident)
                    .ok_or_else(|| TransformError::UnconvertibleAssocType(ident.span()))?;
                *type_ = dest_type.get_dest();
                return Ok(dest_type.type_transformation());
            } else if ident == "Option" && path.segments.len() == 1 {
                let first_seg = path.segments.first_mut().unwrap();

                if let PathArguments::AngleBracketed(args) = &mut first_seg.arguments {
                    if args.args.len() == 1 {
                        if let GenericArgument::Type(generic_type) = args.args.first_mut().unwrap()
                        {
                            if iter_type(generic_type).any(match_assoc_type) {
                                return Ok(TypeTransform::Map(
                                    self.parse_type_path(generic_type)?.into(),
                                ));
                            }
                        }
                    }
                }
            } else if ident == "Result" && path.segments.len() == 1 {
                let first_seg = path.segments.first_mut().unwrap();
                if let PathArguments::AngleBracketed(args) = &mut first_seg.arguments {
                    if args.args.len() == 2 {
                        let mut args_iter = args.args.iter_mut();
                        if let (GenericArgument::Type(ok_type), GenericArgument::Type(err_type)) =
                            (args_iter.next().unwrap(), args_iter.next().unwrap())
                        {
                            if iter_type(ok_type).any(match_assoc_type)
                                || iter_type(err_type).any(match_assoc_type)
                            {
                                return Ok(TypeTransform::Result(
                                    self.parse_type_path(ok_type)?.into(),
                                    self.parse_type_path(err_type)?.into(),
                                ));
                            }
                        }
                    }
                }
            } else if is_supported_collection(ident) && path.segments.len() == 1 {
                let first_seg = path.segments.first_mut().unwrap();

                if let PathArguments::AngleBracketed(args) = &mut first_seg.arguments {
                    if args.args.len() == 1 {
                        if let GenericArgument::Type(generic_type) = args.args.first_mut().unwrap()
                        {
                            if iter_type(generic_type).any(match_assoc_type) {
                                return Ok(TypeTransform::Collection(
                                    self.parse_type_path(generic_type)?.into(),
                                ));
                            }
                        }
                    }
                }
            } else {
                let last_seg = &path.segments.last().unwrap();
                if last_seg.ident == "Result" {
                    let last_seg = path.segments.last_mut().unwrap();
                    if let PathArguments::AngleBracketed(args) = &mut last_seg.arguments {
                        if args.args.len() == 1 {
                            if let GenericArgument::Type(generic_type) =
                                args.args.first_mut().unwrap()
                            {
                                if iter_type(generic_type).any(match_assoc_type) {
                                    return Ok(TypeTransform::Map(
                                        self.parse_type_path(generic_type)?.into(),
                                    ));
                                }
                            }
                        }
                    }
                }
            }
        }

        // the type contains an associated type but we
        // don't know how to deal with it so we abort
        Err(TransformError::AssocTypeInUnsupportedType(type_.span()))
    }
}

pub fn dynamize_function_bounds(
    generics: &mut Generics,
    assoc_type_conversions: &AssocTypeConversions,
) -> Result<HashMap<Ident, Vec<TypeTransform>>, (Span, MethodParseError)> {
    let mut type_param_transforms = HashMap::new();

    for type_param in generics.type_params_mut() {
        for bound in &mut type_param.bounds {
            if let TypeParamBound::Trait(bound) = bound {
                dynamize_trait_bound(
                    bound,
                    assoc_type_conversions,
                    &type_param.ident,
                    &mut type_param_transforms,
                )?;
            }
        }
    }

    if let Some(where_clause) = &mut generics.where_clause {
        for predicate in &mut where_clause.predicates {
            if let WherePredicate::Type(predicate_type) = predicate {
                if let Type::Path(path) = &mut predicate_type.bounded_ty {
                    if let Some(ident) = path.path.get_ident() {
                        for bound in &mut predicate_type.bounds {
                            if let TypeParamBound::Trait(bound) = bound {
                                dynamize_trait_bound(
                                    bound,
                                    assoc_type_conversions,
                                    ident,
                                    &mut type_param_transforms,
                                )?;
                            }
                        }
                        continue;
                    }
                }

                // just to provide better error messages
                if let Some(assoc_type) =
                    iter_type(&predicate_type.bounded_ty).find_map(filter_map_assoc_paths)
                {
                    return Err((
                        assoc_type.span(),
                        MethodParseError::UnconvertibleAssocTypeInWhereClause,
                    ));
                }

                // just to provide better error messages
                for bound in &mut predicate_type.bounds {
                    if let TypeParamBound::Trait(bound) = bound {
                        if let Some(assoc_type) =
                            iter_path(&bound.path).find_map(filter_map_assoc_paths)
                        {
                            return Err((
                                assoc_type.span(),
                                MethodParseError::UnconvertibleAssocTypeInWhereClause,
                            ));
                        }
                    }
                }
            }
        }
    }

    Ok(type_param_transforms)
}

fn dynamize_trait_bound(
    bound: &mut TraitBound,
    assoc_type_conversions: &AssocTypeConversions,
    type_ident: &Ident,
    type_param_transforms: &mut HashMap<Ident, Vec<TypeTransform>>,
) -> Result<(), (Span, MethodParseError)> {
    if bound.path.segments.len() == 1 {
        let segment = bound.path.segments.first_mut().unwrap();

        if let PathArguments::Parenthesized(args) = &mut segment.arguments {
            if segment.ident == "Fn" || segment.ident == "FnOnce" || segment.ident == "FnMut" {
                let mut transforms = Vec::new();
                for input_type in &mut args.inputs {
                    match assoc_type_conversions.parse_type_path(input_type) {
                        Ok(ret_type) => {
                            transforms.push(ret_type);
                        }
                        Err(TransformError::UnconvertibleAssocType(span)) => {
                            return Err((span, MethodParseError::UnconvertibleAssocType));
                        }
                        Err(TransformError::AssocTypeInUnsupportedType(span)) => {
                            return Err((span, MethodParseError::UnconvertibleAssocTypeInFnInput));
                        }
                    }
                }
                if transforms.iter().any(|t| !matches!(t, TypeTransform::NoOp)) {
                    type_param_transforms.insert(type_ident.clone(), transforms);
                }
            }
        }
    }
    if let Some(path) = iter_path(&bound.path)
        .filter_map(filter_map_assoc_paths)
        .next()
    {
        return Err((
            path.span(),
            MethodParseError::UnconvertibleAssocTypeInTraitBound,
        ));
    }
    Ok(())
}