/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::Number;
use std::borrow::Cow;
use std::collections::HashMap;

#[cfg(any(
    all(aws_sdk_unstable, feature = "serde-deserialize"),
    all(aws_sdk_unstable, feature = "serde-serialize")
))]
use serde;

/* ANCHOR: document */

/// Document Type
///
/// Document types represents protocol-agnostic open content that is accessed like JSON data.
/// Open content is useful for modeling unstructured data that has no schema, data that can't be
/// modeled using rigid types, or data that has a schema that evolves outside of the purview of a model.
/// The serialization format of a document is an implementation detail of a protocol.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
    all(aws_sdk_unstable, feature = "serde-serialize"),
    derive(serde::Serialize)
)]
#[cfg_attr(
    all(aws_sdk_unstable, feature = "serde-deserialize"),
    derive(serde::Deserialize)
)]
#[cfg_attr(
    any(
        all(aws_sdk_unstable, feature = "serde-deserialize"),
        all(aws_sdk_unstable, feature = "serde-serialize")
    ),
    serde(untagged)
)]
pub enum Document {
    /// JSON object
    Object(HashMap<String, Document>),
    /// JSON array
    Array(Vec<Document>),
    /// JSON number
    Number(Number),
    /// JSON string
    String(String),
    /// JSON boolean
    Bool(bool),
    /// JSON null
    Null,
}

impl Document {
    /// Returns the inner map value if this `Document` is an object.
    pub fn as_object(&self) -> Option<&HashMap<String, Document>> {
        if let Self::Object(object) = self {
            Some(object)
        } else {
            None
        }
    }

    /// Returns the mutable inner map value if this `Document` is an object.
    pub fn as_object_mut(&mut self) -> Option<&mut HashMap<String, Document>> {
        if let Self::Object(object) = self {
            Some(object)
        } else {
            None
        }
    }

    /// Returns the inner array value if this `Document` is an array.
    pub fn as_array(&self) -> Option<&Vec<Document>> {
        if let Self::Array(array) = self {
            Some(array)
        } else {
            None
        }
    }

    /// Returns the mutable inner array value if this `Document` is an array.
    pub fn as_array_mut(&mut self) -> Option<&mut Vec<Document>> {
        if let Self::Array(array) = self {
            Some(array)
        } else {
            None
        }
    }

    /// Returns the inner number value if this `Document` is a number.
    pub fn as_number(&self) -> Option<&Number> {
        if let Self::Number(number) = self {
            Some(number)
        } else {
            None
        }
    }

    /// Returns the inner string value if this `Document` is a string.
    pub fn as_string(&self) -> Option<&str> {
        if let Self::String(string) = self {
            Some(string)
        } else {
            None
        }
    }

    /// Returns the inner boolean value if this `Document` is a boolean.
    pub fn as_bool(&self) -> Option<bool> {
        if let Self::Bool(boolean) = self {
            Some(*boolean)
        } else {
            None
        }
    }

    /// Returns `Some(())` if this `Document` is a null.
    pub fn as_null(&self) -> Option<()> {
        if let Self::Null = self {
            Some(())
        } else {
            None
        }
    }

    /// Returns `true` if this `Document` is an object.
    pub fn is_object(&self) -> bool {
        matches!(self, Self::Object(_))
    }

    /// Returns `true` if this `Document` is an array.
    pub fn is_array(&self) -> bool {
        matches!(self, Self::Array(_))
    }

    /// Returns `true` if this `Document` is a number.
    pub fn is_number(&self) -> bool {
        matches!(self, Self::Number(_))
    }

    /// Returns `true` if this `Document` is a string.
    pub fn is_string(&self) -> bool {
        matches!(self, Self::String(_))
    }

    /// Returns `true` if this `Document` is a bool.
    pub fn is_bool(&self) -> bool {
        matches!(self, Self::Bool(_))
    }

    /// Returns `true` if this `Document` is a boolean.
    pub fn is_null(&self) -> bool {
        matches!(self, Self::Null)
    }
}

/// The default value is `Document::Null`.
impl Default for Document {
    fn default() -> Self {
        Self::Null
    }
}

impl From<bool> for Document {
    fn from(value: bool) -> Self {
        Document::Bool(value)
    }
}

impl<'a> From<&'a str> for Document {
    fn from(value: &'a str) -> Self {
        Document::String(value.to_string())
    }
}

impl<'a> From<Cow<'a, str>> for Document {
    fn from(value: Cow<'a, str>) -> Self {
        Document::String(value.into_owned())
    }
}

impl From<String> for Document {
    fn from(value: String) -> Self {
        Document::String(value)
    }
}

impl From<Vec<Document>> for Document {
    fn from(values: Vec<Document>) -> Self {
        Document::Array(values)
    }
}

impl From<HashMap<String, Document>> for Document {
    fn from(values: HashMap<String, Document>) -> Self {
        Document::Object(values)
    }
}

impl From<u64> for Document {
    fn from(value: u64) -> Self {
        Document::Number(Number::PosInt(value))
    }
}

impl From<i64> for Document {
    fn from(value: i64) -> Self {
        Document::Number(Number::NegInt(value))
    }
}

impl From<i32> for Document {
    fn from(value: i32) -> Self {
        Document::Number(Number::NegInt(value as i64))
    }
}

impl From<f64> for Document {
    fn from(value: f64) -> Self {
        Document::Number(Number::Float(value))
    }
}

impl From<Number> for Document {
    fn from(value: Number) -> Self {
        Document::Number(value)
    }
}

/* ANCHOR END: document */

#[cfg(test)]
mod test {
    /// checks if a) serialization of json suceeds and b) it is compatible with serde_json
    #[test]
    #[cfg(all(
        aws_sdk_unstable,
        feature = "serde-serialize",
        feature = "serde-deserialize"
    ))]
    fn serialize_json() {
        use crate::Document;
        use crate::Number;
        use std::collections::HashMap;
        let mut map: HashMap<String, Document> = HashMap::new();
        // string
        map.insert("hello".into(), "world".to_string().into());
        // numbers
        map.insert("pos_int".into(), Document::Number(Number::PosInt(1).into()));
        map.insert(
            "neg_int".into(),
            Document::Number(Number::NegInt(-1).into()),
        );
        map.insert(
            "float".into(),
            Document::Number(Number::Float(0.1 + 0.2).into()),
        );
        // booleans
        map.insert("true".into(), true.into());
        map.insert("false".into(), false.into());
        // check if array with different datatypes would succeed
        map.insert(
            "array".into(),
            vec![
                map.clone().into(),
                "hello-world".to_string().into(),
                true.into(),
                false.into(),
            ]
            .into(),
        );
        // map
        map.insert("map".into(), map.clone().into());
        // null
        map.insert("null".into(), Document::Null);
        let obj = Document::Object(map);
        // comparing string isnt going to work since there is no gurantee for the ordering of the keys
        let target_file = include_str!("../test_data/serialize_document.json");
        let json: Result<serde_json::Value, _> = serde_json::from_str(target_file);
        // serializer
        assert_eq!(serde_json::to_value(&obj).unwrap(), json.unwrap());
        let doc: Result<Document, _> = serde_json::from_str(target_file);
        assert_eq!(obj, doc.unwrap());
    }
}
