//! The update engine.
//!
//! A [`Context`] holds the equality function and the table of custom commands.
//! [`Context::update`] runs the algorithm: walk the spec, run each command,
//! recurse into key paths, and copy a container only when a real change is
//! found. Untouched branches keep their original `Rc`, so the result shares
//! structure with the input.

use std::collections::HashMap;
use std::rc::Rc;

use indexmap::IndexMap;

use crate::error::{stringifiable, UpdateError, UpdateErrorKind, UpdateResult};
use crate::spec::{Command, Spec, SpecEntry, SpliceArgs};
use crate::value::Value;

/// The equality test used to decide whether a node changed.
///
/// The default compares by reference for containers and by value for
/// primitives, matching JavaScript `===`. A custom function can widen this, for
/// example to a deep comparison, which lets value-preserving updates collapse
/// back to the original reference.
pub type IsEquals = Rc<dyn Fn(&Value, &Value) -> bool>;

/// A custom command registered through [`Context::extend`].
///
/// The callback receives `(param, original, original_object)`: `param` is the
/// spec value for the command, `original` is the current working value, and
/// `original_object` is the untouched target of this update frame. It returns
/// the replacement value.
pub type CustomCommand = Rc<dyn Fn(&Value, &Value, &Value) -> UpdateResult>;

/// The update engine and its command table.
///
/// Each context owns the built-in command names plus any custom commands added
/// with [`Context::extend`]. Extending one context does not affect another.
#[derive(Clone)]
pub struct Context {
    is_equals: IsEquals,
    custom: HashMap<String, CustomCommand>,
    /// The command names in registration order. Built-ins come first, then any
    /// custom commands in the order they were added. The order feeds the
    /// invalid-spec error message.
    order: Vec<String>,
}

/// The ten built-in command names, in table order. The spec parser reads this
/// to tell a command key from a child key.
pub(crate) const BUILTINS: [&str; 10] = [
    "$push", "$unshift", "$splice", "$set", "$toggle", "$unset", "$add", "$remove", "$merge",
    "$apply",
];

impl Default for Context {
    fn default() -> Self {
        Context::new()
    }
}

impl Context {
    /// Build a context with the default equality and the built-in commands.
    pub fn new() -> Self {
        Context {
            is_equals: Rc::new(default_is_equals),
            custom: HashMap::new(),
            order: BUILTINS.iter().map(|s| s.to_string()).collect(),
        }
    }

    /// Return the current equality function.
    pub fn is_equals(&self) -> &IsEquals {
        &self.is_equals
    }

    /// Replace the equality function.
    ///
    /// The new function drives the per-command identity collapse and the
    /// write-if-changed decision in key paths.
    pub fn set_is_equals<F>(&mut self, f: F)
    where
        F: Fn(&Value, &Value) -> bool + 'static,
    {
        self.is_equals = Rc::new(f);
    }

    /// Register a custom command.
    ///
    /// The directive name should start with `$`. The function receives the spec
    /// value, the current value, and the original target, and returns the
    /// replacement. Registering a name that already exists overwrites it and
    /// keeps its position in the command list. A built-in name can be
    /// overridden the same way: the custom function then runs in place of the
    /// built-in.
    pub fn extend<F>(&mut self, directive: impl Into<String>, f: F)
    where
        F: Fn(&Value, &Value, &Value) -> UpdateResult + 'static,
    {
        let directive = directive.into();
        if !self.custom.contains_key(&directive) && !BUILTINS.contains(&directive.as_str()) {
            self.order.push(directive.clone());
        }
        self.custom.insert(directive, Rc::new(f));
    }

    /// The "invalid spec, no such command" error built from the live table.
    ///
    /// Both the parser and the dispatcher reach this when a node is not a valid
    /// spec, so the message lives in one place. The command list reflects the
    /// built-ins plus any custom commands in registration order. The caller
    /// supplies the kind: a malformed spec node is [`UpdateErrorKind::InvalidSpec`],
    /// an unregistered custom name is [`UpdateErrorKind::UnknownCommand`].
    pub(crate) fn invalid_spec_error(&self, kind: UpdateErrorKind) -> UpdateError {
        UpdateError::new(
            kind,
            format!(
                "update(): You provided an invalid spec to update(). The spec and \
every included key path must be plain objects containing one of the following \
commands: {}.",
                self.order.join(", ")
            ),
        )
    }

    /// Report whether a custom command with this name is registered.
    pub(crate) fn has_custom(&self, name: &str) -> bool {
        self.custom.contains_key(name)
    }

