Experimenting with more structured ways to handle command-line input/output in Rust
use crate::fluent;
use std::collections::HashSet;

use heck::ToKebabCase;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::punctuated::Punctuated;

use super::{MacroError, UnsupportedError, UnsupportedReason};

#[derive(Clone, Copy, Debug)]
pub enum ReferenceKind {
    EnumField,
    StructField,
}

#[derive(Clone, Debug)]
pub struct Context {
    pub reference_kind: ReferenceKind,
    pub valid_references: HashSet<String>,
}

fn expr_for_message(
    group: &mut fluent::Group,
    ident: &Ident,
    reference_kind: &Context,
) -> Result<TokenStream, fluent::GroupError> {
    let canonical_message = group.remove_canonical_message(ident, reference_kind)?;

    let (additional_locales, additional_messages): (Vec<_>, Vec<_>) = group
        .remove_additional_messages(ident, reference_kind)?
        .into_iter()
        .unzip();

    let additional_locales = additional_locales
        .iter()
        .map(|locale| locale.id.to_string())
        .collect::<Vec<_>>();

    Ok(quote! {
        // TODO: handle different rule types according to fluent code (not just cardinal)
        // TODO: only generate this when needed in message
        const PLURAL_RULE_TYPE: ::fluent_embed::PluralRuleType =
            ::fluent_embed::PluralRuleType::Cardinal;
            let plural_options = ::fluent_embed::PluralRulesOptions::default().with_type(PLURAL_RULE_TYPE);
        let plural_rules = ::fluent_embed::PluralRules::try_new(locale.into(), plural_options).unwrap();

        // Handle any additional locales
        #(if locale.normalizing_eq(#additional_locales) {
            #additional_messages
            return Ok(());
        }) else*
        // Fall back to the canonical locale, if no other valid locale was matched
        #canonical_message

        Ok(())
    })
}

/// Create a list of unique field names that can be referenced
fn unique_named_fields(named_fields: &syn::FieldsNamed) -> HashSet<String> {
    named_fields
        .named
        .iter()
        // Get the `syn::Ident` for each field
        .map(|field| {
            field
                .ident
                .as_ref()
                .expect("Named fields should have an associated ident")
        })
        .map(|ident| ident.to_string())
        .collect::<HashSet<String>>()
}

pub fn locales_for_ident(group: &fluent::Group, ident: &syn::Ident) -> TokenStream {
    let id = ident.to_string().to_kebab_case();

    let locale_literals = group
        .locales_for_message(&id)
        .map(|locale| locale.id.to_string())
        .map(|locale_string| syn::LitStr::new(&locale_string, proc_macro2::Span::call_site()));

    // There is only one message for this struct, so just list every supported locale
    quote!(vec![#(::fluent_embed::langid!(#locale_literals)),*])
}

pub fn message_for_struct(
    mut group: fluent::Group,
    ident: &syn::Ident,
    fields: &syn::Fields,
) -> Result<TokenStream, MacroError> {
    // Turn the struct fields into a list of valid references
    let references = match fields {
        syn::Fields::Named(named_fields) => Context {
            // Reference using `self.{reference_name}`
            reference_kind: ReferenceKind::StructField,
            // Create a list of unique field names that can be referenced
            valid_references: unique_named_fields(named_fields),
        },
        syn::Fields::Unit => Context {
            reference_kind: ReferenceKind::StructField,
            valid_references: HashSet::new(),
        },
        syn::Fields::Unnamed(_unnamed_fields) => {
            return Err(MacroError::Unsupported(UnsupportedError {
                span: ident.clone(),
                reason: UnsupportedReason::UnnamedFields,
            }))
        }
    };

    Ok(expr_for_message(&mut group, ident, &references)?)
}

pub fn locales_for_enum(
    group: &fluent::Group,
    enum_variants: &Punctuated<syn::Variant, syn::token::Comma>,
) -> TokenStream {
    let mut match_arms: Vec<TokenStream> = Vec::with_capacity(enum_variants.len());

    for enum_variant in enum_variants {
        let variant_ident = &enum_variant.ident;

        // Simplify match code by always ignoring enum fields (even if they don't exist)
        // We are matching the variant name, not any data, so each arm will have something like:
        // Self::VariantName { .. }
        // Even if `Self::VariantName` doesn't contain any data
        let locales_for_variant = locales_for_ident(group, variant_ident);
        match_arms.push(quote!(Self::#variant_ident { .. } => #locales_for_variant));
    }

    quote! {
        match self {
            #(#match_arms),*
        }
    }
}

pub fn messages_for_enum(
    mut group: fluent::Group,
    enum_variants: &Punctuated<syn::Variant, syn::token::Comma>,
) -> Result<TokenStream, MacroError> {
    // let mut match_arms: HashMap<String, syn::ExprBlock> = HashMap::with_capacity(enum_variants.len());
    let mut match_arms: Vec<TokenStream> = Vec::with_capacity(enum_variants.len());

    for enum_variant in enum_variants {
        let variant_pascal_case = &enum_variant.ident;

        // Destructure fields of the enum variant
        // E.g. for the variant:
        // Emails { unread_emails: u64 }
        // Create the expression:
        // Self::Emails { unread_emails }
        let destructuring_pattern = match &enum_variant.fields {
            syn::Fields::Named(named_fields) => {
                let named_field_idents = named_fields.named.iter().map(|field| &field.ident);
                quote!(#variant_pascal_case { #(#named_field_idents),* })
            }
            syn::Fields::Unit => quote!(#variant_pascal_case),
            syn::Fields::Unnamed(_unnamed_fields) => {
                return Err(MacroError::Unsupported(UnsupportedError {
                    span: enum_variant.ident.clone(),
                    reason: UnsupportedReason::UnnamedFields,
                }))
            }
        };

        let references = match &enum_variant.fields {
            syn::Fields::Named(named_fields) => Context {
                reference_kind: ReferenceKind::EnumField,
                valid_references: unique_named_fields(named_fields),
            },
            syn::Fields::Unit => Context {
                reference_kind: ReferenceKind::EnumField,
                valid_references: HashSet::new(),
            },
            syn::Fields::Unnamed(_unnamed_fields) => {
                return Err(MacroError::Unsupported(UnsupportedError {
                    span: enum_variant.ident.clone(),
                    reason: UnsupportedReason::UnnamedFields,
                }))
            }
        };

        let arm_body = expr_for_message(&mut group, &enum_variant.ident, &references)?;
        match_arms.push(quote!(Self::#destructuring_pattern => { #arm_body }));
    }

    Ok(quote! {
        match self {
            #(#match_arms),*
        }
    })
}