use itertools::Itertools;
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use std::str::FromStr;

use crate::model::unit::EnergyUnit;

#[derive(Debug, Clone, Eq, PartialEq, Copy, Hash, PartialOrd)]
pub enum EnergyRateUnit {
    GGPM,
    GDPM,
    KWHPM,
    KWHPKM,
}

impl EnergyRateUnit {
    pub fn to_uom(&self, _value: f64) -> String {
        todo!()
    }

    pub fn associated_energy_unit(&self) -> EnergyUnit {
        match self {
            EnergyRateUnit::GGPM => EnergyUnit::GallonsGasolineEquivalent,
            EnergyRateUnit::GDPM => EnergyUnit::GallonsDieselEquivalent,
            EnergyRateUnit::KWHPM => EnergyUnit::KilowattHours,
            EnergyRateUnit::KWHPKM => EnergyUnit::KilowattHours,
        }
    }
}

impl std::fmt::Display for EnergyRateUnit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EnergyRateUnit::GGPM => write!(f, "gallons gasoline/mile"),
            EnergyRateUnit::GDPM => write!(f, "gallons diesel/mile"),
            EnergyRateUnit::KWHPM => write!(f, "kilowatt hour/mile"),
            EnergyRateUnit::KWHPKM => write!(f, "kilowatt hour/kilometer"),
        }
    }
}

impl FromStr for EnergyRateUnit {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.split("/").collect_vec()[..] {
            ["gallons gasoline", "mile"] => Ok(EnergyRateUnit::GGPM),
            ["gallons diesel", "mile"] => Ok(EnergyRateUnit::GDPM),
            ["kilowatt hour", "mile"] => Ok(EnergyRateUnit::KWHPM),
            ["kilowatt hour", "kilometer"] => Ok(EnergyRateUnit::KWHPKM),
            ["kWh", "mile"] => Ok(EnergyRateUnit::KWHPM),
            ["kWh", "kilometer"] => Ok(EnergyRateUnit::KWHPKM),
            _ => Err(format!(
                "expected energy rate unit in the format '<energy>/<distance>', found: {s}"
            )),
        }
    }
}

struct StrVisitor;

impl Visitor<'_> for StrVisitor {
    type Value = EnergyRateUnit;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("a string value in the form '<energy_unit>/<distance_unit>'")
    }

    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        let result = FromStr::from_str(v);
        result.map_err(|e| {
            serde::de::Error::custom(format!(
                "while attempting to deserialize value '{v}', had the following error: {e}"
            ))
        })
    }
}

impl<'de> Deserialize<'de> for EnergyRateUnit {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(StrVisitor)
    }
}

impl Serialize for EnergyRateUnit {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.collect_str(&self.to_string())
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::EnergyRateUnit as ERU;
    use serde_json::{self as sj, json};

    #[test]
    fn test_mpg_from_str() {
        let result = ERU::from_str("gallons gasoline/mile");
        let expected = ERU::GGPM;
        assert_eq!(result, Ok(expected))
    }

    #[test]
    fn test_gdpm_from_str() {
        let result = ERU::from_str("gallons diesel/mile");
        let expected = ERU::GDPM;
        assert_eq!(result, Ok(expected))
    }

    #[test]
    fn test_gpm_from_json() {
        let result: Result<ERU, String> =
            sj::from_value(json!("gallons gasoline/mile")).map_err(|e| e.to_string());
        let expected = ERU::GGPM;
        assert_eq!(result, Ok(expected))
    }
}