    /// Apply a spec to a target and return the updated value.
    ///
    /// On success the result shares structure with the input wherever nothing
    /// changed. On a guard failure it returns an [`UpdateError`] whose message
    /// matches the JavaScript text.
    pub fn update(&self, object: &Value, spec: &Spec) -> UpdateResult {
        let entries = match spec {
            // A bare function is shorthand for `{$apply: fn}`.
            Spec::Apply(f) => {
                let result = f(object);
                return Ok(if (self.is_equals)(&result, object) {
                    object.clone()
                } else {
                    result
                });
            }
            Spec::Object(entries) => entries,
        };

        let mut next_object = object.clone();
        for entry in entries {
            match entry {
                SpecEntry::Command(command) => {
                    let object_was_next_object = next_object.same_value(object);
                    let result = self.dispatch(command, &next_object, object)?;
                    if object_was_next_object && (self.is_equals)(&result, object) {
                        next_object = object.clone();
                    } else {
                        next_object = result;
                    }
                }
                SpecEntry::Child { key, spec } => {
                    let child = read_child(object, key);
                    let next_value_for_key = self.update(&child, spec)?;
                    let next_object_value = read_child(&next_object, key);

                    let changed = !(self.is_equals)(&next_value_for_key, &next_object_value);
                    let force_undefined =
                        matches!(next_value_for_key, Value::Undefined) && !has_own(object, key);

                    if changed || force_undefined {
                        if next_object.same_value(object) {
                            next_object = copy(object);
                        }
                        write_child(&mut next_object, key, next_value_for_key);
                    }
                }
            }
        }
        Ok(next_object)
    }

    /// Dispatch one command to its implementation.
    fn dispatch(&self, command: &Command, next_object: &Value, object: &Value) -> UpdateResult {
        match command {
            Command::Push(items) => cmd_push(next_object, items),
            Command::Unshift(items) => cmd_unshift(next_object, items),
            Command::Splice(args) => cmd_splice(next_object, object, args),
            Command::Set(value) => Ok(value.clone()),
            Command::Toggle(keys) => cmd_toggle(next_object, keys),
            Command::Unset(keys) => cmd_unset(next_object, object, keys),
            Command::Add(items) => cmd_add(next_object, object, items),
            Command::Remove(keys) => cmd_remove(next_object, object, keys),
            Command::Merge(value) => cmd_merge(next_object, object, value),
            Command::Apply(f) => Ok(f(next_object)),
            Command::Custom(name, param) => match self.custom.get(name) {
                Some(custom) => custom(param, next_object, object),
                None => Err(self.invalid_spec_error(UpdateErrorKind::UnknownCommand)),
            },
        }
    }
}

/// The default `===` equality. Containers compare by reference, primitives by
/// value, and `NaN` is not equal to itself.
fn default_is_equals(a: &Value, b: &Value) -> bool {
    a.same_value(b)
}

/// Shallow-clone a container into a fresh allocation.
///
/// Arrays, objects, maps, and sets get a new `Rc` whose contents are cloned one
/// level deep. Children keep their own `Rc`, so they stay shared. Primitives
/// and functions are returned as-is.
fn copy(value: &Value) -> Value {
    match value {
        Value::Array(items) => Value::Array(Rc::new((**items).clone())),
        Value::Object(map) => Value::Object(Rc::new((**map).clone())),
        Value::Map(map) => Value::Map(Rc::new((**map).clone())),
        Value::Set(set) => Value::Set(Rc::new((**set).clone())),
        other => other.clone(),
    }
}

/// Read the child at `key`. A missing key reads as `Undefined`, matching
/// JavaScript member access. The spec parser shares this for its array-spec
/// guard.
pub(crate) fn read_child(value: &Value, key: &str) -> Value {
    match value {
        Value::Object(map) => map.get(key).cloned().unwrap_or(Value::Undefined),
        Value::Map(map) => map
            .get(&Value::from(key))
            .cloned()
            .unwrap_or(Value::Undefined),
        Value::Array(items) => key
            .parse::<usize>()
            .ok()
            .and_then(|i| items.get(i))
            .cloned()
            .unwrap_or(Value::Undefined),
        _ => Value::Undefined,
    }
}

/// Report whether `value` has its own entry at `key`.
fn has_own(value: &Value, key: &str) -> bool {
    match value {
        Value::Object(map) => map.contains_key(key),
        Value::Map(map) => map.contains_key(&Value::from(key)),
        Value::Array(items) => key.parse::<usize>().is_ok_and(|i| i < items.len()),
        _ => false,
    }
}

/// Write `child` at `key` into a mutable container.
///
/// The container must already be a fresh copy. For a Map the key is wrapped in
/// a string value. For an array a numeric index extends the array as needed.
fn write_child(value: &mut Value, key: &str, child: Value) {
    match value {
        Value::Object(map) => {
            Rc::make_mut(map).insert(key.to_string(), child);
        }
        Value::Map(map) => {
            Rc::make_mut(map).insert(Value::from(key), child);
        }
        Value::Array(items) => {
            if let Ok(index) = key.parse::<usize>() {
                let items = Rc::make_mut(items);
                if index >= items.len() {
                    items.resize(index + 1, Value::Undefined);
                }
                items[index] = child;
            }
        }
        _ => {}
    }
}

