//! The data model for targets and spec values.
//!
//! [`Value`] mirrors the plain JavaScript values that the update DSL operates
//! on. Container variants wrap their contents in [`Rc`] so that two values can
//! share the same allocation. Sharing is the basis for the reference-equality
//! contract: an update that changes nothing returns a clone of the same `Rc`,
//! and [`Value::same_value`] reports `true` for it.

use std::fmt;
use std::hash::{Hash, Hasher};
use std::rc::Rc;

use indexmap::{IndexMap, IndexSet};

/// A callback used by `$apply` and by custom commands.
///
/// The callback receives the current value and returns a replacement. It is
/// stored behind an [`Rc`] so a [`Value::Function`] is cheap to clone and can
/// be compared by pointer.
pub type Callback = Rc<dyn Fn(&Value) -> Value>;

/// A key in an [`Value::Object`].
///
/// Keys are strings. Insertion order is preserved, which matches the iteration
/// order of `Object.keys` in JavaScript and keeps results stable.
pub type Key = String;

/// A plain data value.
///
/// `Undefined` and `Null` are distinct. `$set: undefined` adds an enumerable
/// key whose value is `Undefined`, so the two cannot collapse into one variant.
///
/// Container variants (`Array`, `Object`, `Map`, `Set`) hold their contents in
/// an [`Rc`]. Cloning a `Value` clones the pointer, not the contents, so shared
/// subtrees stay shared until an update copies them.
#[derive(Clone)]
pub enum Value {
    /// The JavaScript `undefined`.
    Undefined,
    /// The JavaScript `null`.
    Null,
    /// A boolean.
    Bool(bool),
    /// A number. All JavaScript numbers are IEEE-754 doubles.
    Number(f64),
    /// A string.
    String(Rc<str>),
    /// An ordered list. Extra non-index keys are not modeled.
    Array(Rc<Vec<Value>>),
    /// A plain object keyed by string, in insertion order.
    Object(Rc<IndexMap<Key, Value>>),
    /// A `Map` keyed by arbitrary values, in insertion order.
    Map(Rc<IndexMap<Value, Value>>),
    /// A `Set` of arbitrary values, in insertion order.
    Set(Rc<IndexSet<Value>>),
    /// A function value, used by `$apply` and the direct-apply shorthand.
    Function(Callback),
}

impl Value {
    /// Build an array value from a vector of items.
    pub fn array(items: Vec<Value>) -> Value {
        Value::Array(Rc::new(items))
    }

    /// Build an object value from key-value pairs, preserving order.
    pub fn object<I, K>(pairs: I) -> Value
    where
        I: IntoIterator<Item = (K, Value)>,
        K: Into<Key>,
    {
        Value::Object(Rc::new(
            pairs.into_iter().map(|(k, v)| (k.into(), v)).collect(),
        ))
    }

    /// Build a `Map` value from key-value pairs, preserving order.
    pub fn map<I>(pairs: I) -> Value
    where
        I: IntoIterator<Item = (Value, Value)>,
    {
        Value::Map(Rc::new(pairs.into_iter().collect()))
    }

    /// Build a `Set` value from items, preserving order.
    pub fn set<I>(items: I) -> Value
    where
        I: IntoIterator<Item = Value>,
    {
        Value::Set(Rc::new(items.into_iter().collect()))
    }

    /// Build a function value from a closure.
    pub fn function<F>(f: F) -> Value
    where
        F: Fn(&Value) -> Value + 'static,
    {
        Value::Function(Rc::new(f))
    }

    /// Return the JavaScript class tag for this value.
    ///
    /// The tag matches `Object.prototype.toString.call(x).slice(8, -1)`, so
    /// numbers report `"Number"`, maps report `"Map"`, and so on. Error
    /// messages and the `$add`/`$remove` target check use this.
    pub fn type_tag(&self) -> &'static str {
        match self {
            Value::Undefined => "Undefined",
            Value::Null => "Null",
            Value::Bool(_) => "Boolean",
            Value::Number(_) => "Number",
            Value::String(_) => "String",
            Value::Array(_) => "Array",
            Value::Object(_) => "Object",
            Value::Map(_) => "Map",
            Value::Set(_) => "Set",
            Value::Function(_) => "Function",
        }
    }

    /// Report `===` equality between two values.
    ///
    /// Containers compare by reference: two values are equal only when they
    /// share the same [`Rc`] allocation. Primitives compare by value, which
    /// matches `===` since JavaScript primitives have no identity. `NaN` is
    /// never equal to itself, matching `===`. This is the default comparator
    /// and the probe for whether an update changed a node.
    pub fn same_value(&self, other: &Value) -> bool {
        match (self, other) {
            (Value::Undefined, Value::Undefined) => true,
            (Value::Null, Value::Null) => true,
            (Value::Bool(a), Value::Bool(b)) => a == b,
            (Value::Number(a), Value::Number(b)) => a == b,
            (Value::String(a), Value::String(b)) => Rc::ptr_eq(a, b) || a == b,
            (Value::Array(a), Value::Array(b)) => Rc::ptr_eq(a, b),
            (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
            (Value::Map(a), Value::Map(b)) => Rc::ptr_eq(a, b),
            (Value::Set(a), Value::Set(b)) => Rc::ptr_eq(a, b),
            (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
            _ => false,
        }
    }
}

impl fmt::Debug for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Value::Undefined => write!(f, "undefined"),
            Value::Null => write!(f, "null"),
            Value::Bool(b) => write!(f, "{b}"),
            Value::Number(n) => write!(f, "{}", format_number(*n)),
            Value::String(s) => write!(f, "{s:?}"),
            Value::Array(items) => f.debug_list().entries(items.iter()).finish(),
            Value::Object(map) => f.debug_map().entries(map.iter()).finish(),
            Value::Map(map) => {
                write!(f, "Map ")?;
                f.debug_map().entries(map.iter()).finish()
            }
            Value::Set(set) => {
                write!(f, "Set ")?;
                f.debug_set().entries(set.iter()).finish()
            }
            Value::Function(_) => write!(f, "[Function]"),
        }
    }
}

