//! Map builtins: `put`, `del`, `keys`, `values`, and the in-place variants
//! `put!`, `del!`. Maps are persistent ([`im::HashMap`]) and keyed by any
//! [`Value`]; the unsuffixed ops return a new map sharing structure with the
//! input, the `!` variants mutate a map held in a [`Value::Ref`].
//!
//! The polymorphic readers (`len`, `get`, `contains?`, `fmap`, …) work on
//! maps too — they live in [`crate::prelude::collections`].

use im::{HashMap, Vector};

use crate::runtime::{RuntimeError, Value};
use std::rc::Rc;

use crate::runtime::{Env, NativeFn};

/// All map builtins bound to their canonical names.
pub fn env() -> Env {
    Env::of_builtins(vec![
        ("put", put()),
        ("put!", put_bang()),
        ("keys", keys()),
        ("values", values()),
        ("del", del()),
        ("del!", del_bang()),
    ])
}

/// `(put m k v)`: returns a new map with `k` bound to `v`; `m` is unchanged.
fn put() -> NativeFn {
    NativeFn::pure("put".into(), 3, |args| match &*args[0] {
        Value::Map(m) => {
            let m = m.update(args[1].clone(), args[2].clone());
            Ok(Rc::new(Value::Map(m)))
        }
        other => Err(RuntimeError::TypeMismatch {
            name: "put".into(),
            expected: Value::type_name(&Value::Map(HashMap::new())).into(),
            got: Value::type_name(other).into(),
        }),
    })
    .with_doc(
        "\
(put M K V)

Returns a new map with K bound to V, replacing any existing binding
for K. M is not mutated.

See also: (put! REF K V), (del M K), (get COLL K)."
            .into(),
    )
}

/// `(keys m)`: an array of the map's keys (order unspecified).
fn keys() -> NativeFn {
    NativeFn::pure("keys".into(), 1, |args| match &*args[0] {
        Value::Map(m) => {
            let out: Vector<Rc<Value>> = m.keys().cloned().collect();
            Ok(Rc::new(Value::Array(out)))
        }
        other => Err(RuntimeError::type_mismatch("keys", "map", other)),
    })
    .with_doc(
        "\
(keys M)

Returns array: M's keys, in unspecified order.

See also: (values M)."
            .into(),
    )
}

/// `(values m)`: an array of the map's values (order unspecified).
fn values() -> NativeFn {
    NativeFn::pure("values".into(), 1, |args| match &*args[0] {
        Value::Map(m) => {
            let out: Vector<Rc<Value>> = m.values().cloned().collect();
            Ok(Rc::new(Value::Array(out)))
        }
        other => Err(RuntimeError::type_mismatch("values", "map", other)),
    })
    .with_doc(
        "\
(values M)

Returns array: M's values, in unspecified order.

See also: (keys M)."
            .into(),
    )
}

/// `(del m k)`: the map with key `k` removed (a no-op if `k` is absent).
fn del() -> NativeFn {
    NativeFn::pure("del".into(), 2, |args| match &*args[0] {
        Value::Map(m) => Ok(Rc::new(Value::Map(m.without(&args[1])))),
        other => Err(RuntimeError::type_mismatch("del", "map", other)),
    })
    .with_doc(
        "\
(del M K)

Returns a new map with the binding for K removed — a no-op if K is
absent. M is not mutated.

See also: (del! REF K), (put M K V)."
            .into(),
    )
}

/// `(put! ref k v)`: inserts `(k → v)` into the map held in `ref` and returns
/// the new map. Errors if `ref` is not a ref, or its cell does not hold a map.
fn put_bang() -> NativeFn {
    NativeFn::pure("put!".into(), 3, |args| match &*args[0] {
        Value::Ref(cell) => {
            let new = match &*cell.borrow() {
                Value::Map(m) => Value::Map(m.update(args[1].clone(), args[2].clone())),
                other => return Err(RuntimeError::type_mismatch("put!", "ref<map>", other)),
            };
            *cell.borrow_mut() = new.clone();
            Ok(Rc::new(new))
        }
        other => Err(RuntimeError::type_mismatch("put!", "ref", other)),
    })
    .with_doc(
        "\
(put! REF K V)

Binds K to V in the map held in REF, mutating it in place, and
returns the new map.

REF — ref: must hold a map.

See also: (put M K V), (del! REF K)."
            .into(),
    )
}

/// `(del! ref k)`: removes key `k` from the map held in `ref` (a no-op if `k`
/// is absent) and returns the new map. Errors if `ref` is not a ref, or its
/// cell does not hold a map.
fn del_bang() -> NativeFn {
    NativeFn::pure("del!".into(), 2, |args| match &*args[0] {
        Value::Ref(cell) => {
            let new = match &*cell.borrow() {
                Value::Map(m) => Value::Map(m.without(&args[1])),
                other => return Err(RuntimeError::type_mismatch("del!", "ref<map>", other)),
            };
            *cell.borrow_mut() = new.clone();
            Ok(Rc::new(new))
        }
        other => Err(RuntimeError::type_mismatch("del!", "ref", other)),
    })
    .with_doc(
        "\
(del! REF K)

Removes the binding for K from the map held in REF, mutating it in
place, and returns the new map. A no-op if K is absent.

REF — ref: must hold a map.

See also: (del M K), (put! REF K V)."
            .into(),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::RizzError;

    fn run(src: &str) -> Result<Rc<Value>, RizzError> {
        crate::parse_and_run(src.as_bytes()).map(|(v, _)| v)
    }
    fn run_ok(src: &str) -> Rc<Value> {
        run(src).expect("expected successful eval")
    }

    #[test]
    fn keys_and_values() {
        assert_eq!(*run_ok("(len (keys {1: 2 3: 4}))"), Value::Int(2));
        assert_eq!(*run_ok("(len (values {1: 2 3: 4}))"), Value::Int(2));
        assert_eq!(*run_ok("(contains? (keys {1: 2 3: 4}) 1)"), Value::Int(1));
        assert_eq!(*run_ok("(contains? (values {1: 2 3: 4}) 4)"), Value::Int(1));
    }

    #[test]
    fn del_removes_key() {
        assert_eq!(*run_ok("(len (del {1: 2 3: 4} 1))"), Value::Int(1));
        assert_eq!(*run_ok("(get (del {1: 2 3: 4} 1) 1)"), Value::Unit);
        // deleting an absent key is a no-op
        assert_eq!(*run_ok("(len (del {1: 2} 9))"), Value::Int(1));
    }

    #[test]
    fn keys_rejects_non_map() {
        assert!(matches!(
            run("(keys [1 2])"),
            Err(RizzError::RuntimeError(RuntimeError::TypeMismatch { .. }))
        ));
    }
}
