use crate::tests::*;
use proptest::prelude::*;
use quote::quote;
use std::collections::HashMap;
use syn::LitStr;

pub(crate) const MAX_VALUE_DEEP: u8 = 1;

#[enum_ids::enum_ids]
#[derive(Debug, Clone)]
pub(crate) enum Value {
    U8(u8),
    U16(u16),
    U32(u32),
    U64(u64),
    U128(u128),
    I8(i8),
    I16(i16),
    I32(i32),
    I64(i64),
    I128(i128),
    F32(f32),
    F64(f64),
    Bool(bool),
    Blob(Vec<u8>),
    String(String),
    Option(Option<Box<Value>>),
    Tuple(Box<Value>, Box<Value>),
    HashMap(HashMap<String, Value>),
    Vec(Vec<Value>),
}

impl Value {
    pub fn is_ordered_ty(&self) -> bool {
        match self {
            Self::U8(..)
            | Self::U16(..)
            | Self::U32(..)
            | Self::U64(..)
            | Self::U128(..)
            | Self::I8(..)
            | Self::I16(..)
            | Self::I32(..)
            | Self::I64(..)
            | Self::I128(..)
            | Self::F32(..)
            | Self::F64(..)
            | Self::Bool(..)
            | Self::Blob(..)
            | Self::String(..) => true,
            Self::HashMap(..) => false,
            Self::Vec(v) => v.first().map(|v| v.is_ordered_ty()).unwrap_or(true),
            Self::Tuple(a, b) => {
                let a = a.is_ordered_ty();
                let b = b.is_ordered_ty();
                a && b
            }
            Self::Option(v) => v.as_ref().map(|v| v.is_ordered_ty()).unwrap_or(true),
        }
    }
}

#[allow(clippy::derivable_impls)]
impl Default for ValueId {
    fn default() -> Self {
        Self::U8
    }
}

impl Arbitrary for Value {
    type Parameters = (ValueId, u8);

    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with((id, deep): (ValueId, u8)) -> Self::Strategy {
        match id {
            ValueId::U8 => any::<u8>().prop_map(Value::U8).boxed(),
            ValueId::U16 => any::<u16>().prop_map(Value::U16).boxed(),
            ValueId::U32 => any::<u32>().prop_map(Value::U32).boxed(),
            ValueId::U64 => any::<u64>().prop_map(Value::U64).boxed(),
            ValueId::U128 => any::<u128>().prop_map(Value::U128).boxed(),
            ValueId::I8 => any::<i8>().prop_map(Value::I8).boxed(),
            ValueId::I16 => any::<i16>().prop_map(Value::I16).boxed(),
            ValueId::I32 => any::<i32>().prop_map(Value::I32).boxed(),
            ValueId::I64 => any::<i64>().prop_map(Value::I64).boxed(),
            ValueId::I128 => any::<i128>().prop_map(Value::I128).boxed(),
            ValueId::F32 => any::<f32>()
                .prop_filter("not NaN; not Inf", |v| !v.is_infinite() && !v.is_nan())
                .prop_map(Value::F32)
                .boxed(),
            ValueId::F64 => any::<f64>()
                .prop_filter("not NaN; not Inf", |v| !v.is_infinite() && !v.is_nan())
                .prop_map(Value::F64)
                .boxed(),
            ValueId::Bool => any::<bool>().prop_map(Value::Bool).boxed(),
            ValueId::Blob => prop::collection::vec(any::<u8>(), 0..100)
                .prop_map(|v| Value::Blob(v.into_iter().collect()))
                .boxed(),
            ValueId::String => any::<String>().prop_map(Value::String).boxed(),
            ValueId::Vec => if deep > MAX_VALUE_DEEP {
                Target::primitive_values()
            } else {
                Target::nested_values()
            }
            .prop_flat_map(move |id| {
                prop::collection::vec(Value::arbitrary_with((id, deep + 1)), 0..100)
                    .prop_map(Value::Vec)
            })
            .boxed(),
            ValueId::HashMap => if deep > MAX_VALUE_DEEP {
                Target::primitive_values()
            } else {
                Target::nested_values()
            }
            .prop_flat_map(move |id| {
                prop::collection::vec(Value::arbitrary_with((id, deep + 1)), 0..100).prop_map(
                    |els| {
                        let mut map = HashMap::new();
                        for (n, el) in els.into_iter().enumerate() {
                            map.insert(n.to_string(), el);
                        }
                        Value::HashMap(map)
                    },
                )
            })
            .boxed(),
            ValueId::Tuple => if deep > MAX_VALUE_DEEP {
                (Target::primitive_values(), Target::primitive_values())
            } else {
                (Target::nested_values(), Target::nested_values())
            }
            .prop_flat_map(move |(a, b)| {
                (
                    Value::arbitrary_with((a, deep + 1)),
                    Value::arbitrary_with((b, deep + 1)),
                )
                    .prop_map(|(a, b)| Value::Tuple(Box::new(a), Box::new(b)))
            })
            .boxed(),
            ValueId::Option => if deep > MAX_VALUE_DEEP {
                Target::primitive_values()
            } else {
                Target::nested_values()
            }
            .prop_flat_map(move |id| {
                prop::option::of(Value::arbitrary_with((id, deep + 1)))
                    .prop_map(|v| Value::Option(v.map(Box::new)))
            })
            .boxed(),
        }
    }
}