/// Render a number the way JavaScript string-coerces it.
///
/// Integers print without a decimal point. This matches the error-message
/// expectations, for example `got 7.` rather than `got 7.0.`.
pub fn format_number(n: f64) -> String {
    if n.is_nan() {
        "NaN".to_string()
    } else if n.is_infinite() {
        if n > 0.0 { "Infinity" } else { "-Infinity" }.to_string()
    } else if n == 0.0 {
        // Both +0.0 and -0.0 string-coerce to "0".
        "0".to_string()
    } else if n.fract() == 0.0 && n.abs() < 1e21 {
        // Format the integer straight from the f64. Casting to i64 would
        // saturate near 9.22e18 and misprint large magnitudes.
        format!("{n:.0}")
    } else if n.abs() >= 1e21 || n.abs() < 1e-6 {
        format_exponential(n)
    } else {
        format!("{n}")
    }
}

fn format_exponential(n: f64) -> String {
    let raw = format!("{n:e}");
    let Some((mantissa, exponent)) = raw.split_once('e') else {
        return raw;
    };

    if exponent.starts_with('-') {
        raw
    } else {
        format!("{mantissa}e+{exponent}")
    }
}

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

    #[test]
    fn finite_numbers_use_exponential_notation_at_decimal_thresholds() {
        assert_eq!(format_number(1e21), "1e+21");
        assert_eq!(format_number(1e-7), "1e-7");
    }
}

impl PartialEq for Value {
    /// Structural equality used by tests and by `IndexMap`/`IndexSet`.
    ///
    /// This is deep equality, distinct from [`Value::same_value`]. `NaN` is treated
    /// as equal to itself here so that maps and sets can store it as a key.
    fn eq(&self, other: &Value) -> bool {
        match (self, other) {
            (Value::Undefined, Value::Undefined) => true,
            (Value::Null, Value::Null) => true,
            (Value::Bool(a), Value::Bool(b)) => a == b,
            (Value::Number(a), Value::Number(b)) => a == b || (a.is_nan() && b.is_nan()),
            (Value::String(a), Value::String(b)) => a == b,
            (Value::Array(a), Value::Array(b)) => a == b,
            (Value::Object(a), Value::Object(b)) => a == b,
            (Value::Map(a), Value::Map(b)) => a == b,
            (Value::Set(a), Value::Set(b)) => a == b,
            (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
            _ => false,
        }
    }
}

impl Eq for Value {}

impl Hash for Value {
    fn hash<H: Hasher>(&self, state: &mut H) {
        std::mem::discriminant(self).hash(state);
        match self {
            Value::Undefined | Value::Null => {}
            Value::Bool(b) => b.hash(state),
            Value::Number(n) => {
                // Canonicalize so that NaN and signed zero hash consistently
                // with the PartialEq above.
                if n.is_nan() {
                    f64::NAN.to_bits().hash(state);
                } else if *n == 0.0 {
                    0u64.hash(state);
                } else {
                    n.to_bits().hash(state);
                }
            }
            Value::String(s) => s.hash(state),
            Value::Array(items) => items.hash(state),
            // Object, Map, and Set compare equal regardless of insertion order,
            // so fold their entry hashes with a commutative sum to keep Hash
            // consistent with Eq.
            Value::Object(map) => {
                let combined = map
                    .iter()
                    .fold(0u64, |acc, entry| acc.wrapping_add(entry_hash(&entry)));
                combined.hash(state);
            }
            Value::Map(map) => {
                let combined = map
                    .iter()
                    .fold(0u64, |acc, entry| acc.wrapping_add(entry_hash(&entry)));
                combined.hash(state);
            }
            Value::Set(set) => {
                let combined = set
                    .iter()
                    .fold(0u64, |acc, value| acc.wrapping_add(entry_hash(value)));
                combined.hash(state);
            }
            Value::Function(cb) => (Rc::as_ptr(cb) as *const () as usize).hash(state),
        }
    }
}

/// Hash one entry with a standalone hasher so container hashes can be folded
/// together without depending on iteration order.
fn entry_hash<T: Hash>(value: &T) -> u64 {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    value.hash(&mut hasher);
    hasher.finish()
}

impl From<bool> for Value {
    fn from(b: bool) -> Value {
        Value::Bool(b)
    }
}

impl From<f64> for Value {
    fn from(n: f64) -> Value {
        Value::Number(n)
    }
}

/// Build a numeric value from an integer type.
///
/// Every JavaScript number is an IEEE-754 double, so the integer is widened to
/// `f64`. Values beyond 2^53 lose precision, the same as in JavaScript.
macro_rules! impl_from_int {
    ($($t:ty),*) => {
        $(
            impl From<$t> for Value {
                fn from(n: $t) -> Value {
                    Value::Number(n as f64)
                }
            }
        )*
    };
}

impl_from_int!(i8, i16, i32, i64, u8, u16, u32, u64, usize);

impl From<&str> for Value {
    fn from(s: &str) -> Value {
        Value::String(Rc::from(s))
    }
}

impl From<String> for Value {
    fn from(s: String) -> Value {
        Value::String(Rc::from(s.as_str()))
    }
}

impl From<&String> for Value {
    fn from(s: &String) -> Value {
        Value::String(Rc::from(s.as_str()))
    }
}

impl From<Vec<Value>> for Value {
    fn from(items: Vec<Value>) -> Value {
        Value::Array(Rc::new(items))
    }
}
