use bevy_macro_utils::BevyManifest;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
    parse::ParseStream, parse_macro_input, punctuated::Punctuated, Data, DataStruct, DeriveInput,
    Field, Fields, Path,
};

#[derive(Default)]
struct RenderResourceFieldAttributes {
    pub ignore: bool,
    pub buffer: bool,
}

#[derive(Default)]
struct RenderResourceAttributes {
    pub from_self: bool,
}

static RENDER_RESOURCE_ATTRIBUTE_NAME: &str = "render_resources";

pub fn derive_render_resources(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    let bevy_render_path: Path = BevyManifest::default().get_path(crate::modules::BEVY_RENDER);
    let attributes = ast
        .attrs
        .iter()
        .find(|a| *a.path.get_ident().as_ref().unwrap() == RENDER_RESOURCE_ATTRIBUTE_NAME)
        .map_or_else(RenderResourceAttributes::default, |a| {
            syn::custom_keyword!(from_self);
            let mut attributes = RenderResourceAttributes::default();
            a.parse_args_with(|input: ParseStream| {
                if input.parse::<Option<from_self>>()?.is_some() {
                    attributes.from_self = true;
                }
                Ok(())
            })
            .expect("Invalid 'render_resources' attribute format.");

            attributes
        });
    let struct_name = &ast.ident;
    let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
    let struct_name_string = struct_name.to_string();

    if attributes.from_self {
        TokenStream::from(quote! {
            impl #impl_generics #bevy_render_path::renderer::RenderResources for #struct_name #type_generics #where_clause {
                fn render_resources_len(&self) -> usize {
                    1
                }

                fn get_render_resource(&self, index: usize) -> Option<&dyn #bevy_render_path::renderer::RenderResource> {
                    if index == 0 {
                        Some(self)
                    } else {
                        None
                    }
                }

                fn get_render_resource_name(&self, index: usize) -> Option<&str> {
                    if index == 0 {
                        Some(#struct_name_string)
                    } else {
                        None
                    }
                }

                fn iter(&self) -> #bevy_render_path::renderer::RenderResourceIterator {
                    #bevy_render_path::renderer::RenderResourceIterator::new(self)
                }
            }
        })
    } else {
        let empty = Punctuated::new();

        let fields = match &ast.data {
            Data::Struct(DataStruct {
                fields: Fields::Named(fields),
                ..
            }) => &fields.named,
            Data::Struct(DataStruct {
                fields: Fields::Unit,
                ..
            }) => &empty,
            _ => panic!("Expected a struct with named fields."),
        };
        let field_attributes = fields
            .iter()
            .map(|field| {
                (
                    field,
                    field
                        .attrs
                        .iter()
                        .find(|a| {
                            *a.path.get_ident().as_ref().unwrap() == RENDER_RESOURCE_ATTRIBUTE_NAME
                        })
                        .map_or_else(RenderResourceFieldAttributes::default, |a| {
                            syn::custom_keyword!(ignore);
                            syn::custom_keyword!(buffer);
                            let mut attributes = RenderResourceFieldAttributes::default();
                            a.parse_args_with(|input: ParseStream| {
                                if input.parse::<Option<ignore>>()?.is_some() {
                                    attributes.ignore = true;
                                } else if input.parse::<Option<buffer>>()?.is_some() {
                                    attributes.buffer = true;
                                }
                                Ok(())
                            })
                            .expect("Invalid 'render_resources' attribute format.");

                            attributes
                        }),
                )
            })
            .collect::<Vec<(&Field, RenderResourceFieldAttributes)>>();
        let mut render_resource_names = Vec::new();
        let mut render_resource_fields = Vec::new();
        let mut render_resource_hints = Vec::new();
        for (field, attrs) in field_attributes.iter() {
            if attrs.ignore {
                continue;
            }

            let field_ident = field.ident.as_ref().unwrap();
            let field_name = field_ident.to_string();
            render_resource_fields.push(field_ident);
            render_resource_names.push(format!("{}_{}", struct_name, field_name));
            if attrs.buffer {
                render_resource_hints
                    .push(quote! {Some(#bevy_render_path::renderer::RenderResourceHints::BUFFER)})
            } else {
                render_resource_hints.push(quote! {None})
            }
        }

        let render_resource_count = render_resource_names.len();
        let render_resource_indices = 0..render_resource_count;

        let struct_name_uppercase = struct_name_string.to_uppercase();
        let render_resource_names_ident =
            format_ident!("{}_RENDER_RESOURCE_NAMES", struct_name_uppercase);
        let render_resource_hints_ident =
            format_ident!("{}_RENDER_RESOURCE_HINTS", struct_name_uppercase);

        TokenStream::from(quote! {
            static #render_resource_names_ident: &[&str] = &[
                #(#render_resource_names,)*
            ];

            static #render_resource_hints_ident: &[Option<#bevy_render_path::renderer::RenderResourceHints>] = &[
                #(#render_resource_hints,)*
            ];

            impl #impl_generics #bevy_render_path::renderer::RenderResources for #struct_name #type_generics #where_clause {
                fn render_resources_len(&self) -> usize {
                    #render_resource_count
                }

                fn get_render_resource(&self, index: usize) -> Option<&dyn #bevy_render_path::renderer::RenderResource> {
                    match index {
                        #(#render_resource_indices => Some(&self.#render_resource_fields),)*
                        _ => None,
                    }
                }

                fn get_render_resource_name(&self, index: usize) -> Option<&str> {
                    #render_resource_names_ident.get(index).copied()
                }

                fn get_render_resource_hints(&self, index: usize) -> Option<#bevy_render_path::renderer::RenderResourceHints> {
                    #render_resource_hints_ident.get(index).and_then(|o| *o)
                }

                fn iter(&self) -> #bevy_render_path::renderer::RenderResourceIterator {
                    #bevy_render_path::renderer::RenderResourceIterator::new(self)
                }
            }
        })
    }
}
