///|
fn encode_condition(c : Condition) -> Json {
  match c {
    Equals(a, b) =>
      Json::array([Json("equals"), encode_expr(a), encode_expr(b)])
    Empty(e) => Json::array([Json("empty"), encode_expr(e)])
    Matches(e, p, full) =>
      Json::array([Json("matches"), encode_expr(e), Json(p), Json(full)])
    Not(c) => Json::array([Json("not"), encode_condition(c)])
    All(cs) => Json::array([Json("all"), Json::array(cs.map(encode_condition))])
    Any(cs) => Json::array([Json("any"), Json::array(cs.map(encode_condition))])
  }
}

///|
fn decode_condition(
  v : Json,
  depth : Int32,
  budget : NodeBudget,
) -> Condition raise LoomError {
  budget.used = budget.used + 1
  if depth > 16 || budget.used > 256 {
    raise LoomError("condition exceeds depth/node limits")
  }
  let a = j_array(v)
  if a.length() < 2 {
    raise LoomError("condition requires an operator and arguments")
  }
  let op = j_text(a[0])
  let arity = if op == "matches" { 4 } else if op == "equals" { 3 } else { 2 }
  if a.length() != arity {
    raise LoomError("wrong condition arity")
  }
  match op {
    "equals" =>
      Equals(
        decode_expr(a[1], depth + 1, budget),
        decode_expr(a[2], depth + 1, budget),
      )
    "empty" => Empty(decode_expr(a[1], depth + 1, budget))
    "matches" => {
      let p = j_text(a[2])
      let full = match a[3] {
        True => true
        False => false
        _ => raise LoomError("full must be boolean")
      }
      ignore(regex_test("", p, full))
      Matches(decode_expr(a[1], depth + 1, budget), p, full)
    }
    "not" => Not(decode_condition(a[1], depth + 1, budget))
    "all" | "any" => {
      let values = j_array(a[1])
      if values.length() > 16 {
        raise LoomError("boolean condition exceeds 16 children")
      }
      let cs = values.map(v => decode_condition(v, depth + 1, budget))
      if op == "all" {
        All(cs)
      } else {
        Any(cs)
      }
    }
    _ => raise LoomError("unknown condition")
  }
}

///|
fn check_condition(
  c : Condition,
  depth : Int32,
  budget : NodeBudget,
) -> Unit raise LoomError {
  budget.used = budget.used + 1
  if depth > 16 || budget.used > 256 {
    raise LoomError("condition exceeds depth/node limits")
  }
  match c {
    Equals(a, b) => {
      check_tree(a, depth + 1, budget)
      check_tree(b, depth + 1, budget)
    }
    Empty(e) => check_tree(e, depth + 1, budget)
    Matches(e, p, full) => {
      ignore(regex_test("", p, full))
      check_tree(e, depth + 1, budget)
    }
    Not(c) => check_condition(c, depth + 1, budget)
    All(cs) | Any(cs) => {
      if cs.length() > 16 {
        raise LoomError("boolean condition exceeds 16 children")
      }
      for c in cs {
        check_condition(c, depth + 1, budget)
      }
    }
  }
}

///|
fn condition_fields(c : Condition, width : Int32) -> Unit raise LoomError {
  match c {
    Equals(a, b) => {
      check_fields(a, width)
      check_fields(b, width)
    }
    Empty(e) | Matches(e, _, _) => check_fields(e, width)
    Not(c) => condition_fields(c, width)
    All(cs) | Any(cs) =>
      for c in cs {
        condition_fields(c, width)
      }
  }
}

///|
pub fn condition_from_json(v : Json) -> Condition raise LoomError {
  decode_condition(v, 0, { used: 0, })
}

///|
pub fn condition_to_json(c : Condition) -> Json raise LoomError {
  check_condition(c, 0, { used: 0, })
  let v = encode_condition(c)
  ignore(condition_from_json(v))
  v
}