//! This crate provides the derive proc-macro for [derpscfg].
//!
//! Refer to the [derpscfg documentation][docs] instead.
//!
//! [derpscfg]: https://crates.io/crates/derpscfg
//! [docs]: https://docs.rs/derpscfg
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
    Data, DeriveInput, Field, Fields, Ident, Variant, parse_macro_input, punctuated::Punctuated,
    token::Comma,
};

mod attr;
use attr::Attr;

use crate::attr::Attrs;

fn needs_params(named_fields: &[Field]) -> bool {
    named_fields.iter().any(|field| {
        let attrs = Attrs::parse(&field.attrs);
        attrs.contains(&Attr::Param)
    })
}

fn generate_decode(name: &Ident, named_fields: &[Field]) -> TokenStream2 {
    let flds: Vec<_> = named_fields
        .iter()
        .map(|field| {
            let mut fname = field.ident.as_ref().unwrap().to_string();
            let fname_alt = fname.replace("_", "-");
            let vname = format_ident!("value_{}", fname);
            let attrs = Attrs::parse(&field.attrs);

            let directives = if let Some(name) = attrs.field_name() {
                fname = name;
                quote! {
                    let ds = c.remove(#fname).unwrap_or_default();
                }
            } else {
                quote! {
                    let ds = c
                        .remove(#fname)
                        .or_else(|| c.remove(#fname_alt))
                        .unwrap_or_default();
                }
            };

            let decode = if attrs.contains(&Attr::Param) {
                quote! {
                    let #vname = derpscfg::Decoder::params_cardinality(f, &mut d)
                }
            } else {
                quote! {
                    #directives
                    let #vname = derpscfg::Decoder::cardinality(#fname, ds)
                }
            };

            if let Some(def) = attrs.default_value(&vname) {
                quote! {
                    #decode;
                    let #vname = match #vname {
                        Ok(v) => v,
                        Err(derpscfg::Error::MissingValue(_)) => #def,
                        Err(e) => return Err(e),
                    }
                }
            } else {
                quote! {
                    #decode?;
                }
            }
        })
        .collect();

    let inits: Vec<_> = named_fields
        .iter()
        .map(|field| {
            let fname = field.ident.as_ref().unwrap().to_string();
            let vname = format_ident!("value_{}", fname);
            let sfield = format_ident!("{}", fname);
            quote! {
                #sfield: #vname,
            }
        })
        .collect();

    let expanded = quote! {
        impl derpscfg::Decode<#name> for #name {
            fn decode(f: &'static str, mut d: derpscfg::Directive) -> std::result::Result<Vec<#name>, derpscfg::Error> {
                let mut c = d.take_child().unwrap_or_default();
                #(#flds; )*
                if let Some(p) = d.pop_param() {
                    Err(derpscfg::Error::ExcessParam(format!("'{p}' on field '{f}'")))
                } else {
                    Ok(vec![#name { #(#inits)* }])
                }
            }
        }
    };
    expanded
}

fn generate_try_from(name: &Ident, named_fields: &[Field]) -> Option<TokenStream2> {
    if needs_params(named_fields) {
        return None;
    }

    let flds: Vec<_> = named_fields
        .iter()
        .map(|field| {
            let fname = field.ident.as_ref().unwrap().to_string();
            let directives = format_ident!("dirs_{}", fname);
            let vname = format_ident!("value_{}", fname);
            quote! {
                let mut #directives = value
                    .remove(#fname)
                    .or_else(|| value.remove(#fname.replace("_", "-").as_str()))
                    .unwrap_or_default();
                let #vname = derpscfg::Decoder::cardinality(#fname, #directives)?;
            }
        })
        .collect();
    let inits: Vec<_> = named_fields
        .iter()
        .map(|field| {
            let fname = field.ident.as_ref().unwrap().to_string();
            let vname = format_ident!("value_{}", fname);
            let sfield = format_ident!("{}", fname);
            quote! {
                #sfield: #vname,
            }
        })
        .collect();

    let expanded = quote! {
        impl std::convert::TryFrom<derpscfg::Scfg> for #name {
            type Error = derpscfg::Error;
            fn try_from(mut value: derpscfg::Scfg) -> std::result::Result<Self, derpscfg::Error> {
                #(#flds; )*
                Ok(#name { #(#inits)* })
            }
        }
    };
    Some(expanded)
}

fn generate_struct(name: Ident, fields: Fields) -> TokenStream2 {
    let named_fields: Vec<_> = match fields {
        Fields::Named(n) => n.named.into_iter().collect(),
        _ => todo!("unnamed struct fields not supported"),
    };

    let try_from_impl = generate_try_from(&name, &named_fields);
    let decode_impl = generate_decode(&name, &named_fields);

    quote! {
        #try_from_impl
        #decode_impl
    }
}

fn generate_enum(name: Ident, variants: Punctuated<Variant, Comma>) -> TokenStream2 {
    let tname = name.to_string();
    let vars: Vec<_> = variants
        .into_iter()
        .map(|v| {
            if !v.fields.is_empty() {
                todo!("enums with fields not supported")
            };
            let vid = v.ident;
            let vname = vid.to_string();
            let vname_lower = vname.to_lowercase();
            quote! {
                if p == #vname {
                    return Ok(#name::#vid);
                }
                if p.replace("_", "") == #vname_lower {
                    return Ok(#name::#vid);
                }
            }
        })
        .collect();

    quote! {
        impl std::str::FromStr for #name {
            type Err = derpscfg::Error;

            fn from_str(p: &str) -> Result<Self, Self::Err> {
                #(#vars)*
                Err(derpscfg::Error::FromStr(format!("invalid value '{p}' for '{}'", #tname)))
            }
        }
        impl derpscfg::DecodeScalar for #name {
            fn decode_scalar(f: &'static str, mut d: derpscfg::Directive) -> std::result::Result<Vec<#name>, derpscfg::Error> {
                if d.child().is_some() {
                    return Err(derpscfg::Error::FromStr(format!(
                        "'{f}' should be scalar value, but has child node"
                    )));
                }
                d.params()
                    .into_iter()
                    .map(|p| {
                        #(#vars)*
                        Err(derpscfg::Error::FromStr(format!("invalid value '{p}' for '{f}'")))
                    })
                    .collect()
            }
        }
    }
}

#[doc = include_str!("../derive.md")]
#[proc_macro_derive(Derpscfg, attributes(scfg))]
pub fn scfg_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    // let attrs = scfg_attributes(&input.attrs);

    let expanded = match input.data {
        Data::Struct(d) => generate_struct(name, d.fields),
        Data::Enum(e) => generate_enum(name, e.variants),
        Data::Union(_) => todo!("unions not supported"),
    };

    TokenStream::from(expanded)
}
