use crate::Taplo;
use anyhow::anyhow;
use serde::{
    ser::{SerializeMap, SerializeSeq},
    Serialize,
};
use taplo::dom::{
    node::{DateTimeValue, DomNode},
    Node,
};
use taplo_common::environment::Environment;
use tokio::io::AsyncReadExt;

impl<E: Environment> Taplo<E> {
    pub async fn execute_toml_test(&self) -> Result<(), anyhow::Error> {
        let mut buf = String::new();
        self.env.stdin().read_to_string(&mut buf).await?;

        let parse = taplo::parser::parse(&buf);

        if !parse.errors.is_empty() {
            for err in parse.errors {
                eprintln!("{err}");
            }

            return Err(anyhow!("invalid toml"));
        }
        let dom = parse.into_dom();

        if let Err(err) = dom.validate() {
            for err in err {
                eprintln!("{err}");
            }
            return Err(anyhow!("invalid toml"));
        }

        serde_json::to_writer(std::io::stdout(), &TomlTestValue::new(&dom))?;

        Ok(())
    }
}

#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TomlTestType {
    String,
    Integer,
    Float,
    Bool,
    DateTime,
    #[serde(rename = "datetime-local")]
    DateTimeLocal,
    #[serde(rename = "date-local")]
    DateLocal,
    #[serde(rename = "time-local")]
    TimeLocal,
}

impl TomlTestType {
    fn of(node: &Node) -> Option<Self> {
        match node {
            Node::Bool(_) => Some(TomlTestType::Bool),
            Node::Integer(_) => Some(TomlTestType::Integer),
            Node::Float(_) => Some(TomlTestType::Float),
            Node::Str(_) => Some(TomlTestType::String),
            Node::Date(d) => match d.value() {
                DateTimeValue::OffsetDateTime(_) => Some(TomlTestType::DateTime),
                DateTimeValue::LocalDateTime(_) => Some(TomlTestType::DateTimeLocal),
                DateTimeValue::Date(_) => Some(TomlTestType::DateLocal),
                DateTimeValue::Time(_) => Some(TomlTestType::TimeLocal),
            },
            Node::Array(_) => None,
            Node::Table(_) => None,
            Node::Invalid(_) => unreachable!(),
        }
    }
}

pub struct TomlTestValue<'a> {
    r#type: Option<TomlTestType>,
    node: &'a Node,
}

impl<'a> TomlTestValue<'a> {
    pub fn new(node: &'a Node) -> Self {
        Self {
            r#type: TomlTestType::of(node),
            node,
        }
    }
}

impl<'a> Serialize for TomlTestValue<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        if let Some(ty) = self.r#type {
            let mut map = serializer.serialize_map(Some(2))?;
            map.serialize_entry("type", &ty)?;
            map.serialize_entry(
                "value",
                &match self.node {
                    Node::Str(d) => d.value().to_string(),
                    Node::Float(f) if f.value().is_nan() => String::from("nan"),
                    Node::Float(f) if f.value().is_infinite() => f.syntax().unwrap().to_string(),
                    _ => serde_json::to_string(&self.node).map_err(serde::ser::Error::custom)?,
                },
            )?;
            map.end()
        } else {
            match &self.node {
                Node::Array(array) => {
                    let items = array.items().read();

                    let mut seq = serializer.serialize_seq(Some(items.len()))?;
                    for value in &**items {
                        seq.serialize_element(&TomlTestValue::new(value))?;
                    }
                    seq.end()
                }
                Node::Table(table) => {
                    let entries = table.entries().read();

                    let mut map = serializer.serialize_map(Some(entries.len()))?;
                    for (key, value) in entries.iter() {
                        map.serialize_entry(key.value(), &TomlTestValue::new(value))?;
                    }
                    map.end()
                }
                _ => unreachable!(),
            }
        }
    }
}
