///|
/// Interpreter failures have a stable category and diagnostic message.
pub suberror TemplateError {
  Invalid(String)
  Limit(String)
} derive(Debug)

///|
pub extend TemplateError with @moonbitlang/core/debug.Debug::{to_repr}

///|
priv enum Value {
  Nil
  Boolean(Bool)
  Numeric(Double)
  Text(String)
  Sequence(Array[Value])
  Record(Map[String, Value])
  Function(String)
  Omit
} derive(Eq, Debug)

///|
priv struct Budget {
  mut remaining : Int32
  max_depth : Int32
  max_items : Int32
}

///|
fn Budget::tick(self : Budget, depth : Int32) -> Unit raise TemplateError {
  if depth > self.max_depth {
    raise Limit("maximum nesting depth")
  }
  self.remaining -= 1
  if self.remaining < 0 {
    raise Limit("evaluation step budget exhausted")
  }
}

///|
fn Budget::items(self : Budget, size : Int32) -> Unit raise TemplateError {
  if size > self.max_items {
    raise Limit("collection size limit")
  }
}

///|
fn from_json(j : Json, b : Budget, depth : Int32) -> Value raise TemplateError {
  b.tick(depth)
  match j {
    Null => Nil
    True => Boolean(true)
    False => Boolean(false)
    Number(n, ..) => Numeric(n)
    String(s) => {
      b.items(s.length())
      Text(s)
    }
    Array(a) => {
      b.items(a.length())
      Sequence(a.map(x => from_json(x, b, depth + 1)))
    }
    Object(o) => {
      b.items(o.length())
      let out = Map([])
      for k, v in o {
        out[k] = from_json(v, b, depth + 1)
      }
      Record(out)
    }
  }
}

///|
fn to_json(v : Value, b : Budget, depth : Int32) -> Json raise TemplateError {
  b.tick(depth)
  match v {
    Nil | Omit => Json::null()
    Boolean(x) => Json::boolean(x)
    Numeric(n) => {
      if n.is_nan() || n.is_inf() {
        raise Invalid("non-finite result")
      }
      Json::number(n)
    }
    Text(s) => {
      b.items(s.length())
      Json::string(s)
    }
    Sequence(a) => {
      b.items(a.length())
      Json::array(a.filter(x => x != Omit).map(x => to_json(x, b, depth + 1)))
    }
    Record(o) => {
      let out = Map([])
      b.items(o.length())
      for k, v in o {
        if v != Omit {
          out[k] = to_json(v, b, depth + 1)
        }
      }
      Json::object(out)
    }
    Function(_) => raise Invalid("uncalled function in output")
  }
}

///|
fn truth(v : Value) -> Bool {
  match v {
    Nil | Omit => false
    Boolean(b) => b
    Numeric(n) => n != 0 && !n.is_nan()
    Text(s) => s != ""
    Sequence(a) => !a.is_empty()
    Record(o) => !o.is_empty()
    Function(_) => true
  }
}

///|
fn text(v : Value) -> String raise TemplateError {
  match v {
    Nil => "null"
    Boolean(b) => if b { "true" } else { "false" }
    Text(s) => s
    Numeric(n) =>
      if n == 0 {
        "0"
      } else if n >= -2147483648.0 &&
        n <= 2147483647.0 &&
        n == n.to_int().to_double() {
        n.to_int().to_string()
      } else {
        n.to_string()
      }
    _ => raise Invalid("expected scalar")
  }
}

///|
fn numeric(v : Value) -> Double raise TemplateError {
  match v {
    Numeric(n) => n
    _ => raise Invalid("expected number")
  }
}

///|
fn string_value(v : Value) -> String raise TemplateError {
  match v {
    Text(s) => s
    _ => raise Invalid("expected string")
  }
}

///|
fn identifier(s : String) -> Bool {
  if s.is_empty() {
    return false
  }
  for i = 0; i < s.length(); i = i + 1 {
    let c = s.code_unit_at(i).to_int()
    if !((c >= 65 && c <= 90) ||
      (c >= 97 && c <= 122) ||
      c == 95 ||
      (i > 0 && c >= 48 && c <= 57)) {
      return false
    }
  }
  true
}