use crate::js::*;
use crate::value::*;
use imbl::*;
use std::rc::Rc;

#[derive(Clone, Debug)]
pub enum BaseFn {
    Nullary(&'static str, fn() -> Value),
    Unary(&'static str, fn(&Value) -> Value),
    Binary(&'static str, fn(&Value, &Value) -> Value),
    Ternary(&'static str, fn(&Value, &Value, &Value) -> Value),
}

pub fn eq(x: &Value, y: &Value) -> Value {
    if x == y {
        Value::True
    } else {
        Value::False
    }
}

pub fn add(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => Value::Number(x + y),
        _ => unreachable!("internal Ludus error: wrong arguments to base add: {x}, {y}"),
    }
}

pub fn sub(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => Value::Number(x - y),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn unbox(x: &Value) -> Value {
    match x {
        Value::Box(cell) => cell.as_ref().borrow().clone(),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn store(b: &Value, val: &Value) -> Value {
    if let Value::Box(cell) = b {
        cell.replace(val.clone());
        val.clone()
    } else {
        unreachable!("internal Ludus error")
    }
}

// TODO: do better than returning just the docstr
// name, patterns, AND docstring
pub fn doc(f: &Value) -> Value {
    match f {
        Value::Fn(f) => f.as_ref().doc(),
        _ => Value::Interned("no documentation found"),
    }
}

pub fn assoc(dict: &Value, key: &Value, value: &Value) -> Value {
    match (dict, key) {
        (Value::Dict(d), Value::Keyword(k)) => {
            Value::Dict(Box::new(d.update(Key::Keyword(k), value.clone())))
        }
        (Value::Dict(d), Value::Interned(k)) => {
            Value::Dict(Box::new(d.update(Key::Interned(k), value.clone())))
        }
        (Value::Dict(d), Value::String(s)) => {
            Value::Dict(Box::new(d.update(Key::String(s.clone()), value.clone())))
        }
        _ => unreachable!("internal Ludus error calling assoc with ({dict}, {key}, {value})"),
    }
}

pub fn r#bool(x: &Value) -> Value {
    match x {
        Value::Nil | Value::False => Value::False,
        _ => Value::True,
    }
}

pub fn chars(x: &Value) -> Value {
    match x {
        Value::Interned(s) => {
            let chars = s.chars();

            let mut charlist = vector![];
            for char in chars {
                if char.is_ascii() {
                    charlist.push_back(Value::String(Rc::new(char.to_string())))
                } else {
                    return Value::Tuple(Rc::new(vec![
                        Value::Keyword("err"),
                        Value::String(Rc::new(format!("{char} is not an ascii character"))),
                    ]));
                }
            }
            Value::Tuple(Rc::new(vec![
                Value::Keyword("ok"),
                Value::List(Box::new(charlist)),
            ]))
        }
        Value::String(s) => {
            let chars = s.chars();

            let mut charlist = vector![];
            for char in chars {
                if char.is_ascii() {
                    charlist.push_back(Value::String(Rc::new(char.to_string())))
                } else {
                    return Value::Tuple(Rc::new(vec![
                        Value::Keyword("err"),
                        Value::String(Rc::new(format!("{char} is not an ascii character"))),
                    ]));
                }
            }
            Value::Tuple(Rc::new(vec![
                Value::Keyword("ok"),
                Value::List(Box::new(charlist)),
            ]))
        }
        _ => unreachable!("internal Ludus error"),
    }
}

// TODO: figure out how to get to opportunistic mutation here
pub fn concat(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Interned(x), Value::Interned(y)) => Value::String(Rc::new(format!("{x}{y}"))),
        (Value::String(x), Value::String(y)) => Value::String(Rc::new(format!("{x}{y}"))),
        (Value::String(x), Value::Interned(y)) => Value::String(Rc::new(format!("{x}{y}"))),
        (Value::Interned(x), Value::String(y)) => Value::String(Rc::new(format!("{x}{y}"))),
        (Value::List(x), Value::List(y)) => {
            let mut newlist = *x.clone();
            newlist.append(*y.clone());
            Value::List(Box::new(newlist))
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn append(x: &Value, y: &Value) -> Value {
    match x {
        Value::List(list) => {
            let mut newlist = list.clone();
            newlist.push_back(y.clone());
            Value::List(newlist)
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn dec(x: &Value) -> Value {
    match x {
        Value::Number(n) => Value::Number(n - 1.0),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn inc(x: &Value) -> Value {
    match x {
        Value::Number(n) => Value::Number(n + 1.0),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn div(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => Value::Number(x / y),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn mult(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => Value::Number(x * y),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn dissoc(dict: &Value, key: &Value) -> Value {
    match (dict, key) {
        (Value::Dict(dict), Value::Keyword(key)) => {
            let mut new = dict.clone();
            new.remove(&Key::Keyword(key));
            Value::Dict(new)
        }
        (Value::Dict(dict), Value::Interned(key)) => {
            let mut new = dict.clone();
            new.remove(&Key::Interned(key));
            Value::Dict(new)
        }
        (Value::Dict(dict), Value::String(key)) => {
            let mut new = dict.clone();
            new.remove(&Key::String(key.clone()));
            Value::Dict(new)
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn first(ordered: &Value) -> Value {
    match ordered {
        Value::List(list) => match list.front() {
            Some(n) => n.clone(),
            None => Value::Nil,
        },
        Value::Tuple(tuple) => match tuple.first() {
            Some(n) => n.clone(),
            None => Value::Nil,
        },
        _ => unreachable!("internal Ludus error"),
    }
}

// TODO: figure out how to handle negative numbers
// the cast from f64 to usize discards sign info
pub fn at(ordered: &Value, i: &Value) -> Value {
    match (ordered, i) {
        (Value::List(list), Value::Number(n)) => {
            let i = *n as usize;
            match list.get(i) {
                Some(n) => n.clone(),
                None => Value::Nil,
            }
        }
        (Value::Tuple(tuple), Value::Number(n)) => {
            let i = *n as usize;
            match tuple.get(i) {
                Some(n) => n.clone(),
                None => Value::Nil,
            }
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn get(dict: &Value, key: &Value) -> Value {
    match (dict, key) {
        (Value::Dict(dict), Value::Keyword(key)) => match dict.get(&Key::Keyword(key)) {
            Some(x) => x.clone(),
            None => Value::Nil,
        },
        (Value::Dict(dict), Value::Interned(key)) => match dict.get(&Key::Interned(key)) {
            Some(x) => x.clone(),
            None => Value::Nil,
        },
        (Value::Dict(dict), Value::String(key)) => match dict.get(&Key::String(key.clone())) {
            Some(x) => x.clone(),
            None => Value::Nil,
        },
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn last(ordered: &Value) -> Value {
    match ordered {
        Value::List(list) => match list.last() {
            Some(x) => x.clone(),
            None => Value::Nil,
        },
        Value::Tuple(tuple) => match tuple.last() {
            Some(x) => x.clone(),
            None => Value::Nil,
        },
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn print(x: &Value) -> Value {
    let Value::List(args) = x else {
        unreachable!("internal Ludus error")
    };
    let out = args
        .iter()
        .map(|val| format!("{val}"))
        .collect::<Vec<_>>()
        .join(" ");
    // println!("{out}");
    console_log!("{out}");
    Value::Keyword("ok")
}

pub fn show(x: &Value) -> Value {
    Value::String(Rc::new(format!("{x}")))
}

pub fn rest(ordered: &Value) -> Value {
    match ordered {
        Value::List(list) => Value::List(Box::new(list.clone().split_at(1).1)),
        Value::Tuple(tuple) => {
            Value::List(Box::new(Vector::from_iter(tuple.iter().next().cloned())))
        }
        Value::Interned(str) => Value::String(Rc::new(str.get(1..).unwrap_or("").to_string())),
        Value::String(str) => Value::String(Rc::new(
            str.clone().as_str().get(1..).unwrap_or("").to_string(),
        )),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn count(coll: &Value) -> Value {
    match coll {
        Value::Dict(d) => Value::Number(d.len() as f64),
        Value::List(l) => Value::Number(l.len() as f64),
        Value::Tuple(t) => Value::Number(t.len() as f64),
        Value::String(s) => Value::Number(s.len() as f64),
        Value::Interned(s) => Value::Number(s.len() as f64),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn range(start: &Value, end: &Value) -> Value {
    match (start, end) {
        (Value::Number(start), Value::Number(end)) => {
            let start = *start as isize;
            let end = *end as isize;
            let mut range = Vector::new();
            for n in start..end {
                range.push_back(Value::Number(n as f64))
            }
            Value::List(Box::new(range))
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn slice(ordered: &Value, start: &Value, end: &Value) -> Value {
    match (ordered, start, end) {
        (Value::List(list), Value::Number(start), Value::Number(end)) => {
            let mut newlist = list.clone();
            let start = std::cmp::max(*start as usize, 0);
            let end = std::cmp::min(*end as usize, list.len());
            Value::List(Box::new(newlist.slice(start..end)))
        }
        // TODO: figure out something better to do than return an empty string on a bad slice
        (Value::String(string), Value::Number(start), Value::Number(end)) => {
            let start = std::cmp::max(*start as usize, 0);
            let end = std::cmp::min(*end as usize, string.len());
            Value::String(Rc::new(
                string
                    .clone()
                    .as_str()
                    .get(start..end)
                    .unwrap_or("")
                    .to_string(),
            ))
        }
        (Value::Interned(string), Value::Number(start), Value::Number(end)) => {
            let start = std::cmp::max(*start as usize, 0);
            let end = std::cmp::min(*end as usize, string.len());
            Value::String(Rc::new(string.get(start..end).unwrap_or("").to_string()))
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn list(x: &Value) -> Value {
    match x {
        Value::List(_) => x.clone(),
        Value::Tuple(t) => Value::List(Box::new(Vector::from_iter(t.iter().cloned()))),
        Value::Dict(d) => {
            let kvs = d.iter();
            let mut list = vector![];
            for (key, value) in kvs {
                let kv = Value::Tuple(Rc::new(vec![key.to_value(), value.clone()]));
                list.push_back(kv);
            }
            Value::List(Box::new(list))
        }
        _ => Value::List(Box::new(vector![x.clone()])),
    }
}

pub fn number(x: &Value) -> Value {
    match x {
        Value::Interned(string) => match string.parse::<f64>() {
            Ok(n) => Value::Tuple(Rc::new(vec![Value::Keyword("ok"), Value::Number(n)])),
            Err(_) => Value::Tuple(Rc::new(vec![
                Value::Keyword("err"),
                Value::String(Rc::new(format!("could not parse `{string}` as a number"))),
            ])),
        },
        Value::String(string) => match string.parse::<f64>() {
            Ok(n) => Value::Tuple(Rc::new(vec![Value::Keyword("ok"), Value::Number(n)])),
            Err(_) => Value::Tuple(Rc::new(vec![
                Value::Keyword("err"),
                Value::String(Rc::new(format!("could not parse `{string}` as a number"))),
            ])),
        },
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn r#type(x: &Value) -> Value {
    match x {
        Value::Nil => Value::Keyword("nil"),
        Value::Number(_) => Value::Keyword("number"),
        Value::True | Value::False => Value::Keyword("bool"),
        Value::Keyword(_) => Value::Keyword("keyword"),
        Value::Tuple(_) => Value::Keyword("tuple"),
        Value::Interned(_) => Value::Keyword("string"),
        Value::String(_) => Value::Keyword("string"),
        Value::List(_) => Value::Keyword("list"),
        Value::Dict(_) => Value::Keyword("dict"),
        Value::Fn(_) => Value::Keyword("fn"),
        Value::Box(_) => Value::Keyword("box"),
        Value::BaseFn(_) => Value::Keyword("fn"),
        Value::Partial(_) => Value::Keyword("fn"),
        Value::Process => Value::Keyword("process"),
        Value::Nothing => unreachable!(),
    }
}

pub fn split(source: &Value, splitter: &Value) -> Value {
    match (source, splitter) {
        (Value::String(source), Value::String(splitter)) => {
            println!("splitting {source} with {splitter}");
            let parts = source.split_terminator(splitter.as_str());
            let mut list = vector![];
            for part in parts {
                list.push_back(Value::String(Rc::new(part.to_string())));
            }
            Value::List(Box::new(list))
        }
        (Value::String(_), Value::Interned(splitter)) => {
            split(source, &Value::String(Rc::new(splitter.to_string())))
        }
        (Value::Interned(source), Value::String(_)) => {
            split(&Value::String(Rc::new(source.to_string())), splitter)
        }
        (Value::Interned(source), Value::Interned(splitter)) => {
            let source = Value::String(Rc::new(source.to_string()));
            let splitter = Value::String(Rc::new(splitter.to_string()));
            split(&source, &splitter)
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn upcase(string: &Value) -> Value {
    match string {
        Value::String(string) => Value::String(Rc::new(string.to_uppercase())),
        Value::Interned(string) => Value::String(Rc::new(string.to_uppercase())),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn downcase(string: &Value) -> Value {
    match string {
        Value::String(string) => Value::String(Rc::new(string.to_lowercase())),
        Value::Interned(string) => Value::String(Rc::new(string.to_lowercase())),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn trim(string: &Value) -> Value {
    match string {
        Value::String(string) => Value::String(Rc::new(string.trim().to_string())),
        Value::Interned(string) => Value::String(Rc::new(string.trim().to_string())),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn triml(string: &Value) -> Value {
    match string {
        Value::String(string) => Value::String(Rc::new(string.trim_start().to_string())),
        Value::Interned(string) => Value::String(Rc::new(string.trim_start().to_string())),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn trimr(string: &Value) -> Value {
    match string {
        Value::String(string) => Value::String(Rc::new(string.trim_end().to_string())),
        Value::Interned(string) => Value::String(Rc::new(string.trim_end().to_string())),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn atan_2(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => Value::Number(x.atan2(*y)),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn ceil(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.ceil()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn cos(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.cos()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn floor(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.floor()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn base_random() -> Value {
    Value::Number(random())
}

pub fn round(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.round()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn sin(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.sin()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn sqrt(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.sqrt()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn tan(x: &Value) -> Value {
    match x {
        Value::Number(x) => Value::Number(x.tan()),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn gt(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => {
            if x > y {
                Value::True
            } else {
                Value::False
            }
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn gte(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => {
            if x >= y {
                Value::True
            } else {
                Value::False
            }
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn lt(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => {
            if x < y {
                Value::True
            } else {
                Value::False
            }
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn lte(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => {
            if x <= y {
                Value::True
            } else {
                Value::False
            }
        }
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn r#mod(x: &Value, y: &Value) -> Value {
    match (x, y) {
        (Value::Number(x), Value::Number(y)) => Value::Number(x % y),
        _ => unreachable!("internal Ludus error"),
    }
}

pub fn make_base() -> Value {
    let members = vec![
        ("add", Value::BaseFn(Box::new(BaseFn::Binary("add", add)))),
        (
            "append",
            Value::BaseFn(Box::new(BaseFn::Binary("append", append))),
        ),
        (
            "assoc",
            Value::BaseFn(Box::new(BaseFn::Ternary("assoc", assoc))),
        ),
        ("at", Value::BaseFn(Box::new(BaseFn::Binary("at", at)))),
        (
            "atan_2",
            Value::BaseFn(Box::new(BaseFn::Binary("atan_2", atan_2))),
        ),
        (
            "bool",
            Value::BaseFn(Box::new(BaseFn::Unary("bool", r#bool))),
        ),
        ("ceil", Value::BaseFn(Box::new(BaseFn::Unary("ceil", ceil)))),
        (
            "chars",
            Value::BaseFn(Box::new(BaseFn::Unary("chars", chars))),
        ),
        (
            "concat",
            Value::BaseFn(Box::new(BaseFn::Binary("concat", concat))),
        ),
        ("cos", Value::BaseFn(Box::new(BaseFn::Unary("cos", cos)))),
        (
            "count",
            Value::BaseFn(Box::new(BaseFn::Unary("count", count))),
        ),
        ("dec", Value::BaseFn(Box::new(BaseFn::Unary("dec", dec)))),
        (
            "dissoc",
            Value::BaseFn(Box::new(BaseFn::Binary("dissoc", dissoc))),
        ),
        ("div", Value::BaseFn(Box::new(BaseFn::Binary("div", div)))),
        ("doc!", Value::BaseFn(Box::new(BaseFn::Unary("doc!", doc)))),
        (
            "downcase",
            Value::BaseFn(Box::new(BaseFn::Unary("downcase", downcase))),
        ),
        ("eq?", Value::BaseFn(Box::new(BaseFn::Binary("eq?", eq)))),
        (
            "first",
            Value::BaseFn(Box::new(BaseFn::Unary("first", first))),
        ),
        (
            "floor",
            Value::BaseFn(Box::new(BaseFn::Unary("floor", floor))),
        ),
        ("get", Value::BaseFn(Box::new(BaseFn::Binary("get", get)))),
        ("gt?", Value::BaseFn(Box::new(BaseFn::Binary("gt?", gt)))),
        ("gte?", Value::BaseFn(Box::new(BaseFn::Binary("gte?", gte)))),
        ("inc", Value::BaseFn(Box::new(BaseFn::Unary("inc", inc)))),
        ("last", Value::BaseFn(Box::new(BaseFn::Unary("last", last)))),
        ("list", Value::BaseFn(Box::new(BaseFn::Unary("list", list)))),
        ("lt?", Value::BaseFn(Box::new(BaseFn::Binary("lt?", lt)))),
        ("lte?", Value::BaseFn(Box::new(BaseFn::Binary("lte?", lte)))),
        ("mod", Value::BaseFn(Box::new(BaseFn::Binary("mod", r#mod)))),
        (
            "mult",
            Value::BaseFn(Box::new(BaseFn::Binary("mult", mult))),
        ),
        (
            "number",
            Value::BaseFn(Box::new(BaseFn::Unary("number", number))),
        ),
        ("pi", Value::Number(std::f64::consts::PI)),
        (
            "print!",
            Value::BaseFn(Box::new(BaseFn::Unary("print!", print))),
        ),
        ("process", Value::Process),
        (
            "random",
            Value::BaseFn(Box::new(BaseFn::Nullary("random", base_random))),
        ),
        (
            "range",
            Value::BaseFn(Box::new(BaseFn::Binary("range", range))),
        ),
        ("rest", Value::BaseFn(Box::new(BaseFn::Unary("rest", rest)))),
        (
            "round",
            Value::BaseFn(Box::new(BaseFn::Unary("round", round))),
        ),
        ("show", Value::BaseFn(Box::new(BaseFn::Unary("show", show)))),
        ("sin", Value::BaseFn(Box::new(BaseFn::Unary("sin", sin)))),
        (
            "slice",
            Value::BaseFn(Box::new(BaseFn::Ternary("slice", slice))),
        ),
        (
            "split",
            Value::BaseFn(Box::new(BaseFn::Binary("split", split))),
        ),
        ("sqrt", Value::BaseFn(Box::new(BaseFn::Unary("sqrt", sqrt)))),
        ("sqrt_2", Value::Number(std::f64::consts::SQRT_2)),
        (
            "store!",
            Value::BaseFn(Box::new(BaseFn::Binary("store!", store))),
        ),
        ("sub", Value::BaseFn(Box::new(BaseFn::Binary("sub", sub)))),
        ("tan", Value::BaseFn(Box::new(BaseFn::Unary("tan", tan)))),
        ("trim", Value::BaseFn(Box::new(BaseFn::Unary("trim", trim)))),
        (
            "triml",
            Value::BaseFn(Box::new(BaseFn::Unary("triml", triml))),
        ),
        (
            "trimr",
            Value::BaseFn(Box::new(BaseFn::Unary("trimr", trimr))),
        ),
        (
            "type",
            Value::BaseFn(Box::new(BaseFn::Unary("type", r#type))),
        ),
        (
            "unbox",
            Value::BaseFn(Box::new(BaseFn::Unary("unbox", unbox))),
        ),
        (
            "upcase",
            Value::BaseFn(Box::new(BaseFn::Unary("upcase", upcase))),
        ),
    ];
    let members = members
        .iter()
        .map(|(name, bfn)| (Key::Keyword(name), bfn.clone()))
        .collect::<Vec<_>>();
    Value::Dict(Box::new(HashMap::from(members)))
}
