///|
pub fn arity(
  args : Array[Value],
  count : Int32,
  name : String,
) -> Unit raise SchemeError {
  if args.length() != count {
    raise Invalid(name + ": expected " + count.to_string() + " arguments")
  }
}

///|
fn Runtime::install_builtins(self : Runtime) -> Unit {
  for
    name in [
      "+", "-", "*", "/", "=", "<", ">", "<=", ">=", "quotient", "remainder", "not",
      "cons", "car", "cdr", "list", "null?", "pair?", "list?", "number?", "integer?",
      "boolean?", "string?", "symbol?", "procedure?", "equal?", "length", "append",
      "reverse", "map", "fold-left", "apply", "string-append", "string=?", "symbol->string",
      "string->symbol", "number->string", "string->number", "record", "record-ref",
      "record-set", "record-keys", "vector", "vector-ref", "vector-length", "unit",
      "error",
    ] {
    self.define_native(name, fn(runtime, args) raise {
      builtin(runtime, name, args)
    })
  }
}

///|
fn builtin(
  runtime : Runtime,
  name : String,
  args : Array[Value],
) -> Value raise {
  match name {
    "+" | "*" | "-" | "/" => {
      if args.is_empty() && (name == "-" || name == "/") {
        raise Invalid(name + " needs an operand")
      }
      let mut result = if name == "*" || name == "/" {
        Number::Integer(@bigint.BigInt::from_int(1))
      } else {
        Number::Integer(@bigint.BigInt::from_int(0))
      }
      let start = if (name == "-" || name == "/") && args.length() > 1 {
        result = number(args[0])
        1
      } else {
        0
      }
      for i = start; i < args.length(); i = i + 1 {
        result = arithmetic(name, result, number(args[i]))
      }
      Number(result)
    }
    "=" | "<" | ">" | "<=" | ">=" => {
      if args.length() < 2 {
        raise Invalid("Comparison needs at least two operands")
      }
      let mut result = true
      for i = 1; i < args.length(); i = i + 1 {
        let comparison = compare_numbers(number(args[i - 1]), number(args[i]))
        result = result &&
          (match name {
            "=" => comparison == 0
            "<" => comparison < 0
            ">" => comparison > 0
            "<=" => comparison <= 0
            _ => comparison >= 0
          })
      }
      Boolean(result)
    }
    "quotient" | "remainder" => {
      arity(args, 2, name)
      match (number(args[0]), number(args[1])) {
        (Integer(a), Integer(b)) => {
          if b.is_zero() {
            raise Invalid("Division by zero")
          }
          Number(Integer(if name == "quotient" { a / b } else { a % b }))
        }
        _ => raise Invalid("Expected exact integers")
      }
    }
    "not" => {
      arity(args, 1, name)
      Boolean(!truth(args[0]))
    }
    "cons" => {
      arity(args, 2, name)
      Pair(args[0], args[1])
    }
    "car" | "cdr" => {
      arity(args, 1, name)
      match args[0] {
        Pair(head, tail) => if name == "car" { head } else { tail }
        _ => raise Invalid("Expected pair")
      }
    }
    "list" => list(args)
    "null?"
    | "pair?"
    | "list?"
    | "number?"
    | "integer?"
    | "boolean?"
    | "string?"
    | "symbol?"
    | "procedure?" => {
      arity(args, 1, name)
      Boolean(
        match name {
          "null?" => args[0] is Nil
          "pair?" => args[0] is Pair(_, _)
          "list?" =>
            try {
              ignore(args[0].to_array())
              true
            } catch {
              _ => false
            }
          "number?" => args[0] is Number(_)
          "integer?" => args[0] is Number(Integer(_))
          "boolean?" => args[0] is Boolean(_)
          "string?" => args[0] is Text(_)
          "symbol?" => args[0] is Symbol(_)
          _ =>
            args[0] is Closure(_) ||
            args[0] is Native(_) ||
            args[0] is NativeFunction(_)
        },
      )
    }
    "equal?" => {
      arity(args, 2, name)
      Boolean(runtime.equal_values(args[0], args[1]))
    }
    "length" => {
      arity(args, 1, name)
      Number(Integer(@bigint.BigInt::from_int(args[0].to_array().length())))
    }
    "append" => {
      if args.is_empty() {
        return Nil
      }
      let mut result = args[args.length() - 1]
      for i = args.length() - 2; i >= 0; i = i - 1 {
        let items = args[i].to_array()
        for j = items.length() - 1; j >= 0; j = j - 1 {
          result = Pair(items[j], result)
        }
      }
      result
    }
    "reverse" => {
      arity(args, 1, name)
      let items = args[0].to_array()
      let mut result = Nil
      for item in items {
        result = Pair(item, result)
      }
      result
    }
    "map" => {
      arity(args, 2, name)
      list(args[1].to_array().map(item => runtime.call(args[0], [item])))
    }
    "fold-left" => {
      arity(args, 3, name)
      let mut result = args[1]
      for item in args[2].to_array() {
        result = runtime.call(args[0], [result, item])
      }
      result
    }
    "apply" => {
      if args.length() < 2 {
        raise Invalid("apply needs a procedure and list")
      }
      let actual = args[1:args.length() - 1].to_owned()
      actual.append(args[args.length() - 1].to_array())
      runtime.call(args[0], actual)
    }
    "string-append" => Text(args.map(Value::as_text).join(""))
    "string=?" => {
      arity(args, 2, name)
      Boolean(args[0].as_text() == args[1].as_text())
    }
    "symbol->string" => {
      arity(args, 1, name)
      match args[0] {
        Symbol(s) => Text(s)
        _ => raise Invalid("Expected symbol")
      }
    }
    "string->symbol" => {
      arity(args, 1, name)
      Symbol(args[0].as_text())
    }
    "number->string" => {
      arity(args, 1, name)
      Text(number(args[0]).to_text())
    }
    "string->number" => {
      arity(args, 1, name)
      match parse_number(args[0].as_text()) {
        Some(n) => Number(n)
        None => Boolean(false)
      }
    }
    "record" => {
      let fields : Map[String, Value] = Map([])
      for arg in args {
        match arg {
          Pair(Text(key), value) => {
            if fields.contains(key) {
              raise Invalid("Duplicate record field: " + key)
            }
            fields[key] = value
          }
          _ => raise Invalid("record expects pairs with string keys")
        }
      }
      Record(fields)
    }
    "record-ref" => {
      arity(args, 2, name)
      match args[0] {
        Record(fields) =>
          match fields.get(args[1].as_text()) {
            Some(value) => value
            None => raise Invalid("Missing record field")
          }
        _ => raise Invalid("Expected record")
      }
    }
    "record-set" => {
      arity(args, 3, name)
      match args[0] {
        Record(fields) => {
          let copy = Map::from_array(fields.iter().collect())
          copy[args[1].as_text()] = args[2]
          Record(copy)
        }
        _ => raise Invalid("Expected record")
      }
    }
    "record-keys" => {
      arity(args, 1, name)
      match args[0] {
        Record(fields) =>
          list(fields.iter().collect().map(pair => Text(pair.0)))
        _ => raise Invalid("Expected record")
      }
    }
    "vector" => Vector(args)
    "vector-length" => {
      arity(args, 1, name)
      match args[0] {
        Vector(items) =>
          Number(Integer(@bigint.BigInt::from_int(items.length())))
        _ => raise Invalid("Expected vector")
      }
    }
    "vector-ref" => {
      arity(args, 2, name)
      match (args[0], args[1]) {
        (Vector(items), Number(Integer(index))) => {
          if index < @bigint.BigInt::from_int(0) ||
            index >= @bigint.BigInt::from_int(items.length()) {
            raise Invalid("Vector index out of range")
          }
          items[index.to_int()]
        }
        _ => raise Invalid("Expected vector and integer")
      }
    }
    "unit" => {
      arity(args, 0, name)
      Unit
    }
    "error" => raise Invalid(error_arguments_preview(args))
    _ => raise Unbound(name)
  }
}

