//! Parse a raw spec value into a typed [`Spec`].
//!
//! A spec in the DSL is itself a plain value: an object whose keys are either
//! `$`-prefixed commands or child key paths, or a bare function. This module
//! turns such a value into a [`Spec`] and enforces the two top-level guards
//! that the engine relies on:
//!
//! - An array spec is allowed only when the target is also an array.
//! - A spec must be a non-null object (or a function).
//!
//! Command argument shapes are checked here too, so a malformed `$push`,
//! `$splice`, `$set`, and so on fails with the exact message text.

use indexmap::IndexMap;

use crate::context::{read_child, Context, BUILTINS};
use crate::error::{stringifiable, UpdateError, UpdateErrorKind};
use crate::spec::{Command, Spec, SpecEntry, SpliceArgs};
use crate::value::Value;

impl Context {
    /// Parse a raw spec value and apply it to the target.
    ///
    /// This is the entry point that mirrors the JavaScript `update(object,
    /// spec)` where the spec is an ordinary value. It runs the same guards and
    /// then the engine. A typed [`Spec`] built directly can also be passed to
    /// [`Context::update`].
    pub fn update_value(&self, object: &Value, raw_spec: &Value) -> crate::error::UpdateResult {
        let spec = self.parse_spec(object, raw_spec)?;
        self.update(object, &spec)
    }

    /// Parse a raw spec value into a typed [`Spec`] against a target.
    ///
    /// `object` is needed only for the array-spec guard, which permits an array
    /// spec when the target is an array.
    pub(crate) fn parse_spec(&self, object: &Value, raw_spec: &Value) -> Result<Spec, UpdateError> {
        // A function spec is shorthand for `{$apply: fn}`.
        if let Value::Function(f) = raw_spec {
            return Ok(Spec::Apply(f.clone()));
        }

        let target_is_array = matches!(object, Value::Array(_));
        let spec_is_array = matches!(raw_spec, Value::Array(_));

        // Array-spec guard: an array spec is allowed only when the target is an
        // array as well. The array then carries per-index child specs.
        if spec_is_array && !target_is_array {
            return Err(self.array_spec_error());
        }

        // Object-spec guard: the spec must be a non-null object.
        let entries = match raw_spec {
            Value::Object(map) => self.parse_object_spec(object, map)?,
            Value::Array(items) => self.parse_array_spec(object, items)?,
            _ => return Err(self.invalid_spec_error(UpdateErrorKind::InvalidSpec)),
        };

        Ok(Spec::Object(entries))
    }

    /// Parse an object-shaped spec into ordered entries.
    fn parse_object_spec(
        &self,
        object: &Value,
        map: &IndexMap<String, Value>,
    ) -> Result<Vec<SpecEntry>, UpdateError> {
        // `$set` may not share an object with other string keys.
        if map.contains_key("$set") && map.len() > 1 {
            return Err(UpdateError::new(
                UpdateErrorKind::MultipleKeysWithSet,
                "Cannot have more than one key in an object with $set",
            ));
        }

        let mut entries = Vec::with_capacity(map.len());
        for (key, value) in map {
            if BUILTINS.contains(&key.as_str()) || self.has_custom(key) {
                entries.push(SpecEntry::Command(self.parse_command(object, key, value)?));
            } else {
                // A child key path. The child value must itself be a valid spec.
                let child = read_child(object, key);
                let child_spec = self.parse_spec(&child, value)?;
                entries.push(SpecEntry::Child {
                    key: key.clone(),
                    spec: child_spec,
                });
            }
        }
        Ok(entries)
    }

    /// Parse an array-shaped spec into per-index child entries.
    fn parse_array_spec(
        &self,
        object: &Value,
        items: &[Value],
    ) -> Result<Vec<SpecEntry>, UpdateError> {
        let mut entries = Vec::with_capacity(items.len());
        for (index, value) in items.iter().enumerate() {
            let key = index.to_string();
            let child = read_child(object, &key);
            let child_spec = self.parse_spec(&child, value)?;
            entries.push(SpecEntry::Child {
                key,
                spec: child_spec,
            });
        }
        Ok(entries)
    }

    /// Parse one command key and its raw argument into a [`Command`].
    ///
    /// Guard order follows the JavaScript invariants. For `$push`, `$unshift`,
    /// `$splice`, `$add`, and `$remove` the target type is checked before the
    /// spec argument, so a bad target reports the target error first.
    fn parse_command(
        &self,
        object: &Value,
        key: &str,
        raw: &Value,
    ) -> Result<Command, UpdateError> {
        // A custom command overrides a built-in of the same name. Check the
        // custom table first so `extend("$push", ...)` takes effect.
        if self.has_custom(key) {
            return Ok(Command::Custom(key.to_string(), raw.clone()));
        }
        match key {
            "$push" => {
                expect_array_target(object, "$push")?;
                Ok(Command::Push(expect_array(raw, "$push")?))
            }
            "$unshift" => {
                expect_array_target(object, "$unshift")?;
                Ok(Command::Unshift(expect_array(raw, "$unshift")?))
            }
            "$splice" => {
                expect_splice_target(object)?;
                Ok(Command::Splice(parse_splice(raw)?))
            }
            "$set" => Ok(Command::Set(raw.clone())),
            "$toggle" => Ok(Command::Toggle(expect_key_array(raw, "$toggle")?)),
            "$unset" => Ok(Command::Unset(expect_key_array(raw, "$unset")?)),
            "$merge" => Ok(Command::Merge(parse_merge(raw)?)),
            "$add" => {
                expect_map_or_set(object, "$add")?;
                Ok(Command::Add(expect_array(raw, "$add")?))
            }
            "$remove" => {
                expect_map_or_set(object, "$remove")?;
                Ok(Command::Remove(expect_array(raw, "$remove")?))
            }
            "$apply" => match raw {
                Value::Function(f) => Ok(Command::Apply(f.clone())),
                other => Err(UpdateError::new(
                    UpdateErrorKind::WrongSpecArg {
                        command: "$apply".to_string(),
                    },
                    format!(
                        "update(): expected spec of $apply to be a function; got {}.",
                        stringifiable(other)
                    ),
                )),
            },
            _ => Ok(Command::Custom(key.to_string(), raw.clone())),
        }
    }