impl Generate for Value {
    type Options = ();
    fn declaration(&self, _: ()) -> TokenStream {
        match self {
            Self::U8(..) => quote! { u8 },
            Self::U16(..) => quote! { u16 },
            Self::U32(..) => quote! { u32 },
            Self::U64(..) => quote! { u64 },
            Self::U128(..) => quote! { u128 },
            Self::I8(..) => quote! { i8 },
            Self::I16(..) => quote! { i16 },
            Self::I32(..) => quote! { i32 },
            Self::I64(..) => quote! { i64 },
            Self::I128(..) => quote! { i128 },
            Self::F32(..) => quote! { f32 },
            Self::F64(..) => quote! { f64 },
            Self::Bool(..) => quote! { bool },
            Self::Blob(v) => {
                let len = v.len();
                quote! { [u8; #len] }
            }
            Self::String(..) => quote! { String },
            Self::HashMap(v) => {
                let ty = v
                    .values()
                    .next()
                    .map(|v| v.declaration(()))
                    .unwrap_or(quote! { u8 });
                quote! { std::collections::HashMap<String, #ty>}
            }
            Self::Vec(v) => {
                let ty = v
                    .first()
                    .map(|v| v.declaration(()))
                    .unwrap_or(quote! { u8 });
                quote! { Vec<#ty> }
            }
            Self::Tuple(a, b) => {
                let a = a.declaration(());
                let b = b.declaration(());
                quote! { (#a, #b) }
            }
            Self::Option(v) => {
                let ty = v
                    .as_ref()
                    .map(|v| v.declaration(()))
                    .unwrap_or(quote! { u8 });
                quote! { Option<#ty> }
            }
        }
    }
    fn instance(&self, _: ()) -> TokenStream {
        match self {
            Self::U8(v) => quote! { #v },
            Self::U16(v) => quote! { #v },
            Self::U32(v) => quote! { #v },
            Self::U64(v) => quote! { #v },
            Self::U128(v) => quote! { #v },
            Self::I8(v) => quote! { #v },
            Self::I16(v) => quote! { #v },
            Self::I32(v) => quote! { #v },
            Self::I64(v) => quote! { #v },
            Self::I128(v) => quote! { #v },
            Self::F32(v) => quote! { #v },
            Self::F64(v) => quote! { #v },
            Self::Bool(v) => quote! { #v },
            Self::Blob(v) => {
                let vals = v
                    .iter()
                    .map(|v| quote! { #v })
                    .collect::<Vec<TokenStream>>();
                quote! {[#(#vals,)*]}
            }
            Self::String(s) => {
                let s = LitStr::new(s, proc_macro2::Span::call_site());
                quote! { String::from(#s) }
            }
            Self::HashMap(v) => {
                let vals = v
                    .iter()
                    .map(|(key, v)| {
                        let v = v.instance(());
                        quote! {(String::from(#key), #v)}
                    })
                    .collect::<Vec<TokenStream>>();
                quote! { std::collections::HashMap::from([#(#vals,)*]) }
            }
            Self::Vec(v) => {
                let vals = v
                    .iter()
                    .map(|v| v.instance(()))
                    .collect::<Vec<TokenStream>>();
                quote! {vec![#(#vals,)*]}
            }
            Self::Tuple(a, b) => {
                let a = a.instance(());
                let b = b.instance(());
                quote! { (#a, #b) }
            }
            Self::Option(v) => {
                if let Some(v) = v {
                    let v = v.instance(());
                    quote! { Some(#v) }
                } else {
                    quote! { None }
                }
            }
        }
    }
}
