//! Implementation of a [`TryFrom`] derive macro.

use proc_macro2::{Literal, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::spanned::Spanned as _;

use crate::utils::{
    attr::{self, ParseMultiple as _},
    Spanning,
};

/// Expands a [`TryFrom`] derive macro.
pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
    match &input.data {
        syn::Data::Struct(data) => Err(syn::Error::new(
            data.struct_token.span(),
            "`TryFrom` cannot be derived for structs",
        )),
        syn::Data::Enum(data) => Ok(Expansion {
            repr: attr::ReprInt::parse_attrs(&input.attrs, &format_ident!("repr"))?
                .map(Spanning::into_inner)
                .unwrap_or_default(),
            attr: Some(
                ItemAttribute::parse_attrs(&input.attrs, &format_ident!("try_from"))?
                    .map(|attr| {
                        if matches!(attr.item, ItemAttribute::Types(_)) {
                            Err(syn::Error::new(
                            attr.span,
                            "`#[try_from(repr(...))]` attribute is not supported yet",
                        ))
                        } else {
                            Ok(attr.item)
                        }
                    })
                    .transpose()?
                    .ok_or_else(|| {
                        syn::Error::new(
                            input.ident.span(),
                            "`#[try_from(repr)]` attribute is expected here",
                        )
                    })?,
            ),
            ident: input.ident.clone(),
            generics: input.generics.clone(),
            variants: data.variants.clone().into_iter().collect(),
        }
        .into_token_stream()),
        syn::Data::Union(data) => Err(syn::Error::new(
            data.union_token.span(),
            "`TryFrom` cannot be derived for unions",
        )),
    }
}

/// Representation of a [`TryFrom`] derive macro struct item attribute.
///
/// ```rust,ignore
/// #[try_from(repr)]
/// #[try_from(repr(<types>))]
/// ```
type ItemAttribute = attr::ReprConversion;

/// Expansion of a macro for generating [`TryFrom`] implementation of an enum.
struct Expansion {
    /// `#[repr(u/i*)]` of the enum.
    repr: attr::ReprInt,

    /// [`ItemAttribute`] of the enum.
    attr: Option<ItemAttribute>,

    /// [`syn::Ident`] of the enum.
    ///
    /// [`syn::Ident`]: struct@syn::Ident
    ident: syn::Ident,

    /// [`syn::Generics`] of the enum.
    generics: syn::Generics,

    /// [`syn::Variant`]s of the enum.
    variants: Vec<syn::Variant>,
}

impl ToTokens for Expansion {
    /// Expands [`TryFrom`] implementations for a struct.
    fn to_tokens(&self, tokens: &mut TokenStream) {
        if self.attr.is_none() {
            return;
        }

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

        let repr_ty = &self.repr.ty();

        let mut last_discriminant = quote! { 0 };
        let mut inc = 0usize;
        let (consts, (discriminants, variants)): (
            Vec<syn::Ident>,
            (Vec<TokenStream>, Vec<TokenStream>),
        ) = self
            .variants
            .iter()
            .filter_map(
                |syn::Variant {
                     ident,
                     fields,
                     discriminant,
                     ..
                 }| {
                    if let Some(d) = discriminant {
                        last_discriminant = d.1.to_token_stream();
                        inc = 0;
                    }
                    let ret = {
                        let inc = Literal::usize_unsuffixed(inc);
                        fields.is_empty().then_some((
                            format_ident!("__DISCRIMINANT_{ident}"),
                            (
                                quote! { #last_discriminant + #inc },
                                quote! { #ident #fields },
                            ),
                        ))
                    };
                    inc += 1;
                    ret
                },
            )
            .unzip();

        let error = quote! { derive_more::TryFromReprError<#repr_ty> };

        quote! {
            #[automatically_derived]
            impl #impl_generics derive_more::core::convert::TryFrom<#repr_ty #ty_generics>
             for #ident #where_clause {
                type Error = #error;

                #[allow(deprecated)] // omit warnings on deprecated fields/variants
                #[allow(non_upper_case_globals)]
                #[inline]
                fn try_from(val: #repr_ty) -> derive_more::core::result::Result<Self, #error> {
                    #( const #consts: #repr_ty = #discriminants; )*
                    match val {
                        #(#consts => derive_more::core::result::Result::Ok(#ident::#variants),)*
                        _ => derive_more::core::result::Result::Err(
                            derive_more::TryFromReprError::new(val)
                        ),
                    }
                }
            }
        }.to_tokens(tokens);
    }
}
