use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::{Data, DeriveInput, Fields, parse_macro_input};

#[proc_macro_derive(FromArgs)]
pub fn derive_cli(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident;

    let fields = match input.data {
        Data::Struct(data) => data.fields,
        _ => panic!("Cli can only be derived for structs"),
    };

    let mut field_parsers_init = Vec::new();
    let mut field_parsers = Vec::new();
    let mut field_parsers_post = Vec::new();
    let mut field_names = Vec::new();

    if let Fields::Named(fields_named) = fields {
        for field in fields_named.named {
            let ident = field.ident.unwrap();
            let name_str = ident.to_string();

            let mut long_name = b"--".to_vec();
            long_name.extend_from_slice(name_str.as_bytes());
            let long_name = proc_macro2::Literal::byte_string(&long_name);

            field_names.push(ident.clone());

            // initialization
            field_parsers_init.push(quote! {
                let mut #ident = None;
            });

            let is_bool = field.ty.to_token_stream().to_string() == String::from("bool");
            let is_option = field.ty.to_token_stream().to_string().starts_with("Option"); // bad bad detection

            // in the loop
            if is_bool {
                field_parsers.push(quote! {
                    if arg == #long_name {
                        #ident = Some(true);
                        continue;
                    }
                });
            } else {
                field_parsers.push(quote! {
                    if arg == #long_name {
                        let Some(val) = iter.next() else {
                            return Err(ArgParseError::MissingArgValue(#name_str));
                        };
                        match String::from_utf8_lossy(val).parse() {
                            Ok(parsed) => {
                                #ident = Some(parsed);
                                continue;
                            }
                            Err(err) => {
                                return Err(ArgParseError::ArgValueParseError(#name_str, format!("{err:?}")));
                            }
                        }
                    }
                });
            }

            // after loop
            if is_bool {
                field_parsers_post.push(quote! { let #ident = #ident.unwrap_or(false); });
            } else if !is_option {
                field_parsers_post.push(quote!{ let Some(#ident) = #ident else { return Err(ArgParseError::MissingArg(#name_str)) }; });
            }
        }
    }

    let expanded = quote! {
        impl ArgParse for #name {
            fn parse<'a>(args: &'a [BString]) -> std::result::Result<Self, ArgParseError<'a>> {
                let mut iter = args.iter();

                #(#field_parsers_init)*

                while let Some(arg) = iter.next() {
                    #(#field_parsers)*;
                    return Err(ArgParseError::LeftoverArg(arg));
                }

                #(#field_parsers_post)*

                Ok(Self { #( #field_names ),* })
            }
        }
    };

    TokenStream::from(expanded)
}