// ---------------------------------------------------------------------------
// Built-in commands
// ---------------------------------------------------------------------------

/// `$push`: append items to an array. Empty input keeps the array reference.
fn cmd_push(next_object: &Value, items: &[Value]) -> UpdateResult {
    let Value::Array(arr) = next_object else {
        return Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$push".to_string(),
            },
            format!(
                "update(): expected target of $push to be an array; got {}.",
                stringifiable(next_object)
            ),
        ));
    };
    if items.is_empty() {
        return Ok(next_object.clone());
    }
    let mut out: Vec<Value> = (**arr).clone();
    out.extend(items.iter().cloned());
    Ok(Value::array(out))
}

/// `$unshift`: prepend items to an array. Empty input keeps the reference.
fn cmd_unshift(next_object: &Value, items: &[Value]) -> UpdateResult {
    let Value::Array(arr) = next_object else {
        return Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$unshift".to_string(),
            },
            format!(
                "update(): expected target of $unshift to be an array; got {}.",
                stringifiable(next_object)
            ),
        ));
    };
    if items.is_empty() {
        return Ok(next_object.clone());
    }
    let mut out: Vec<Value> = items.to_vec();
    out.extend(arr.iter().cloned());
    Ok(Value::array(out))
}

/// `$splice`: apply each splice in order, copying on the first real op. An
/// empty argument list is a no-op that does not force a copy.
fn cmd_splice(next_object: &Value, original: &Value, args: &[SpliceArgs]) -> UpdateResult {
    if !matches!(next_object, Value::Array(_)) {
        return Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$splice".to_string(),
            },
            format!(
                "Expected $splice target to be an array; got {}",
                stringifiable(next_object)
            ),
        ));
    }

    let mut current = next_object.clone();
    for arg in args {
        if let SpliceArgs::Call {
            start,
            delete_count,
            items,
        } = arg
        {
            if current.same_value(original) {
                current = copy(original);
            }
            if let Value::Array(arr) = &mut current {
                apply_splice(Rc::make_mut(arr), *start, *delete_count, items);
            }
        }
    }
    Ok(current)
}

/// Apply one `Array.prototype.splice` to a vector in place. `start` and
/// `delete_count` follow JavaScript coercion and clamping.
fn apply_splice(arr: &mut Vec<Value>, start: i64, delete_count: Option<i64>, items: &[Value]) {
    let len = arr.len() as i64;
    let begin = if start < 0 {
        (len + start).max(0)
    } else {
        start.min(len)
    } as usize;
    let max_delete = arr.len() - begin;
    let delete = match delete_count {
        None => max_delete,
        Some(d) => (d.max(0) as usize).min(max_delete),
    };
    arr.splice(begin..begin + delete, items.iter().cloned());
}

/// `$toggle`: set each field to the logical negation of its current value.
///
/// The result for a key is `!current[key]` under JavaScript truthiness, so a
/// truthy number or string toggles to `false` and a falsy value toggles to
/// `true`. Each negation reads the source value, so a repeated key produces the
/// same result both times. Empty input keeps the reference.
fn cmd_toggle(next_object: &Value, keys: &[String]) -> UpdateResult {
    if keys.is_empty() {
        return Ok(next_object.clone());
    }
    let Value::Object(map) = next_object else {
        return Ok(next_object.clone());
    };
    let mut out = (**map).clone();
    for key in keys {
        let was_truthy = truthy(map.get(key));
        out.insert(key.clone(), Value::Bool(!was_truthy));
    }
    Ok(Value::Object(Rc::new(out)))
}

/// JavaScript truthiness of an optional value.
///
/// `undefined`, `null`, `false`, `0`, `NaN`, and the empty string are falsy. A
/// missing key reads as `undefined`, so it is falsy too. Every other value is
/// truthy, including any non-empty string, any non-zero number, and any
/// container.
fn truthy(value: Option<&Value>) -> bool {
    match value {
        None | Some(Value::Undefined) | Some(Value::Null) => false,
        Some(Value::Bool(b)) => *b,
        Some(Value::Number(n)) => *n != 0.0 && !n.is_nan(),
        Some(Value::String(s)) => !s.is_empty(),
        _ => true,
    }
}

