//! Immutable nested-state updates through a command-spec DSL.
//!
//! Apply a declarative spec to a value and get back a new value. Only the
//! touched parts are copied. Everything else is shared with the input by
//! reference, so an unchanged branch stays pointer-identical. That sharing
//! makes cheap identity checks possible after an update.
//!
//! The spec is a tree of `$`-prefixed commands and child key paths, inspired by
//! MongoDB update operators. Ten commands are built in: `$push`, `$unshift`,
//! `$splice`, `$set`, `$toggle`, `$unset`, `$add`, `$remove`, `$merge`, and
//! `$apply`. A bare function is shorthand for `$apply`.
//!
//! # Data model
//!
//! Targets and spec values are [`Value`]s. Container variants hold their
//! contents in [`std::rc::Rc`], so cloning a value clones a pointer. After an
//! update, [`Value::same_value`] reports whether a node is the same allocation as
//! before. This is the basis of the reference-equality contract.
//!
//! # Examples
//!
//! Push onto an array:
//!
//! ```
//! use immutability_helper::{update, Spec, Value};
//!
//! let original = Value::array(vec![Value::from("x")]);
//! let result = update(&original, &Spec::push(vec![Value::from("y")])).unwrap();
//! assert_eq!(
//!     result,
//!     Value::array(vec![Value::from("x"), Value::from("y")])
//! );
//! ```
//!
//! Set a nested value while keeping untouched branches shared:
//!
//! ```
//! use immutability_helper::{update, Spec, Value};
//!
//! let original = Value::object(vec![
//!     ("a", Value::object(vec![("b", Value::from(1))])),
//!     ("c", Value::object(vec![("d", Value::from(2))])),
//! ]);
//!
//! // Change a.b to 2. The c branch stays shared.
//! let spec = Spec::at("a", Spec::at("b", Spec::set(Value::from(2))));
//! let result = update(&original, &spec).unwrap();
//!
//! let (Value::Object(orig), Value::Object(next)) = (&original, &result) else {
//!     unreachable!()
//! };
//! assert!(orig["c"].same_value(&next["c"]));
//! ```

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod context;
mod error;
mod parse;
mod spec;
mod value;

use std::cell::RefCell;

pub use context::{Context, CustomCommand, IsEquals};
pub use error::{stringifiable, UpdateError, UpdateErrorKind, UpdateResult};
pub use spec::{Command, Spec, SpecEntry, SpliceArgs};
pub use value::{Callback, Key, Value};

thread_local! {
    /// The default context backing the free functions.
    ///
    /// Each thread has its own default context. `extend` and
    /// `set_default_is_equals` mutate the default context for the current
    /// thread only. A directive added here is visible to [`update`] and
    /// [`update_value`] calls in the same thread.
    static DEFAULT: RefCell<Context> = RefCell::new(Context::new());
}

/// Apply a typed spec to a target with the default context.
///
/// Build the spec with the helpers on [`Spec`], for example [`Spec::set`] or
/// [`Spec::at`]. To apply a spec expressed as a raw [`Value`], use
/// [`update_value`].
pub fn update(object: &Value, spec: &Spec) -> UpdateResult {
    DEFAULT.with(|ctx| ctx.borrow().update(object, spec))
}

/// Apply a raw spec value to a target with the default context.
///
/// The spec is an ordinary [`Value`]: an object whose keys are commands or
/// child key paths, or a function. The spec guards run during parsing, so a
/// malformed spec returns an [`UpdateError`].
pub fn update_value(object: &Value, raw_spec: &Value) -> UpdateResult {
    DEFAULT.with(|ctx| ctx.borrow().update_value(object, raw_spec))
}

/// Register a custom command on the default context.
///
/// The command becomes visible to [`update`] and [`update_value`] in the
/// current thread only. The callback receives the spec value, the current
/// value, and the original target.
pub fn extend<F>(directive: impl Into<String>, f: F)
where
    F: Fn(&Value, &Value, &Value) -> UpdateResult + 'static,
{
    DEFAULT.with(|ctx| ctx.borrow_mut().extend(directive, f));
}

/// Replace the equality function on the current thread's default context.
pub fn set_default_is_equals<F>(f: F)
where
    F: Fn(&Value, &Value) -> bool + 'static,
{
    DEFAULT.with(|ctx| ctx.borrow_mut().set_is_equals(f));
}

/// Compare two values with the default context's equality function.
///
/// This is the same comparator that [`update`] and [`update_value`] use to
/// decide whether a node changed in the current thread. By default it is `===`
/// semantics: containers by reference, primitives by value, `NaN` never equal
/// to itself. After [`set_default_is_equals`] in the same thread it reflects
/// the installed function, so a caller can read it to predict whether an update
/// will collapse a node.
pub fn is_equals(a: &Value, b: &Value) -> bool {
    DEFAULT.with(|ctx| (ctx.borrow().is_equals())(a, b))
}

/// Return a fresh [`Context`] with its own command table and equality function.
///
/// Use this when you want custom commands that stay isolated from the default
/// context.
pub fn new_context() -> Context {
    Context::new()
}
