use crate::internals::objects::ArrayObject;
use crate::internals::objects::BooleanObject;
use crate::internals::objects::FloatObject;
use crate::internals::objects::IntegerObject;
use crate::internals::objects::NullObject;
use crate::internals::objects::ObjectObject;
use crate::internals::objects::StringObject;
use crate::internals::pretty_formatter::PrettyDisplay;
use crate::internals::pretty_formatter::PrettyFormatter;
use serde_json::Number;
use serde_json::Value;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;

#[derive(Clone, Debug, PartialEq)]
pub enum ValueObject {
    Null(NullObject),
    String(StringObject),
    Float(FloatObject),
    Integer(IntegerObject),
    Boolean(BooleanObject),
    Array(ArrayObject),
    Object(ObjectObject),
}

impl ValueObject {
    pub fn is_number(&self) -> bool {
        matches!(self, Self::Integer(_) | Self::Float(_))
    }
}

impl From<Value> for ValueObject {
    fn from(value: Value) -> Self {
        match value {
            Value::Null => Self::Null(NullObject),
            Value::String(inner) => Self::String(inner.into()),
            Value::Number(number) => number.into(),
            Value::Bool(inner) => Self::Boolean(inner.into()),
            Value::Array(inner) => Self::Array(inner.into()),
            Value::Object(inner) => Self::Object(inner.into()),
        }
    }
}

impl From<Number> for ValueObject {
    fn from(number: Number) -> Self {
        if number.is_f64() {
            let n = number
                .as_f64()
                .expect("Expected to convert serde_json::Number to f64");
            Self::Float(n.into())
        } else if number.is_u64() {
            let n = number
                .as_u64()
                .expect("Expected to convert serde_json::Number to u64");
            Self::Integer(n.into())
        } else {
            let n = number
                .as_i64()
                .expect("Expected to convert serde_json::Number to i64");
            Self::Integer(n.into())
        }
    }
}

impl From<NullObject> for ValueObject {
    fn from(inner: NullObject) -> Self {
        Self::Null(inner)
    }
}

impl From<ArrayObject> for ValueObject {
    fn from(inner: ArrayObject) -> Self {
        Self::Array(inner)
    }
}

impl From<BooleanObject> for ValueObject {
    fn from(inner: BooleanObject) -> Self {
        Self::Boolean(inner)
    }
}

impl From<StringObject> for ValueObject {
    fn from(inner: StringObject) -> Self {
        Self::String(inner)
    }
}

impl From<FloatObject> for ValueObject {
    fn from(inner: FloatObject) -> Self {
        Self::Float(inner)
    }
}

impl From<IntegerObject> for ValueObject {
    fn from(inner: IntegerObject) -> Self {
        Self::Integer(inner)
    }
}

impl From<ObjectObject> for ValueObject {
    fn from(inner: ObjectObject) -> Self {
        Self::Object(inner)
    }
}

impl Display for ValueObject {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
        let mut pretty_formatter = PrettyFormatter::new(formatter);
        self.pretty_fmt(&mut pretty_formatter)?;

        Ok(())
    }
}

impl PrettyDisplay for ValueObject {
    fn pretty_fmt(&self, formatter: &mut PrettyFormatter<'_, '_>) -> FmtResult {
        match self {
            Self::Null(inner) => inner.pretty_fmt(formatter),
            Self::String(inner) => inner.pretty_fmt(formatter),
            Self::Float(inner) => inner.pretty_fmt(formatter),
            Self::Integer(inner) => inner.pretty_fmt(formatter),
            Self::Boolean(inner) => inner.pretty_fmt(formatter),
            Self::Array(inner) => inner.pretty_fmt(formatter),
            Self::Object(inner) => inner.pretty_fmt(formatter),
        }
    }

    fn is_indenting(&self) -> bool {
        match self {
            Self::Null(inner) => inner.is_indenting(),
            Self::String(inner) => inner.is_indenting(),
            Self::Float(inner) => inner.is_indenting(),
            Self::Integer(inner) => inner.is_indenting(),
            Self::Boolean(inner) => inner.is_indenting(),
            Self::Array(inner) => inner.is_indenting(),
            Self::Object(inner) => inner.is_indenting(),
        }
    }
}

#[cfg(test)]
mod test_from {
    use super::*;
    use serde_json::json;
    use std::f64;
    use std::iter::empty;

    #[test]
    fn it_should_convert_from_json_null() {
        let output = ValueObject::from(json!(null));
        assert_eq!(output, ValueObject::Null(NullObject));
    }

    #[test]
    fn it_should_convert_from_json_boolean() {
        let output = ValueObject::from(json!(true));
        assert_eq!(output, ValueObject::Boolean(BooleanObject::from(true)));

        let output = ValueObject::from(json!(false));
        assert_eq!(output, ValueObject::Boolean(BooleanObject::from(false)));
    }

    #[test]
    fn it_should_convert_from_json_floats() {
        let output = ValueObject::from(json!(0.0));
        assert_eq!(output, ValueObject::Float(FloatObject::from(0.0)));

        let output = ValueObject::from(json!(123.456));
        assert_eq!(output, ValueObject::Float(FloatObject::from(123.456)));

        let output = ValueObject::from(json!(f64::MAX));
        assert_eq!(output, ValueObject::Float(FloatObject::from(f64::MAX)));

        let output = ValueObject::from(json!(f64::MIN));
        assert_eq!(output, ValueObject::Float(FloatObject::from(f64::MIN)));

        let output = ValueObject::from(json!(f64::consts::PI));
        assert_eq!(
            output,
            ValueObject::Float(FloatObject::from(f64::consts::PI))
        );
    }

