use std::{borrow, fmt, hash, ops::Deref};

use rustc_hash::FxHashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::Visitor};

/// Add or remove global variables.
///
/// For each global variable, set the corresponding value equal to `"writable"`
/// to allow the variable to be overwritten or `"readonly"` to disallow overwriting.
///
/// Globals can be disabled by setting their value to `"off"`. For example, in
/// an environment where most Es2015 globals are available but `Promise` is unavailable,
/// you might use this config:
///
/// ```json
///
/// {
///     "$schema": "./node_modules/oxlint/configuration_schema.json",
///     "env": {
///         "es6": true
///     },
///     "globals": {
///         "Promise": "off"
///     }
/// }
///
/// ```
///
/// You may also use `"readable"` or `false` to represent `"readonly"`, and
/// `"writeable"` or `true` to represent `"writable"`.
// <https://eslint.org/docs/v8.x/use/configure/language-options#using-configuration-files-1>
#[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema, Clone)]
pub struct OxlintGlobals(FxHashMap<String, GlobalValue>);

impl Deref for OxlintGlobals {
    type Target = FxHashMap<String, GlobalValue>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl OxlintGlobals {
    pub fn is_enabled<Q>(&self, name: &Q) -> bool
    where
        String: borrow::Borrow<Q>,
        Q: ?Sized + Eq + hash::Hash,
    {
        self.0.get(name).is_some_and(|value| *value != GlobalValue::Off)
    }

    pub(crate) fn override_globals(&self, globals_to_override: &mut OxlintGlobals) {
        for (env, supported) in self.0.clone() {
            globals_to_override.0.insert(env, supported);
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum GlobalValue {
    Readonly,
    Writable,
    Off,
}

impl GlobalValue {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Readonly => "readonly",
            Self::Writable => "writable",
            Self::Off => "off",
        }
    }
}

impl<'de> Deserialize<'de> for GlobalValue {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_any(GlobalValueVisitor)
    }
}

impl From<bool> for GlobalValue {
    #[inline]
    fn from(value: bool) -> Self {
        if value { GlobalValue::Writable } else { GlobalValue::Readonly }
    }
}

impl TryFrom<&str> for GlobalValue {
    type Error = &'static str;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "readonly" | "readable" => Ok(GlobalValue::Readonly),
            "writable" | "writeable" => Ok(GlobalValue::Writable),
            "off" => Ok(GlobalValue::Off),
            _ => Err("Invalid global value"),
        }
    }
}

impl fmt::Display for GlobalValue {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.as_str().fmt(f)
    }
}

struct GlobalValueVisitor;
impl Visitor<'_> for GlobalValueVisitor {
    type Value = GlobalValue;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("'readonly', 'writable', 'off', or a boolean")
    }

    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(v.into())
    }

    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        v.try_into().map_err(E::custom)
    }
}

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

    use super::*;

    macro_rules! globals {
        ($($json:tt)+) => {
            OxlintGlobals::deserialize(&json!($($json)+)).unwrap()
        };
    }

    #[test]
    fn test_deserialize_normal() {
        let globals = globals!({
            "foo": "readonly",
            "bar": "writable",
            "baz": "off",
        });
        assert!(globals.is_enabled("foo"));
        assert!(globals.is_enabled("bar"));
        assert!(!globals.is_enabled("baz"));

        assert_eq!(globals.get("foo"), Some(&GlobalValue::Readonly));
        assert_eq!(globals.get("bar"), Some(&GlobalValue::Writable));
        assert_eq!(globals.get("baz"), Some(&GlobalValue::Off));
    }

    #[test]
    fn test_deserialize_legacy_spelling() {
        let globals = globals!({
            "foo": "readable",
            "bar": "writeable",
        });
        assert!(globals.is_enabled("foo"));
        assert!(globals.is_enabled("bar"));
        // Ensure they map to the correct variants
        assert_eq!(globals.get("foo"), Some(&GlobalValue::Readonly));
        assert_eq!(globals.get("bar"), Some(&GlobalValue::Writable));
    }

    #[test]
    fn test_deserialize_bool() {
        let globals = globals!({
            "foo": true,
            "bar": false,
        });
        assert!(globals.is_enabled("foo"));
        assert!(globals.is_enabled("bar"));

        assert_eq!(globals.get("foo"), Some(&GlobalValue::Writable));
        assert_eq!(globals.get("bar"), Some(&GlobalValue::Readonly));
    }

    #[test]
    fn test_override_globals() {
        let mut globals = OxlintGlobals::deserialize(&serde_json::json!({
            "Foo": "writable",
        }))
        .unwrap();
        let override_globals = OxlintGlobals::deserialize(&serde_json::json!({
            "Foo": "off",
        }))
        .unwrap();

        override_globals.override_globals(&mut globals);

        assert!(!globals.is_enabled("Foo"));
    }
}
