use crate::dsl::{secondary_files_dsl, type_dsl};
use serde::de::Error;
use serde::{Deserialize, Deserializer, de::DeserializeOwned};
use serde_json::Value;
use std::collections::HashMap;

pub trait FromShortHand {
    #[must_use]
    fn from_shorthand(_key: &str, _value: Value) -> Option<Value> {
        None
    }
}

fn deserialize_map_list<'de, D, T>(deserializer: D, tag: &str) -> Result<Vec<T>, D::Error>
where
    D: Deserializer<'de>,
    T: DeserializeOwned + FromShortHand + Clone,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum TFormat<T> {
        Array(Vec<T>),
        Map(HashMap<String, Value>),
    }

    match TFormat::deserialize(deserializer)? {
        TFormat::Array(items) => Ok(items),
        TFormat::Map(mut map) => {
            let mut result = vec![];
            for (key, value) in &mut map {
                let normalized = match value {
                    Value::Object(m) => {
                        m.insert(tag.to_string(), Value::String(key.clone()));
                        Value::Object(m.clone())
                    }
                    _ => {
                        if let Some(sh) = T::from_shorthand(key, value.to_owned()) {
                            sh
                        } else {
                            Err(serde::de::Error::custom("From Shorthand returned None"))?
                        }
                    }
                };
                result.push(normalized);
            }
            Ok(serde_json::from_value(Value::Array(result)).map_err(serde::de::Error::custom)?)
        }
    }
}

macro_rules! make_deserialize_map_list {
    ($func_name:ident, $tag:expr) => {
        /// Deserializes map and list types together
        /// # Errors
        /// Errors on deserialization error
        pub fn $func_name<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
        where
            D: Deserializer<'de>,
            T: DeserializeOwned + FromShortHand + Clone,
        {
            deserialize_map_list::<D, T>(deserializer, $tag)
        }
    };
}

macro_rules! make_deserialize_map_list_option {
    ($func_name:ident, $tag:expr) => {
        /// Deserializes map and list types together
        /// # Errors
        /// Errors on deserialization error
        pub fn $func_name<'de, D, T>(deserializer: D) -> Result<Option<Vec<T>>, D::Error>
        where
            D: Deserializer<'de>,
            T: DeserializeOwned + FromShortHand + Clone,
        {
            if let Ok(result) = deserialize_map_list::<D, T>(deserializer, $tag) {
                Ok(Some(result))
            } else {
                Ok(None)
            }
        }
    };
}

make_deserialize_map_list!(deserialize_map_list_class, "class");
make_deserialize_map_list!(deserialize_map_list_id, "id");
make_deserialize_map_list!(deserialize_map_list_package, "package");
make_deserialize_map_list!(deserialize_map_list_envname, "envName");
make_deserialize_map_list_option!(deserialize_map_list_option_name, "name");
make_deserialize_map_list_option!(deserialize_map_list_option_class, "class");

#[macro_export]
macro_rules! make_shorthand_impl {
    ($class:ident, $id:expr, $type:expr) => {
        impl FromShortHand for $class {
            fn from_shorthand(key: &str, value: serde_json::Value) -> Option<serde_json::Value> {
                let mut map = $crate::Mapping::new();
                map.insert($id.into(), serde_json::Value::String(key.to_owned()));
                map.insert($type.into(), value);
                Some(serde_json::Value::Object(map))
            }
        }
    };
}

/// Deserializes a type with the cwl typedsl
/// # Errors
/// Returns Error when deserialization fails
pub fn deserialize_with_type_dsl<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    let value = Value::deserialize(deserializer)?;
    let normalized = type_dsl(value);
    T::deserialize(normalized).map_err(D::Error::custom)
}

/// Deserializes a type with the cwl secondaryfiles dsl
/// # Errors
/// Returns Error when deserialization fails
pub fn deserialize_with_secondary_files_dsl<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    let value = Value::deserialize(deserializer)?;
    let normalized = secondary_files_dsl(value);
    T::deserialize(normalized).map_err(D::Error::custom)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[allow(unused)]
    fn test_shorthand_serialization() {
        #[derive(Deserialize, Debug, Clone)]
        enum TheType {
            Art,
            Music,
        }

        #[derive(Deserialize, Debug, Clone)]

        struct DaInput {
            id: String,
            #[serde(rename = "type")]
            type_: TheType,
        }
        make_shorthand_impl!(DaInput, "id", "type");

        #[derive(Deserialize, Debug)]
        struct DaInputBag {
            #[serde(deserialize_with = "deserialize_map_list_id")]
            inputs: Vec<DaInput>,
        }

        let shorthand = r#"
        inputs:
          my_id: Music
        "#;
        let res = serde_saphyr::from_str::<DaInputBag>(shorthand);
        assert!(res.is_ok());
    }
}
