use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemStruct, LitStr, parse::Parse, parse_macro_input};

struct AttrArgs {
    key: String,
    value: String,
}

impl Parse for AttrArgs {
    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
        let ident: syn::Ident = input.parse()?;
        input.parse::<syn::Token![=]>()?;
        let value: LitStr = input.parse()?;

        while !input.is_empty() {
            input.parse::<syn::Token![,]>()?;
            if input.is_empty() {
                break;
            }
            let _extra_ident: syn::Ident = input.parse()?;
            input.parse::<syn::Token![=]>()?;
            let _extra_value: LitStr = input.parse()?;
        }

        Ok(Self {
            key: ident.to_string(),
            value: value.value(),
        })
    }
}

#[proc_macro_attribute]
pub fn source(attr: TokenStream, item: TokenStream) -> TokenStream {
    expand_descriptor(
        attr,
        item,
        "id",
        quote!(::rubo_engine::registry::SourceDescriptor),
        quote!(::rubo_engine::registry::source_constructor),
    )
}

#[proc_macro_attribute]
pub fn function(attr: TokenStream, item: TokenStream) -> TokenStream {
    expand_descriptor(
        attr,
        item,
        "id",
        quote!(::rubo_engine::registry::FunctionDescriptor),
        quote!(::rubo_engine::registry::function_constructor),
    )
}

#[proc_macro_attribute]
pub fn sink(attr: TokenStream, item: TokenStream) -> TokenStream {
    expand_descriptor(
        attr,
        item,
        "id",
        quote!(::rubo_engine::registry::SinkDescriptor),
        quote!(::rubo_engine::registry::sink_constructor),
    )
}

#[proc_macro_attribute]
pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream {
    expand_descriptor(
        attr,
        item,
        "kind",
        quote!(::rubo_engine::registry::DeviceDescriptor),
        quote!(::rubo_engine::registry::device_constructor),
    )
}

fn expand_descriptor(
    attr: TokenStream,
    item: TokenStream,
    expected_key: &str,
    descriptor: proc_macro2::TokenStream,
    constructor: proc_macro2::TokenStream,
) -> TokenStream {
    let args = parse_macro_input!(attr as AttrArgs);
    let item_struct = parse_macro_input!(item as ItemStruct);
    let ty = &item_struct.ident;
    let value = args.value;

    if args.key != expected_key {
        let message = format!("expected `{expected_key} = \"...\"`");
        return quote! {
            compile_error!(#message);
            #item_struct
        }
        .into();
    }

    quote! {
        #item_struct

        ::rubo_engine::inventory::submit! {
            #descriptor {
                id: #value,
                type_name: stringify!(#ty),
                constructor: #constructor::<#ty>,
            }
        }
    }
    .into()
}