    fn array_spec_error(&self) -> UpdateError {
        UpdateError::new(
            UpdateErrorKind::ArraySpecOnNonArray,
            "update(): You provided an invalid spec to update(). The spec may \
not contain an array except as the value of $set, $push, $unshift, $splice or \
any custom command allowing an array value.",
        )
    }
}

/// Require the target of `$push`/`$unshift` to be an array.
fn expect_array_target(object: &Value, command: &str) -> Result<(), UpdateError> {
    if matches!(object, Value::Array(_)) {
        Ok(())
    } else {
        Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: command.to_string(),
            },
            format!(
                "update(): expected target of {command} to be an array; got {}.",
                stringifiable(object)
            ),
        ))
    }
}

/// Require the target of `$splice` to be an array.
fn expect_splice_target(object: &Value) -> Result<(), UpdateError> {
    if matches!(object, Value::Array(_)) {
        Ok(())
    } else {
        Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: "$splice".to_string(),
            },
            format!(
                "Expected $splice target to be an array; got {}",
                stringifiable(object)
            ),
        ))
    }
}

/// Require the target of `$add`/`$remove` to be a `Map` or `Set`.
fn expect_map_or_set(object: &Value, command: &str) -> Result<(), UpdateError> {
    if matches!(object, Value::Map(_) | Value::Set(_)) {
        Ok(())
    } else {
        Err(UpdateError::new(
            UpdateErrorKind::WrongTarget {
                command: command.to_string(),
            },
            format!(
                "update(): {command} expects a target of type Set or Map; got {}",
                object.type_tag()
            ),
        ))
    }
}

/// Require an array argument, returning its items.
fn expect_array(raw: &Value, command: &str) -> Result<Vec<Value>, UpdateError> {
    match raw {
        Value::Array(items) => Ok((**items).clone()),
        other => Err(UpdateError::new(
            UpdateErrorKind::WrongSpecArg {
                command: command.to_string(),
            },
            format!(
                "update(): expected spec of {command} to be an array; got {}. Did \
you forget to wrap your parameter in an array?",
                stringifiable(other)
            ),
        )),
    }
}

/// Require an array of string keys, returning them.
fn expect_key_array(raw: &Value, command: &str) -> Result<Vec<String>, UpdateError> {
    let items = expect_array(raw, command)?;
    Ok(items
        .into_iter()
        .map(|v| match v {
            Value::String(s) => s.to_string(),
            other => stringifiable(&other),
        })
        .collect())
}

/// Parse a `$splice` argument: an array of argument arrays.
fn parse_splice(raw: &Value) -> Result<Vec<SpliceArgs>, UpdateError> {
    let outer = match raw {
        Value::Array(items) => items,
        other => {
            return Err(UpdateError::new(
                UpdateErrorKind::WrongSpecArg {
                    command: "$splice".to_string(),
                },
                format!(
                    "update(): expected spec of $splice to be an array of arrays; \
got {}. Did you forget to wrap your parameters in an array?",
                    stringifiable(other)
                ),
            ))
        }
    };

    let mut args = Vec::with_capacity(outer.len());
    for item in outer.iter() {
        let inner = match item {
            Value::Array(inner) => inner,
            other => {
                return Err(UpdateError::new(
                    UpdateErrorKind::WrongSpecArg {
                        command: "$splice".to_string(),
                    },
                    format!(
                        "update(): expected spec of $splice to be an array of \
arrays; got {}. Did you forget to wrap your parameters in an array?",
                        stringifiable(other)
                    ),
                ))
            }
        };
        if inner.is_empty() {
            args.push(SpliceArgs::empty());
            continue;
        }
        let start = number_as_i64(&inner[0]);
        let delete_count = inner.get(1).map(number_as_i64);
        let items: Vec<Value> = inner.iter().skip(2).cloned().collect();
        args.push(SpliceArgs::new(start, delete_count, items));
    }
    Ok(args)
}

/// Parse a `$merge` argument: a non-null object.
fn parse_merge(raw: &Value) -> Result<IndexMap<String, Value>, UpdateError> {
    match raw {
        Value::Object(map) => Ok((**map).clone()),
        other => Err(UpdateError::new(
            UpdateErrorKind::WrongSpecArg {
                command: "$merge".to_string(),
            },
            format!(
                "update(): $merge expects a spec of type 'object'; got {}",
                stringifiable(other)
            ),
        )),
    }
}

/// Coerce a value to an integer for splice indices, matching JS number coercion
/// of the common cases.
fn number_as_i64(value: &Value) -> i64 {
    match value {
        Value::Number(n) => *n as i64,
        _ => 0,
    }
}
