//! The error type and the helpers that build the exact failure messages.
//!
//! Every guard produces the same text the JavaScript library throws. The
//! messages are part of the observable contract, so they are reproduced
//! verbatim. Each error also carries an [`UpdateErrorKind`], so a caller can
//! branch on the failure category without parsing the message text.
//! [`stringifiable`] formats a value the way the messages expect: a number
//! prints without a decimal point, a string prints as itself, and a container
//! prints its class tag.

use std::error::Error;
use std::fmt;

use crate::value::{format_number, Value};

/// The category of an update failure.
///
/// The message text is the contract for humans. The kind is the contract for
/// code. Match on it to handle a failure without inspecting the message string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateErrorKind {
    /// The spec node was not a valid command object, function, or key path.
    InvalidSpec,
    /// The spec held an array where one is not allowed, and the target was not
    /// an array.
    ArraySpecOnNonArray,
    /// A command ran against a target of the wrong type, for example `$push` on
    /// a non-array.
    WrongTarget {
        /// The command that rejected the target.
        command: String,
    },
    /// A command's spec argument had the wrong shape, for example a `$push`
    /// value that is not an array.
    WrongSpecArg {
        /// The command whose argument was malformed.
        command: String,
    },
    /// An object carried `$set` together with another key.
    MultipleKeysWithSet,
    /// A custom command name was dispatched that no context has registered.
    UnknownCommand,
}

/// An update failed a validation guard.
///
/// The message matches the corresponding JavaScript error text exactly. The
/// [`kind`](UpdateError::kind) reports the failure category.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateError {
    kind: UpdateErrorKind,
    message: String,
}

impl UpdateError {
    /// Build an error from a kind and its message.
    pub fn new(kind: UpdateErrorKind, message: impl Into<String>) -> Self {
        UpdateError {
            kind,
            message: message.into(),
        }
    }

    /// The failure category.
    pub fn kind(&self) -> &UpdateErrorKind {
        &self.kind
    }

    /// Borrow the message text.
    pub fn message(&self) -> &str {
        &self.message
    }
}

impl fmt::Display for UpdateError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl Error for UpdateError {}

/// Format a value for interpolation into an error message.
///
/// Numbers print without a trailing decimal. Strings print as their contents.
/// Booleans and `null`/`undefined` print their literal text. Containers print
/// their class tag, matching the JavaScript `[object Tag]` slice.
pub fn stringifiable(value: &Value) -> String {
    match value {
        Value::Undefined => "undefined".to_string(),
        Value::Null => "null".to_string(),
        Value::Bool(b) => b.to_string(),
        Value::Number(n) => format_number(*n),
        Value::String(s) => s.to_string(),
        Value::Array(_) => "Array".to_string(),
        Value::Object(_) => "Object".to_string(),
        Value::Map(_) => "Map".to_string(),
        Value::Set(_) => "Set".to_string(),
        Value::Function(_) => "Function".to_string(),
    }
}

/// A shorthand result for update operations.
pub type UpdateResult = Result<Value, UpdateError>;
