use std::{collections::HashSet, iter::FromIterator};

use proc_macro2::{Ident, TokenStream};
use proc_macro_error::{diagnostic, emit_error, emit_warning, Level};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::{
    parse_str,
    spanned::Spanned,
    Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, Generics,
    Meta::{List, NameValue, Path as MetaPath},
    NestedMeta::{Lit as LitMeta, Meta},
    Path, Type, Variant,
};

use crate::{
    attributes::{get_extraction, get_jaded_meta, get_str, get_type, Naming},
    internal::{Symbol, CLASS, EXTRACT, FIELD_NAME, FROM, RENAME},
    Result,
};

const VALUE: Symbol = Symbol("value");
const DATA: Symbol = Symbol("data");

/// Struct holding configuration required to convert a `DeriveInput` into a `Model`
/// instance. The configuration is read from the attributes of the input.
pub struct Reader {
    /// Whether field names should be converted to camelCase before being read
    /// from the serialized java object.
    naming: Naming,
    /// An optional FQCN to restrict the objects that can be read.
    class: ClassCheck,
    /// The type to read the java object as before converting to the type being
    /// modelled here. Type should implement `Into<T>` where `T` is the type
    /// this derive is implementing.
    proxy: Option<Type>,
}

impl Reader {
    /// Wrapper around creating reader and reading input
    pub fn read_model(input: DeriveInput) -> Result<Model> {
        Reader::from(input.attrs.iter())?.read(input)
    }
    /// Create a new reader from the attributes of the original input
    pub fn from<'a, A: Iterator<Item = &'a Attribute>>(attrs: A) -> Result<Self> {
        let mut class = None;
        let mut rename = None;
        let mut proxy = None;
        for meta in attrs.filter_map(get_jaded_meta).flatten() {
            match meta {
                Meta(NameValue(m)) if m.path == CLASS => match class {
                    Some(_) => emit_warning!(m, "Duplicate class value"),
                    None => class = get_str(&m.lit),
                },
                Meta(m) if m.path() == CLASS => emit_error!(
                    m, "class should be used as a literal attribute";
                    help=r#"Try class = "com.example.FooBar""#),
                Meta(MetaPath(p)) if p == RENAME => match rename {
                    Some(_) => emit_warning!(p, "duplicate rename attribute"),
                    None => rename = Some(Naming::Renamed),
                },
                Meta(m) if m.path() == RENAME => emit_error!(
                        m, "rename does not take any values";
                        help="Try #[jaded(rename)]"),
                Meta(NameValue(m)) if m.path == FROM => match proxy {
                    Some(_) => emit_warning!(m, "Duplicate 'from' attrubute"),
                    None => proxy = get_type(&m.lit),
                },
                Meta(m) if m.path() == FROM => {
                    emit_error!(m, "From requires a path to the alternate type";
                                help=r#"Try #[jaded(from = "path::to::other")]"#);
                }
                Meta(m) => emit_error!(m, "Unknown attribute"),
                LitMeta(v) => emit_error!(v, "Unexpected literal"),
            }
        }
        Ok(Self {
            class: ClassCheck(class),
            naming: rename.unwrap_or(Naming::FieldName),
            proxy,
        })
    }

    /// Read a model from the given input using the current configuration
    pub fn read(self, input: DeriveInput) -> Result<Model> {
        let data = match self.proxy {
            Some(t) => ConversionType::Proxy(input.ident, t),
            None => ConversionType::Derive(match input.data {
                Data::Enum(data) => TypeData::Enum(self.read_enum(input.ident, data)?),
                Data::Struct(data) => TypeData::Struct(self.read_struct(input.ident, data)?),
                Data::Union(_) => {
                    return Err(diagnostic!(
                        input.ident,
                        Level::Error,
                        "Unions are not supported"
                    ))
                }
            }),
        };
        let mut generics = input.generics;
        generics
            .type_params_mut()
            .for_each(|tp| tp.bounds.push(parse_str("::jaded::FromJava").unwrap()));
        Ok(Model { generics, data })
    }

    fn read_enum(self, name: Ident, data: DataEnum) -> Result<EnumData> {
        let vars = data
            .variants
            .into_iter()
            .map(|var| self.read_variant(var))
            .collect::<Result<_>>()?;
        Ok(EnumData(name, vars))
    }

    fn read_struct(self, name: Ident, data: DataStruct) -> Result<StructData> {
        let fields = self.read_fields(data.fields)?;
        Ok(StructData(name, self.class, fields))
    }

    fn read_variant(&self, var: Variant) -> Result<VariantData> {
        let name = var.ident;
        let mut class_name = None;
        for att in var.attrs.iter().filter_map(get_jaded_meta).flatten() {
            match att {
                Meta(NameValue(m)) if m.path == CLASS => match class_name {
                    Some(_) => emit_warning!(m, "Duplicate class attribute"),
                    None => class_name = get_str(&m.lit),
                },
                Meta(m) if m.path() == CLASS => emit_error!(m, "Class should be string"),
                other => emit_error!(other, "Unknown attribute"),
            }
        }
        Ok(VariantData(name, class_name, self.read_fields(var.fields)?))
    }

    fn read_fields(&self, fields: Fields) -> Result<FieldSpec> {
        match fields {
            Fields::Unit => Ok(FieldSpec::Unit),
            Fields::Named(f) => f
                .named
                .into_iter()
                .map(|f| self.read_named_field(f))
                .collect::<Result<_>>(),
            Fields::Unnamed(f) => f
                .unnamed
                .into_iter()
                .map(|f| self.read_unnamed_field(f))
                .collect::<Result<_>>(),
        }
    }

    fn read_named_field(&self, field: Field) -> Result<NamedField> {
        let span = field.span();
        let name = field.ident.clone().ok_or(diagnostic!(
            span,
            Level::Error,
            "Expected a named field - this is a jaded error. Please report it."
        ))?;
        let source = self.read_field_source(field)?;
        Ok(NamedField(name, source))
    }

    fn read_unnamed_field(&self, field: Field) -> Result<UnnamedField> {
        let source = self.read_field_source(field)?;
        Ok(UnnamedField(source))
    }

    fn read_field_source(&self, field: Field) -> Result<FieldSource> {
        let mut field_name = None;
        let mut extraction = None;
        let mut proxy = None;
        for att in field
            .attrs
            .iter()
            .filter_map(get_jaded_meta)
            .flatten()
        {
            match att {
                Meta(NameValue(nv)) if nv.path == FIELD_NAME => match field_name {
                    Some(_) => emit_warning!(nv, "Duplicate field name given"),
                    None => field_name = get_str(&nv.lit).map(|v| (nv.span(), v)),
                },
                Meta(m) if m.path() == FIELD_NAME => emit_error!(
                    m, "Field name requires a string literal";
                    help=r#"Try #[jaded(field = "fooBar")]"#),
                Meta(List(path)) if path.path == EXTRACT => match extraction {
                    Some(_) => emit_warning!(path, "Duplicate extraction method"),
                    None => extraction = get_extraction(&path),
                },
                Meta(m) if m.path() == EXTRACT => emit_error!(
                    m, concat!("Extraction requires a path to a function and an (optional) ",
                    "annotation number");
                    help="#[jaded(extract(path::to::function, 0))]"),
                Meta(NameValue(nv)) if nv.path == FROM => match proxy {
                    Some(_) => emit_warning!(nv, "Duplicate proxy type"),
                    None => proxy = get_type(&nv.lit).map(|v| (nv.span(), v)),
                },
                Meta(m) if m.path() == FROM => emit_error!(
                    m, "Type proxy requires a literal string";
                    help=r#"Try #[jaded(from = "path::to::Type")]"#),
                unk => emit_error!(unk, "Unknown attribute"),
            }
        }
        Ok(match (field_name, extraction) {
            (name, Some(ext)) => {
                if let Some((span, _)) = name {
                    emit_warning!(
                        span,
                        "Field name is not used when extraction method is supplied"
                    );
                }
                if let Some((span, _)) = proxy {
                    emit_warning!(
                        span,
                        "Type proxy is not used when extraction method is supplied"
                    );
                }
                FieldSource::Annotation(ext.0, ext.1)
            }
            (Some((_, name)), None) => {
                FieldSource::Field(proxy.map(|(_, ty)| ty).unwrap_or(field.ty), name)
            }
            (None, None) => match field.ident {
                Some(name) => FieldSource::Field(
                    proxy.map(|(_, ty)| ty).unwrap_or(field.ty),
                    self.naming.name_for(&name),
                ),
                None => FieldSource::Transparent(proxy.map(|(_, ty)| ty).unwrap_or(field.ty)),
            },
        })
    }
}

/// Top level representation of information required to implement `FromJava` for a type
pub struct Model {
    generics: Generics,
    data: ConversionType,
}

enum ConversionType {
    Proxy(Ident, Type),
    Derive(TypeData),
}

struct ClassCheck(Option<String>);

struct VariantData(Ident, Option<String>, FieldSpec);
struct StructData(Ident, ClassCheck, FieldSpec);
struct EnumData(Ident, Vec<VariantData>);

enum TypeData {
    Struct(StructData),
    Enum(EnumData),
}

struct NamedField(Ident, FieldSource);
struct UnnamedField(FieldSource);

enum FieldSpec {
    Named(Vec<NamedField>),
    Unnamed(Vec<UnnamedField>),
    Unit,
}

impl FieldSpec {
    fn setup(&self) -> TokenStream {
        let fields = match self {
            FieldSpec::Unit => HashSet::new(),
            FieldSpec::Named(named) => named
                .iter()
                .filter_map(|spec| spec.1.annotation())
                .collect(),
            FieldSpec::Unnamed(unnamed) => unnamed
                .iter()
                .filter_map(|spec| spec.0.annotation())
                .collect(),
        };
        fields.iter().map(|a| {
            let anno = format_ident!("anno_{}", a);
            quote!(let mut #anno = #DATA.get_annotation(#a).ok_or(::jaded::ConversionError::MissingAnnotations(#a))?;)
        }).collect()
    }
    #[cfg(test)]
    fn is_unit(&self) -> bool {
        matches!(self, FieldSpec::Unit)
    }
    #[cfg(test)]
    fn named_fields(&self) -> &[NamedField] {
        match &self {
            FieldSpec::Named(named) => named,
            _ => panic!("Fields were not named"),
        }
    }
    #[cfg(test)]
    fn unnamed_fields(&self) -> &[UnnamedField] {
        match &self {
            FieldSpec::Unnamed(unnamed) => unnamed,
            _ => panic!("Fields were not unnamed"),
        }
    }
}

impl FromIterator<NamedField> for FieldSpec {
    fn from_iter<T: IntoIterator<Item = NamedField>>(iter: T) -> Self {
        Self::Named(iter.into_iter().collect())
    }
}

impl FromIterator<UnnamedField> for FieldSpec {
    fn from_iter<T: IntoIterator<Item = UnnamedField>>(iter: T) -> Self {
        Self::Unnamed(iter.into_iter().collect())
    }
}

enum FieldSource {
    Field(Type, String),
    Annotation(Path, usize),
    Transparent(Type),
}

impl FieldSource {
    const fn annotation(&self) -> Option<usize> {
        match self {
            FieldSource::Annotation(_, anno) => Some(*anno),
            _ => None,
        }
    }
    #[cfg(test)]
    fn field_type(&self) -> Option<&Type> {
        match self {
            Self::Field(t, _) | Self::Transparent(t) => Some(t),
            _ => None,
        }
    }
    #[cfg(test)]
    fn field_name(&self) -> Option<&str> {
        match self {
            Self::Field(_, name) => Some(name),
            _ => None,
        }
    }
}

impl Model {
    const fn name(&self) -> &Ident {
        use ConversionType::*;
        use TypeData::*;
        let (Proxy(name, _) | Derive(Enum(EnumData(name, _)) | Struct(StructData(name, _, _)))) =
            &self.data;
        name
    }

    pub fn to_tokens(&self) -> TokenStream {
        let name = self.name();
        let name_span = name.span();
        let core = &self.data;
        let (impl_gen, type_gen, where_clause) = self.generics.split_for_impl();
        quote_spanned! {name_span=>
            #[automatically_derived]
            impl #impl_gen ::jaded::FromJava for #name #type_gen #where_clause {
                fn from_value(#VALUE: &::jaded::Value) -> ::jaded::ConversionResult<Self> {
                    #core
                }
            }
        }
    }
    /// Extract proxy name and type as strings or panic
    /// This is only useful for testing where it is known in advance what types
    /// are expected.
    #[cfg(test)]
    fn proxy_data(&self) -> (String, String) {
        match &self.data {
            ConversionType::Proxy(name, ptype) => {
                (name.to_string(), ptype.to_token_stream().to_string())
            }
            _ => panic!("Model was not a proxy"),
        }
    }
    #[cfg(test)]
    fn enum_data(&self) -> &EnumData {
        match &self.data {
            ConversionType::Derive(TypeData::Enum(data)) => data,
            _ => panic!("Model was not an enum"),
        }
    }
    #[cfg(test)]
    fn struct_data(&self) -> &StructData {
        match &self.data {
            ConversionType::Derive(TypeData::Struct(data)) => data,
            _ => panic!("Model was not a struct"),
        }
    }
}

mod tokens {
    use super::*;
    impl ToTokens for ConversionType {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let derive = match self {
                Self::Proxy(_, proxy) => quote!(Ok(<#proxy>::from_value(#VALUE)?.into())),
                Self::Derive(data) => quote! {
                    match #VALUE {
                        ::jaded::Value::Object(#DATA) => {
                            #data
                        },
                        ::jaded::Value::Null => Err(::jaded::ConversionError::NullPointerException),
                        _ => Err(::jaded::ConversionError::InvalidType("object")),
                    }
                },
            };
            tokens.extend(quote! {
                #derive
            });
        }
    }

    impl ToTokens for TypeData {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            tokens.extend(match self {
                TypeData::Enum(data) => quote!(#data),
                TypeData::Struct(data) => quote!(#data),
            });
        }
    }

    impl ToTokens for StructData {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let class = &self.1;
            let name = &self.0;
            let setup = self.2.setup();
            let fields = &self.2;
            tokens.extend(quote! {
                #class
                #setup
                Ok(#name #fields)
            });
        }
    }

    impl ToTokens for EnumData {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let variants = self.1.iter().filter(|v| v.1.is_some());
            tokens.extend(quote! {
                match #DATA.class_name() {
                    #(#variants),*,
                    other => Err(::jaded::ConversionError::UnexpectedClass(other.into()))
                }
            });
        }
    }

    impl ToTokens for ClassCheck {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            if let Some(class) = &self.0 {
                tokens.extend(quote!{
                if #DATA.class_name() != #class {
                    return Err(::jaded::ConversionError::InvalidClass(#DATA.class_name().into(), #class.into()));
                }
            });
            }
        }
    }

    impl ToTokens for VariantData {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let setup = self.2.setup();
            let fields = &self.2;
            let class = &self.1;
            let name = &self.0;
            tokens.extend(quote! {
                #class => {
                    #setup
                    Ok(Self::#name #fields)
                }
            });
        }
    }

    impl ToTokens for FieldSpec {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            tokens.extend(match self {
                FieldSpec::Unit => quote!(),
                FieldSpec::Named(fields) => quote!({#(#fields),*,}),
                FieldSpec::Unnamed(fields) => quote!((#(#fields),*,)),
            });
        }
    }

    impl ToTokens for NamedField {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let NamedField(name, source) = &self;
            tokens.extend(quote!(#name: #source));
        }
    }

    impl ToTokens for UnnamedField {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let source = &self.0;
            tokens.extend(quote!(#source));
        }
    }

    impl ToTokens for FieldSource {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            tokens.extend(match self {
                FieldSource::Field(ftype, name) => {
                    let span = ftype.span();
                    quote_spanned!(span=> #DATA.get_field_as::<#ftype>(#name)?.into())
                }
                FieldSource::Annotation(method, anno) => {
                    let anno = format_ident!("anno_{}", anno);
                    quote!(#method(&mut #anno)?.into())
                }
                FieldSource::Transparent(ftype) => quote!(<#ftype>::from_value(#VALUE)?.into()),
            });
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parse;
    use syn::parse_str;
    macro_rules! model{
        ($($src:tt)*) => {
            Reader::read_model(parse(quote!($($src)*)).unwrap()).unwrap()
        };
    }
    macro_rules! check_name (
        ($act:expr, $exp:literal) => (assert_eq!($act.0, Symbol($exp)));
    );
    macro_rules! check_token (
        ($act:expr, $exp:literal) => (assert_eq!($act.unwrap(), &parse_str($exp).unwrap()));
    );
    #[test]
    fn build_named_struct_model() {
        let model = model!(
            struct FooBar {
                foo: i32,
                #[jaded(extract(utils::read_bar, 3))]
                bar: Vec<Bar>,
            }
        );
        let data = model.struct_data();
        check_name!(data, "FooBar");
        assert_eq!(data.1 .0, None);
        let fields = data.2.named_fields();
        match fields {
            [f1, f2] => {
                check_name!(f1, "foo");
                check_token!(f1.1.field_type(), "i32");
                check_name!(f2, "bar");
                assert_eq!(f2.1.field_name(), None);
            }
            _ => panic!("There were not two named fields"),
        }
    }

    #[test]
    fn build_unnamed_struct_model() {
        let model = model!(
            struct FooBar(i32, String);
        );
        let data = model.struct_data();
        check_name!(data, "FooBar");
        assert_eq!(data.1 .0, None);
        let fields = data.2.unnamed_fields();
        match fields {
            [f1, f2] => {
                check_token!(f1.0.field_type(), "i32");
                check_token!(f2.0.field_type(), "String");
            }
            _ => panic!("There were not two unnamed fields"),
        }
    }

    #[test]
    fn build_unit_struct_model() {
        let model = model!(
            struct FooBar;
        );
        let data = model.struct_data();
        assert!(data.2.is_unit(), "Struct was not a unit struct");
        check_name!(data, "FooBar");
    }

    #[test]
    fn build_enum_model() {
        let model = model!(
            enum Foo {
                Fizz(i32),
                Buzz { x: i32, y: i32 },
            }
        );
        let data = model.enum_data();
        check_name!(data, "Foo");
    }
    #[test]
    fn build_proxy_model() {
        let model = model!(
            #[jaded(from = "FizzBuzz")]
            struct Foo {
                foo: i32,
                bar: String,
            }
        );
        let (name, ptype) = model.proxy_data();
        assert_eq!(name, "Foo");
        assert_eq!(ptype, "FizzBuzz");
    }
    #[test]
    fn rename_fields() {
        let model = model!(
            #[jaded(rename)]
            struct CamelCase {
                foo_bar: String,
                fizz_buzz: i32,
                #[jaded(field = "something")]
                not_this_one: Option<Vec<String>>,
            }
        );
        let data = model.struct_data();
        check_name!(data, "CamelCase");
        let fields = data.2.named_fields();
        match fields {
            [f1, f2, f3] => {
                check_name!(f1, "foo_bar");
                assert_eq!(f1.1.field_name(), Some("fooBar"));
                check_name!(f2, "fizz_buzz");
                assert_eq!(f2.1.field_name(), Some("fizzBuzz"));
                check_name!(f3, "not_this_one");
                assert_eq!(f3.1.field_name(), Some("something"));
            }
            _ => panic!("There were not three named fields"),
        }
    }
    #[test]
    #[should_panic]
    fn no_unions() {
        model!(
            union Foo {
                foo: i32,
                bar: String,
            }
        );
    }
}