///|
pub fn Runtime::equal_values(
  self : Runtime,
  left : Value,
  right : Value,
) -> Bool raise {
  let pending = [(left, right)]
  while pending.pop() is Some((left, right)) {
    self.tick()
    match (left, right) {
      (Number(a), Number(b)) => if compare_numbers(a, b) != 0 { return false }
      (Boolean(a), Boolean(b)) => if a != b { return false }
      (Text(a), Text(b)) | (Symbol(a), Symbol(b)) => if a != b { return false }
      (Character(a), Character(b)) => if a != b { return false }
      (Nil, Nil) | (Unit, Unit) => ()
      (Pair(a, b), Pair(c, d)) => {
        pending.push((a, c))
        pending.push((b, d))
      }
      (Vector(a), Vector(b)) | (Tagged(_, a), Tagged(_, b)) => {
        if left is Tagged(a, _) && right is Tagged(b, _) && a != b {
          return false
        }
        if a.length() != b.length() {
          return false
        }
        for i = 0; i < a.length(); i = i + 1 {
          pending.push((a[i], b[i]))
        }
      }
      (Record(a), Record(b)) => {
        if a.length() != b.length() {
          return false
        }
        for key, value in a {
          match b.get(key) {
            Some(other) => pending.push((value, other))
            None => return false
          }
        }
      }
      (IR(a), IR(b)) => if a != b { return false }
      (IRType(a), IRType(b)) => if a != b { return false }
      (IRFile(a), IRFile(b)) => if a != b { return false }
      _ => return false
    }
  }
  true
}

///|
pub fn value_to_string(value : Value) -> String raise SchemeError {
  render(value, 0)
}

///|
fn render(value : Value, depth : Int32) -> String raise SchemeError {
  if depth > 256 {
    raise Limit("value printing depth")
  }
  match value {
    Number(n) => n.to_text()
    Boolean(b) => if b { "#t" } else { "#f" }
    Character(c) => Datum::Character(c).to_source()
    Text(s) => Datum::Text(s).to_source()
    Symbol(s) => symbol_text(s)
    Nil => "()"
    Pair(_, _) => {
      let items : Array[String] = []
      let mut rest = value
      while rest is Pair(head, tail) {
        items.push(render(head, depth + 1))
        rest = tail
      }
      "(" +
      items.join(" ") +
      (match rest {
        Nil => ""
        _ => " . " + render(rest, depth + 1)
      }) +
      ")"
    }
    Vector(items) =>
      "#(" + items.map(item => render(item, depth + 1)).join(" ") + ")"
    Record(fields) =>
      "# pair.0 + "=" + render(pair.1, depth + 1))
      .join(" ") +
      ">"
    Tagged(tag, items) =>
      "#<" +
      tag +
      " " +
      items.map(item => render(item, depth + 1)).join(" ") +
      ">"
    Unit => "#"
    Closure(_) | Native(_) | NativeFunction(_) => "#"
    IR(_) => "#"
    IRType(_) => "#"
    IRFile(_) => "#"
  }
}