    #[test]
    fn it_should_convert_from_json_integers() {
        let output = ValueObject::from(json!(0));
        assert_eq!(output, ValueObject::Integer(IntegerObject::from(0_u64)));

        let output = ValueObject::from(json!(123));
        assert_eq!(output, ValueObject::Integer(IntegerObject::from(123_u64)));

        let output = ValueObject::from(json!(u64::MAX));
        assert_eq!(output, ValueObject::Integer(IntegerObject::from(u64::MAX)));

        let output = ValueObject::from(json!(u64::MIN));
        assert_eq!(output, ValueObject::Integer(IntegerObject::from(u64::MIN)));

        let output = ValueObject::from(json!(i64::MAX));
        assert_eq!(
            output,
            ValueObject::Integer(IntegerObject::from(i64::MAX as u64))
        );

        let output = ValueObject::from(json!(i64::MIN));
        assert_eq!(output, ValueObject::Integer(IntegerObject::from(i64::MIN)));
    }

    #[test]
    fn it_should_convert_from_json_strings() {
        let output = ValueObject::from(json!(""));
        assert_eq!(output, ValueObject::String(StringObject::from("")));

        let output = ValueObject::from(json!("abc123"));
        assert_eq!(output, ValueObject::String(StringObject::from("abc123")));
    }

    #[test]
    fn it_should_convert_from_json_arrays() {
        let output = ValueObject::from(json!([]));
        assert_eq!(output, ValueObject::Array(ArrayObject::from(empty())));

        let output = ValueObject::from(json!([0, 123.456, "something"]));
        assert_eq!(
            output,
            ValueObject::Array(ArrayObject::from([
                json!(0),
                json!(123.456),
                json!("something")
            ]))
        );
    }

    #[test]
    fn it_should_convert_from_json_objects() {
        let output = ValueObject::from(json!({}));
        assert_eq!(
            output,
            ValueObject::Object(ObjectObject::from_iter(empty()))
        );

        let output = ValueObject::from(json!({
            "age": 30,
            "name": "Joe",
            "ids": [1, 2, 3],
        }));
        assert_eq!(
            output,
            ValueObject::Object(ObjectObject::from_iter([
                ("age".to_string(), json!(30)),
                ("name".to_string(), json!("Joe")),
                ("ids".to_string(), json!([1, 2, 3])),
            ]))
        );
    }
}

#[cfg(test)]
mod test_fmt {
    use super::*;
    use pretty_assertions::assert_eq;
    use serde_json::Map;
    use std::iter::empty;

    #[test]
    fn it_should_display_null() {
        let output = format!("{}", ValueObject::from(NullObject));
        assert_eq!(output, "null");
    }

    #[test]
    fn it_should_display_boolean() {
        let output = format!("{}", ValueObject::from(BooleanObject::from(true)));
        assert_eq!(output, "true");

        let output = format!("{}", ValueObject::from(BooleanObject::from(false)));
        assert_eq!(output, "false");
    }

    #[test]
    fn it_should_display_integer_type() {
        let output = format!("{}", ValueObject::from(IntegerObject::from(0_u64)));
        assert_eq!(output, "0");

        let output = format!("{}", ValueObject::from(IntegerObject::from(123_u64)));
        assert_eq!(output, "123");

        let output = format!("{}", ValueObject::from(IntegerObject::from(0_i64)));
        assert_eq!(output, "0");

        let output = format!("{}", ValueObject::from(IntegerObject::from(123_i64)));
        assert_eq!(output, "123");

        let output = format!("{}", ValueObject::from(IntegerObject::from(-123_i64)));
        assert_eq!(output, "-123");
    }

    #[test]
    fn it_should_display_float_type() {
        let output = format!("{}", ValueObject::from(FloatObject::from(0.0)));
        assert_eq!(output, "0.0");

        let output = format!("{}", ValueObject::from(FloatObject::from(123.456)));
        assert_eq!(output, "123.456");

        let output = format!("{}", ValueObject::from(FloatObject::from(-123.456)));
        assert_eq!(output, "-123.456");
    }

    #[test]
    fn it_should_display_string_type() {
        let output = format!("{}", ValueObject::from(StringObject::from("")));
        assert_eq!(output, r#""""#);

        let output = format!("{}", ValueObject::from(StringObject::from("something")));
        assert_eq!(output, r#""something""#);
    }

    #[test]
    fn it_should_display_array_type() {
        let output = format!("{}", ValueObject::from(ArrayObject::from(empty())));
        assert_eq!(output, "[]");
    }

    #[test]
    fn it_should_display_object_type() {
        let obj = Map::from_iter(empty());
        let output = format!("{}", ValueObject::from(ObjectObject::from(obj)));
        assert_eq!(output, "{}");
    }
}

#[cfg(test)]
mod test_is_indenting {
    use super::*;
    use serde_json::json;

    #[test]
    fn it_should_defer_to_inside_for_indentation() {
        let obj = ObjectObject::from_iter([("name".to_string(), json!("Joe"))]);
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), true);

        let obj = ArrayObject::from(vec![json!(123), json!(456)]);
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), false);

        let obj = ArrayObject::from(vec![json!({ "foo": "bar" }), json!(456)]);
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), true);

        let obj = FloatObject::from(123.456);
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), false);

        let obj = IntegerObject::from(123_u64);
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), false);

        let obj = BooleanObject::from(true);
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), false);

        let obj = StringObject::from("Joe".to_string());
        let value_obj: ValueObject = obj.clone().into();
        assert_eq!(value_obj.is_indenting(), obj.is_indenting());
        assert_eq!(value_obj.is_indenting(), false);

        let null = NullObject;
        let value_obj: ValueObject = null.clone().into();
        assert_eq!(value_obj.is_indenting(), null.is_indenting());
        assert_eq!(value_obj.is_indenting(), false);
    }
}