/// `$unset`: remove own keys from an object or Map.
///
/// A copy happens only when a key is actually removed. An absent key, or a
/// target that holds no removable key at this key, keeps the original
/// reference. Arrays and primitives have nothing to remove by string key, so
/// `$unset` is a no-op on them.
fn cmd_unset(next_object: &Value, original: &Value, keys: &[String]) -> UpdateResult {
    let mut current = next_object.clone();
    for key in keys {
        let removable = match &current {
            Value::Object(map) => map.contains_key(key),
            Value::Map(map) => map.contains_key(&Value::from(key)),
            _ => false,
        };
        if !removable {
            continue;
        }
        if current.same_value(original) {
            current = copy(original);
        }
        match &mut current {
            Value::Object(map) => {
                Rc::make_mut(map).shift_remove(key);
            }
            Value::Map(map) => {
                Rc::make_mut(map).shift_remove(&Value::from(key));
            }
            _ => {}
        }
    }
    Ok(current)
}

/// `$add`: add to a `Map` (as `[key, value]` pairs) or a `Set`. A Map writes
/// only when the entry differs. A Set writes only when the value is absent.
/// When nothing is written the original reference is kept.
///
/// Membership is compared by value. A structurally equal value counts as
/// already present, so adding it is a no-op.
fn cmd_add(next_object: &Value, original: &Value, items: &[Value]) -> UpdateResult {
    match next_object {
        Value::Map(_) => {
            let mut current = next_object.clone();
            for item in items {
                let (key, value) = match item {
                    Value::Array(pair) if pair.len() == 2 => (pair[0].clone(), pair[1].clone()),
                    _ => continue,
                };
                let differs = matches!(&current, Value::Map(map) if map.get(&key) != Some(&value));
                if !differs {
                    continue;
                }
                if current.same_value(original) {
                    current = copy(original);
                }
                if let Value::Map(map) = &mut current {
                    Rc::make_mut(map).insert(key, value);
                }
            }
            Ok(current)
        }
        Value::Set(_) => {
            let mut current = next_object.clone();
            for value in items {
                let absent = matches!(&current, Value::Set(set) if !set.contains(value));
                if !absent {
                    continue;
                }
                if current.same_value(original) {
                    current = copy(original);
                }
                if let Value::Set(set) = &mut current {
                    Rc::make_mut(set).insert(value.clone());
                }
            }
            Ok(current)
        }
        _ => Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$add".to_string(),
            },
            format!(
                "update(): $add expects a target of type Set or Map; got {}",
                next_object.type_tag()
            ),
        )),
    }
}

/// `$remove`: remove keys from a `Map` or values from a `Set`. An absent key
/// causes no write, so the reference is kept.
///
/// Membership is compared by value. A structurally equal value matches an
/// existing member and is removed.
fn cmd_remove(next_object: &Value, original: &Value, keys: &[Value]) -> UpdateResult {
    match next_object {
        Value::Map(_) => {
            let mut current = next_object.clone();
            for key in keys {
                let present = matches!(&current, Value::Map(map) if map.contains_key(key));
                if !present {
                    continue;
                }
                if current.same_value(original) {
                    current = copy(original);
                }
                if let Value::Map(map) = &mut current {
                    Rc::make_mut(map).shift_remove(key);
                }
            }
            Ok(current)
        }
        Value::Set(_) => {
            let mut current = next_object.clone();
            for value in keys {
                let present = matches!(&current, Value::Set(set) if set.contains(value));
                if !present {
                    continue;
                }
                if current.same_value(original) {
                    current = copy(original);
                }
                if let Value::Set(set) = &mut current {
                    Rc::make_mut(set).shift_remove(value);
                }
            }
            Ok(current)
        }
        _ => Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$remove".to_string(),
            },
            format!(
                "update(): $remove expects a target of type Set or Map; got {}",
                next_object.type_tag()
            ),
        )),
    }
}

/// `$merge`: shallow-merge keys into an object-like target.
///
/// The target guard rejects only non-objects: numbers, strings, booleans,
/// `null`, `undefined`, and functions. Objects, arrays, and Maps pass, matching
/// the `typeof target === 'object'` rule. A key is copied only when its value
/// differs by reference from the current value, so a merge of already-equal
/// values keeps the original reference. For an array a numeric key writes that
/// index. For a Map the key is stored as a string entry.
fn cmd_merge(
    next_object: &Value,
    original: &Value,
    value: &IndexMap<String, Value>,
) -> UpdateResult {
    if !matches!(
        next_object,
        Value::Object(_) | Value::Array(_) | Value::Map(_)
    ) {
        return Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$merge".to_string(),
            },
            format!(
                "update(): $merge expects a target of type 'object'; got {}",
                stringifiable(next_object)
            ),
        ));
    }

    let mut current = next_object.clone();
    for (key, new_value) in value {
        let existing = read_child(&current, key);
        if !new_value.same_value(&existing) {
            if current.same_value(original) {
                current = copy(original);
            }
            write_child(&mut current, key, new_value.clone());
        }
    }
    Ok(current)
}
