use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::spanned::Spanned;
use syn::{GenericArgument, Ident, PathArguments, PathSegment, TraitItemType, Type};

use crate::match_assoc_type;
use crate::parse_trait_sig::TypeTransform;
use crate::syn_utils::{iter_type, lifetime_bounds, trait_bounds};

#[derive(Debug)]
pub enum AssocTypeParseError {
    AssocTypeInBound,
    GenericAssociatedType,
    NoIntoBound,
}

#[derive(Debug, Clone)]
pub struct BoxType {
    pub inner: TokenStream,
    pub placeholder_lifetime: bool,
}

impl ToTokens for BoxType {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let inner = &self.inner;
        match self.placeholder_lifetime {
            true => tokens.extend(quote! {Box<#inner + '_>}),
            false => tokens.extend(quote! {Box<#inner>}),
        }
    }
}

pub enum DestType<'a> {
    Into(&'a Type),
    Box(BoxType),
}

impl DestType<'_> {
    pub fn get_dest(&self) -> Type {
        match self {
            DestType::Into(ty) => (*ty).clone(),
            DestType::Box(b) => Type::Verbatim(quote!(#b)),
        }
    }

    pub fn type_transformation(&self) -> TypeTransform {
        match self {
            DestType::Into(_) => TypeTransform::Into,
            DestType::Box(b) => TypeTransform::Box(b.clone()),
        }
    }
}

pub fn parse_assoc_type(
    assoc_type: &TraitItemType,
) -> Result<(&Ident, DestType), (Span, AssocTypeParseError)> {
    if let Some(bound) = trait_bounds(&assoc_type.bounds).next() {
        if let PathSegment {
            ident,
            arguments: PathArguments::AngleBracketed(args),
        } = bound.path.segments.first().unwrap()
        {
            if ident == "Into" && args.args.len() == 1 {
                if let GenericArgument::Type(into_type) = args.args.first().unwrap() {
                    // provide a better error message for type A: Into<Self::B>
                    if iter_type(into_type).any(match_assoc_type) {
                        return Err((into_type.span(), AssocTypeParseError::AssocTypeInBound));
                    }

                    // TODO: support lifetime GATs (see the currently failing tests/gats.rs)
                    if !assoc_type.generics.params.is_empty() {
                        return Err((
                            assoc_type.generics.params.span(),
                            AssocTypeParseError::GenericAssociatedType,
                        ));
                    }

                    return Ok((&assoc_type.ident, DestType::Into(into_type)));
                }
            }
        }
        let path = &bound.path;
        return Ok((
            &assoc_type.ident,
            DestType::Box(BoxType {
                inner: quote! {dyn #path},
                placeholder_lifetime: !lifetime_bounds(&assoc_type.bounds)
                    .any(|l| l.ident == "static"),
            }),
        ));
    }
    Err((assoc_type.span(), AssocTypeParseError::NoIntoBound))
}

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

    use crate::parse_assoc_type::{parse_assoc_type, AssocTypeParseError, DestType};

    #[test]
    fn ok() {
        let type1: TraitItemType = syn::parse2(quote! {
            type A: Into<String>;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Ok((id, DestType::Into(Type::Path(path))))
            if id == "A" && path.path.is_ident("String")
        ));
    }

    #[test]
    fn err_no_bound() {
        let type1: TraitItemType = syn::parse2(quote! {
            type A;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Err((_, AssocTypeParseError::NoIntoBound))
        ));
    }

    #[test]
    fn err_assoc_type_in_bound() {
        let type1: TraitItemType = syn::parse2(quote! {
            type A: Into<Self::B>;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Err((_, AssocTypeParseError::AssocTypeInBound))
        ));
    }

    #[test]
    fn err_gat_type() {
        let type1: TraitItemType = syn::parse2(quote! {
            type A<X>: Into<Foobar<X>>;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Err((_, AssocTypeParseError::GenericAssociatedType))
        ));
    }

    #[test]
    fn err_gat_lifetime() {
        let type1: TraitItemType = syn::parse2(quote! {
            type A<'a>: Into<Foobar<'a>>;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Err((_, AssocTypeParseError::GenericAssociatedType))
        ));
    }
}