use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Parse;

use crate::context::{Context, Tokens};

pub(super) fn expand(mut input: syn::DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {
    let cx = Context::new();

    let attr = parse_type_attr(&cx, &input.attrs);

    let tokens = cx.tokens_with_module(attr.module.as_ref());

    if !attr.predicates.is_empty() {
        input
            .generics
            .make_where_clause()
            .predicates
            .extend(attr.predicates);
    }

    let Tokens {
        try_clone, alloc, ..
    } = &tokens;

    let implementation = if attr.copy {
        quote!(*self)
    } else {
        match input.data {
            syn::Data::Struct(st) => {
                let fields = st.fields.into_iter().enumerate().map(|(index, f)| {
                    let member = match &f.ident {
                        Some(ident) => syn::Member::Named(ident.clone()),
                        None => syn::Member::Unnamed(syn::Index::from(index)),
                    };

                    let attr = parse_field_attr(&cx, &f.attrs);

                    let expr = match attr.with {
                        With::Copy => quote! { self.#member },
                        With::None => quote! { #try_clone::try_clone(&self.#member)? },
                        With::With(with) => quote! { #with(&self.#member) },
                        With::TryWith(with) => quote! { #with(&self.#member)? },
                    };

                    syn::FieldValue {
                        attrs: Vec::new(),
                        member,
                        colon_token: Some(<syn::Token![:]>::default()),
                        expr: syn::Expr::Verbatim(expr),
                    }
                });

                quote! {
                    Self { #(#fields),* }
                }
            }
            syn::Data::Enum(en) => {
                let variants = en.variants.into_iter().map(|v| {
                    let name = v.ident;

                    let members = v.fields.iter().enumerate().map(|(index, f)| {
                        let (member, var) = match &f.ident {
                            Some(ident) => (
                                syn::Member::Named(ident.clone()),
                                quote::format_ident!("{}", ident),
                            ),
                            None => (
                                syn::Member::Unnamed(syn::Index::from(index)),
                                quote::format_ident!("_{}", index),
                            ),
                        };

                        let attr = parse_field_attr(&cx, &f.attrs);
                        (index, f, member, var, attr)
                    });

                    let assigns = members.clone().map(|(index, f, member, var, _)| {
                        if f.ident.is_some() {
                            syn::FieldValue {
                                attrs: Vec::new(),
                                member,
                                colon_token: None,
                                expr: syn::Expr::Verbatim(quote!()),
                            }
                        } else {
                            let member = syn::Member::Unnamed(syn::Index::from(index));

                            let expr = syn::Expr::Path(syn::ExprPath {
                                attrs: Vec::new(),
                                qself: None,
                                path: syn::Path::from(var),
                            });

                            syn::FieldValue {
                                attrs: Vec::new(),
                                member,
                                colon_token: Some(<syn::Token![:]>::default()),
                                expr,
                            }
                        }
                    });

                    let fields = members.clone().map(|(_, _, member, var, attr)| {
                        let expr = match attr.with {
                            With::Copy => quote! { *#var },
                            With::None => quote! { #try_clone::try_clone(#var)? },
                            With::With(with) => quote! { #with(#var) },
                            With::TryWith(with) => quote! { #with(#var)? },
                        };

                        syn::FieldValue {
                            attrs: Vec::new(),
                            member,
                            colon_token: Some(<syn::Token![:]>::default()),
                            expr: syn::Expr::Verbatim(expr),
                        }
                    });

                    quote! {
                        Self::#name { #(#assigns),* } => {
                            Self::#name { #(#fields),* }
                        }
                    }
                });

                quote! {
                    match self {
                        #(#variants),*
                    }
                }
            }
            syn::Data::Union(un) => {
                cx.error(syn::Error::new_spanned(
                    un.union_token,
                    "TryClone: Unions are not supported",
                ));
                quote!()
            }
        }
    };

    if cx.has_errors() {
        return Err(cx.into_errors());
    }

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

    Ok(quote! {
        impl #impl_generics #try_clone for #name #ty_generics #where_clause {
            fn try_clone(&self) -> #alloc::Result<Self> {
                Ok(#implementation)
            }
        }
    })
}

#[derive(Default)]
struct TypeAttr {
    predicates: syn::punctuated::Punctuated<syn::WherePredicate, syn::Token![,]>,
    copy: bool,
    module: Option<syn::Path>,
}

fn parse_type_attr(cx: &Context, input: &[syn::Attribute]) -> TypeAttr {
    let mut attr = TypeAttr::default();

    for a in input {
        if !a.path().is_ident("try_clone") {
            continue;
        }

        let result = a.parse_nested_meta(|parser| {
            if parser.path.is_ident("bound") {
                parser.input.parse::<syn::Token![=]>()?;
                let content;
                syn::braced!(content in parser.input);
                attr.predicates
                    .extend(content.parse_terminated(syn::WherePredicate::parse, syn::Token![,])?);
                return Ok(());
            }

            if parser.path.is_ident("copy") {
                attr.copy = true;
                return Ok(());
            }

            if parser.path.is_ident("crate") {
                if parser.input.parse::<Option<syn::Token![=]>>()?.is_some() {
                    attr.module = Some(parser.input.parse::<syn::Path>()?);
                } else {
                    attr.module = Some(syn::parse_quote!(crate));
                }

                return Ok(());
            }

            Err(syn::Error::new(
                parser.input.span(),
                "unsupported attribute",
            ))
        });

        if let Err(error) = result {
            cx.error(error);
        }
    }

    attr
}

#[derive(Default, Clone)]
enum With {
    #[default]
    None,
    Copy,
    With(syn::Path),
    TryWith(syn::Path),
}

#[derive(Default, Clone)]
struct FieldAttr {
    with: With,
}

fn parse_field_attr(cx: &Context, input: &[syn::Attribute]) -> FieldAttr {
    let mut attr = FieldAttr::default();

    for a in input {
        if !a.path().is_ident("try_clone") {
            continue;
        }

        let result = a.parse_nested_meta(|parser| {
            if parser.path.is_ident("with") {
                parser.input.parse::<syn::Token![=]>()?;
                attr.with = With::With(parser.input.parse()?);
                return Ok(());
            }

            if parser.path.is_ident("try_with") {
                parser.input.parse::<syn::Token![=]>()?;
                attr.with = With::TryWith(parser.input.parse()?);
                return Ok(());
            }

            if parser.path.is_ident("copy") {
                attr.with = With::Copy;
                return Ok(());
            }

            Err(syn::Error::new(
                parser.input.span(),
                "unsupported attribute",
            ))
        });

        if let Err(error) = result {
            cx.error(error);
        }
    }

    attr
}
