///|
fn parse_json_value(text : String) -> Json raise DesignError {
  if text.length() > 1048576 {
    raise Budget("JSON text exceeds one million code units")
  }
  let mut depth = 0
  let mut in_string = false
  let mut escaped = false
  for c in text {
    if in_string {
      if escaped {
        escaped = false
      } else if c == '\\' {
        escaped = true
      } else if c == '"' {
        in_string = false
      }
    } else if c == '"' {
      in_string = true
    } else if c == '[' || c == '{' {
      depth += 1
      if depth > 12 {
        raise Budget("JSON nesting exceeds 12")
      }
    } else if c == ']' || c == '}' {
      depth -= 1
    }
  }
  @json.parse(text[:]) catch {
    _ => raise Invalid("invalid JSON")
  }
}

///|
fn fields_of(
  value : Json,
  names : Array[String],
) -> Map[String, Json] raise DesignError {
  match value {
    Object(fields) => {
      if fields.length() != names.length() ||
        !names.all(s => fields.contains(s)) {
        raise Invalid("JSON fields differ from versioned schema")
      }
      fields
    }
    _ => raise Invalid("expected JSON object")
  }
}

///|
fn required(
  fields : Map[String, Json],
  name : String,
) -> Json raise DesignError {
  match fields.get(name) {
    Some(v) => v
    None => raise Invalid("missing JSON field")
  }
}

///|
fn array_of(value : Json, limit : Int32) -> Array[Json] raise DesignError {
  match value {
    Array(values) => {
      if values.length() > limit {
        raise Budget("JSON array allowance")
      }
      values
    }
    _ => raise Invalid("expected JSON array")
  }
}

///|
fn string_of(value : Json) -> String raise DesignError {
  match value {
    String(s) => s
    _ => raise Invalid("expected JSON string")
  }
}

///|
fn attribute_array(value : Json, n : Int32) -> Array[Int32] raise DesignError {
  let out = []
  for v in array_of(value, 16) {
    match v {
      Number(x, ..) => {
        if x.is_nan() ||
          x.is_inf() ||
          x < 0.0 ||
          x >= n.to_double() ||
          x.to_int().to_double() != x {
          raise Invalid("JSON attribute must be an in-range integer")
        }
        out.push(x.to_int())
      }
      _ => raise Invalid("expected JSON attribute number")
    }
  }
  indices(out, n)
}

///|
fn ordered_attributes(
  value : Json,
  n : Int32,
) -> Array[Int32] raise DesignError {
  let attrs = attribute_array(value, n)
  let original = array_of(value, 16)
  if original.length() != attrs.length() {
    raise Invalid("duplicate table columns")
  }
  for i in 0..
        if x != attrs[i].to_double() {
          raise Invalid("table columns must be ordered")
        }
      _ => raise Invalid("table column must be numeric")
    }
  }
  attrs
}

///|
fn attrs_json(attrs : Array[Int32]) -> Json {
  Json::array(attrs.map(i => Json::number(i.to_double())))
}

///|
fn fd_json(fd : FD) -> Json {
  Json::object({ "left": attrs_json(fd.left), "right": attrs_json(fd.right) })
}

///|
pub fn Schema::to_json(self : Schema) -> Json {
  Json::object({
    "format": Json::string("moonnormal-schema/v1"),
    "attributes": Json::array(self.labels.map(s => Json::string(s))),
    "dependencies": Json::array(self.dependencies().map(fd => fd_json(fd))),
  })
}

///|
pub fn parse_schema_json(text : String) -> Schema raise DesignError {
  let fields = fields_of(parse_json_value(text), [
    "format", "attributes", "dependencies",
  ])
  if string_of(required(fields, "format")) != "moonnormal-schema/v1" {
    raise Invalid("unsupported schema format")
  }
  let labels = array_of(required(fields, "attributes"), 16).map(v => {
    string_of(v)
  })
  let dependencies = array_of(required(fields, "dependencies"), 256).map(v => {
    let f = fields_of(v, ["left", "right"])
    {
      left: attribute_array(required(f, "left"), labels.length()),
      right: attribute_array(required(f, "right"), labels.length()),
    }
  })
  schema(labels, dependencies)
}

///|
pub fn Table::to_json(self : Table) -> Json {
  Json::object({
    "format": Json::string("moonnormal-table/v1"),
    "attributes": Json::array(self.labels.map(s => Json::string(s))),
    "columns": attrs_json(self.columns),
    "rows": Json::array(
      self.data.map(row => Json::array(row.map(s => Json::string(s)))),
    ),
  })
}

///|
pub fn parse_table_json(
  owner : Schema,
  text : String,
  limits~ : Limits = default_limits(),
) -> Table raise DesignError {
  let fields = fields_of(parse_json_value(text), [
    "format", "attributes", "columns", "rows",
  ])
  if string_of(required(fields, "format")) != "moonnormal-table/v1" {
    raise Invalid("unsupported table format")
  }
  let labels = array_of(required(fields, "attributes"), 16).map(v => {
    string_of(v)
  })
  if labels != owner.attributes() {
    raise Invalid("JSON table schema mismatch")
  }
  let columns = ordered_attributes(required(fields, "columns"), labels.length())
  let rows = array_of(required(fields, "rows"), limits.max_rows).map(row => {
    array_of(row, 16).map(cell => string_of(cell))
  })
  table(owner, columns, rows, limits~)
}

///|
pub fn Plan::to_json(self : Plan, owner : Schema) -> Json raise DesignError {
  let relations = owner.fragments(self.relations, true)
  let target = match self.target {
    Second => "2NF"
    Third => "3NF"
    BoyceCodd => "BCNF"
  }
  Json::object({
    "format": Json::string("moonnormal-plan/v1"),
    "target": Json::string(target),
    "relations": Json::array(relations.map(r => attrs_json(r))),
    "added_key_relation": Json::boolean(self.added_key_relation),
    "splits": Json::array(
      self.splits.map(step => {
        Json::object({
          "input": attrs_json(step.input),
          "dependency": fd_json(step.dependency),
          "first": attrs_json(step.first),
          "second": attrs_json(step.second),
        })
      }),
    ),
  })
}

///|
pub fn parse_plan_json(owner : Schema, text : String) -> Plan raise DesignError {
  let fields = fields_of(parse_json_value(text), [
    "format", "target", "relations", "added_key_relation", "splits",
  ])
  if string_of(required(fields, "format")) != "moonnormal-plan/v1" {
    raise Invalid("unsupported plan format")
  }
  let target = match string_of(required(fields, "target")) {
    "2NF" => Second
    "3NF" => Third
    "BCNF" => BoyceCodd
    _ => raise Invalid("unknown normal form")
  }
  let n = owner.attribute_count()
  let relations = owner.fragments(
    array_of(required(fields, "relations"), 64).map(v => attribute_array(v, n)),
    true,
  )
  let added_key_relation = match required(fields, "added_key_relation") {
    True => true
    False => false
    _ => raise Invalid("key relation flag must be boolean")
  }
  let splits = array_of(required(fields, "splits"), 64).map(v => {
    let f = fields_of(v, ["input", "dependency", "first", "second"])
    let d = fields_of(required(f, "dependency"), ["left", "right"])
    {
      input: attribute_array(required(f, "input"), n),
      dependency: {
        left: attribute_array(required(d, "left"), n),
        right: attribute_array(required(d, "right"), n),
      },
      first: attribute_array(required(f, "first"), n),
      second: attribute_array(required(f, "second"), n),
    }
  })
  { target, relations, splits, added_key_relation, }
}