#[cfg(feature = "renaming")]
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, DeriveInput, Error, Field, Ident, Lit, MetaList, NestedMeta, Path, Result};

use syn::Meta::*;
use syn::NestedMeta::*;

use crate::internal::{Attr, ANNO, CLASS, EXTRACT, FIELD_NAME, JADED, RENAME};

#[derive(Default)]
pub(crate) struct FieldData {
    /// The name of the field in the Java class being read
    pub field: Option<String>,
    /// The index of the annotation to read data from
    pub annotation_number: Option<usize>,
    /// The function used to read data from annotations
    pub extract_method: Option<Path>,
}

impl FieldData {
    pub fn from_input(input: &Field) -> Result<Self> {
        let mut field = Attr::new();
        let mut anno = Attr::new();
        let mut extr = Attr::new();
        for meta in input.attrs.iter().flat_map(|a| get_jaded_meta(a)).flatten() {
            match meta {
                Meta(NameValue(m)) if m.path == FIELD_NAME => field.set(&m, get_str(&m.lit)?)?,
                Meta(NameValue(m)) if m.path == ANNO => anno.set(&m, get_usize(&m.lit)?)?,
                Meta(List(m)) if m.path == EXTRACT => extr.set(&m, get_path(&m)?)?,
                _ => return Err(Error::new_spanned(meta, "Unknown attribute")),
            }
        }

        // If an annotation number is specified, there needs to be some way to extract the value
        if anno.is_set() && !extr.is_set() {
            return Err(Error::new_spanned(
                anno.tokens(),
                "Expected extract method if annotation number is given",
            ));
        }
        Ok(FieldData {
            field: field.get(),
            annotation_number: anno.get(),
            extract_method: extr.get(),
        })
    }
}

#[derive(Default)]
pub(crate) struct ClassData {
    /// Whether fields should be renamed automatically
    #[cfg(feature = "renaming")]
    pub rename: bool,
    /// Whether the class of the Java object should be checked
    pub class: Option<String>,
}

impl ClassData {
    pub fn from_input(input: &DeriveInput) -> Result<Self> {
        let mut class = Attr::new();
        #[cfg(feature = "renaming")]
        let mut rename = Attr::new();
        for meta in input.attrs.iter().flat_map(|a| get_jaded_meta(a)).flatten() {
            match meta {
                Meta(NameValue(m)) if m.path == CLASS => class.set(&m, get_str(&m.lit)?)?,
                #[cfg(feature = "renaming")]
                Meta(Path(p)) if p == RENAME => rename.set(&p, true)?,
                #[cfg(not(feature = "renaming"))]
                Meta(Path(p)) if p == RENAME => {
                    return Err(Error::new_spanned(
                        p,
                        "Renaming requires the 'renaming' feature",
                    ))
                }
                _ => return Err(Error::new_spanned(meta, "Unknown attribute")),
            }
        }
        Ok(ClassData {
            class: class.get(),
            #[cfg(feature = "renaming")]
            rename: rename.get().unwrap_or(false),
        })
    }

    pub(crate) fn class_check(&self) -> TokenStream {
        match &self.class {
            Some(name) => quote!(
                if data.class_name() != #name {
                    return Err(jaded::ConversionError::InvalidType(#name));
                }
            ),
            None => quote!(),
        }
    }

    #[cfg(not(feature = "renaming"))]
    pub(crate) fn field_name_for(&self, name: &Ident) -> String {
        name.to_string()
    }
    #[cfg(feature = "renaming")]
    pub(crate) fn field_name_for(&self, name: &Ident) -> String {
        match self.rename {
            true => name.to_string().to_case(Case::Camel),
            false => name.to_string(),
        }
    }
}

fn get_usize(lit: &Lit) -> Result<usize> {
    match lit {
        Lit::Int(repr) => repr
            .base10_parse()
            .map_err(|_| Error::new_spanned(lit, "Expected usize")),
        _ => Err(Error::new_spanned(lit, "Expected number literal")),
    }
}

fn get_str(lit: &Lit) -> Result<String> {
    match lit {
        Lit::Str(s) => Ok(s.value()),
        _ => Err(Error::new_spanned(lit, "Expected string")),
    }
}
fn get_path(list: &MetaList) -> Result<Path> {
    match list.nested.len() {
        0 => Err(Error::new_spanned(
            list,
            "Expected path to extractor function",
        )),
        1 => match &list.nested[0] {
            Meta(Path(p)) => Ok(p.clone()),
            _ => Err(Error::new_spanned(
                list,
                "Expected path to extractor function",
            )),
        },
        _ => Err(Error::new_spanned(
            list,
            "Expected single extractor function",
        )),
    }
}

fn get_jaded_meta(attr: &Attribute) -> Result<Vec<NestedMeta>> {
    if attr.path != JADED {
        return Ok(vec![]);
    }
    match attr.parse_meta()? {
        List(meta) => Ok(meta.nested.into_iter().collect()),
        other => Err(Error::new_spanned(other, "Expected #[jaded(...)]")),
    }
}
