use proc_macro2::Ident;
use proc_macro_error::{emit_error, emit_warning};
use quote::ToTokens;
use syn::{
    parse_str, Attribute, Lit,
    Meta::{List, Path as MetaPath},
    MetaList,
    NestedMeta::{self, Meta},
    Path, Type,
};

use crate::internal::JADED;

#[derive(Debug)]
pub enum Naming {
    Renamed,
    FieldName,
}

impl Naming {
    pub fn name_for(&self, field: &Ident) -> String {
        match self {
            Naming::Renamed => {
                let field = field.to_string();
                let mut new_name = String::with_capacity(field.len());
                let mut words = field.split('_').filter(|w| !w.is_empty());
                // _ is not a valid ident so unwrap should be safe
                new_name.push_str(words.next().unwrap());
                for word in words {
                    let mut letters = word.chars();
                    // unwrap on first letter should be ok for non-empty string
                    new_name.push(letters.next().unwrap().to_ascii_uppercase());
                    letters.for_each(|l| new_name.push(l));
                }
                new_name
            }
            Naming::FieldName => field.to_string(),
        }
    }
}

#[cfg(test)]
mod name_tests {
    use super::Naming;
    use quote::format_ident;

    #[test]
    fn unchanged() {
        let field = format_ident!("foo");
        assert_eq!(Naming::Renamed.name_for(&field), "foo");
        assert_eq!(Naming::FieldName.name_for(&field), "foo");
    }

    #[test]
    fn basic_renaming() {
        let field = format_ident!("foo_bar");
        assert_eq!(Naming::Renamed.name_for(&field), "fooBar");
        assert_eq!(Naming::FieldName.name_for(&field), "foo_bar");
    }

    #[test]
    fn double_under() {
        let field = format_ident!("foo__bar");
        assert_eq!(Naming::Renamed.name_for(&field), "fooBar");
        assert_eq!(Naming::FieldName.name_for(&field), "foo__bar");
    }

    #[test]
    fn leading_under() {
        let field = format_ident!("_foo_bar");
        assert_eq!(Naming::Renamed.name_for(&field), "fooBar");
        assert_eq!(Naming::FieldName.name_for(&field), "_foo_bar");
    }
}

pub fn get_str(lit: &Lit) -> Option<String> {
    match lit {
        Lit::Str(s) => Some(s.value()),
        _ => {
            emit_error!(lit, "Expected string");
            None
        }
    }
}

pub fn get_type(lit: &Lit) -> Option<Type> {
    let text = get_str(lit)?;
    match parse_str::<Type>(&text) {
        Ok(value) => Some(value),
        Err(e) => {
            emit_error!(lit, "Couldn't parse type: {}", e);
            None
        }
    }
}

pub fn get_extraction(list: &MetaList) -> Option<(Path, usize)> {
    let mut iter = list.nested.iter();
    let path = iter.next();
    let number = iter.next();
    if let Some(extra) = iter.next() {
        emit_error!(
            extra,
            "Expected maximum of 2 arguments: method and (optionally) annotation number";
            help="Try #[jaded(extract({}, {}))]", path.to_token_stream(), number.to_token_stream());
    }
    match (path, number) {
        (Some(Meta(MetaPath(p))), Some(NestedMeta::Lit(Lit::Int(num)))) => {
            match num.base10_parse() {
                Ok(0) => {
                    emit_warning!(num, "Annotation number is optional and 0 is default");
                    Some((p.clone(), 0))
                }
                Ok(num) => Some((p.clone(), num)),
                Err(e) => {
                    emit_error!(num, "Annotation number must be positive value: {}", e);
                    None
                }
            }
        }
        (Some(Meta(MetaPath(p))), None) => Some((p.clone(), 0)),
        (Some(Meta(MetaPath(_))), Some(other)) => {
            emit_error!(other, "Annotation number should be number literal");
            None
        }
        (Some(other), _) => {
            emit_error!(other, "Extraction method should be path to function");
            None
        }
        (None, _) => {
            emit_error!(
                list,
                "Extract requires path to function and optionally an annotation number";
                help="Try #[jaded(extract(path::to::method, 1))]");
            None
        }
    }
}

pub fn get_jaded_meta(attr: &Attribute) -> Option<impl Iterator<Item = NestedMeta>> {
    if attr.path == JADED {
        match attr.parse_meta() {
            Ok(List(meta)) => return Some(meta.nested.into_iter()),
            Ok(other) => emit_error!(other, "Expected #[jaded(...)]"),
            Err(e) => emit_error!(e.span(), "Could not parse attribute: {}", e),
        }
    }
    None
}
