use proc_macro2::{Ident, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, Data, Field, Generics};

use crate::{
    constants::HASH,
    utils::{get_any_attribute_by_meta_prefix, meta_not_found_in_all_fields, resolve_all_fields},
};

pub fn impl_hashed_by(ident: &Ident, data: &Data, generics: &Generics) -> TokenStream {
    let ident_location = ident.to_string();
    let fields = resolve_all_fields(
        data,
        true,
        &is_hash_field,
        &meta_not_found_in_all_fields(HASH, &ident_location),
    );
    let hash_fields_token = hash_fields_token(&fields);
    let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
    quote! {
        impl #impl_generics std::hash::Hash for #ident #type_generics #where_clause {
            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
                #hash_fields_token
            }
        }
    }
}

fn is_hash_field(field: &Field) -> bool {
    get_any_attribute_by_meta_prefix(HASH, &field.attrs, false, "").is_some()
}

fn hash_fields_token(fields: &[Field]) -> TokenStream {
    let hashed_fields = fields.iter().map(|f| hash_field_token(f));
    quote! {
        #(#hashed_fields);*
    }
}

fn hash_field_token(field: &Field) -> TokenStream {
    let field = field.to_owned();
    let field_span = field.span();
    let field_ident = field.ident.unwrap();
    quote_spanned! { field_span =>
        self.#field_ident.hash(state)
    }
}
