///|
fn program_variable_indices(
  names : Map[String, Int32],
  value : Json,
) -> Array[Int32] raise GraphError {
  let result = []
  for value in items(value, max_variables) {
    let name = text(value)
    match names.get(name) {
      Some(index) => result.push(index)
      None => raise Invalid("unknown variable: " + name)
    }
  }
  result
}

///|
/// Parse cairndom.program.v1 with a complete nested cairndom.graph.v1 graph.
/// Each listed block requires node, defines and uses. Omitted nodes have empty
/// facts; duplicate blocks, facts, IDs and unknown fields are rejected. Uses
/// retain the FlowProgram convention of occurring before local definitions.
pub fn parse_program(input : String) -> FlowProgram raise GraphError {
  if input.length() > 2_097_152 {
    raise Invalid("program JSON exceeds 2 MiB")
  }
  let value = @json.parse(
    input,
    max_nesting_depth=12,
    reject_duplicate_keys=true,
  ) catch {
    _ => raise Invalid("invalid JSON, repeated key or excessive nesting")
  }
  let object = fields(value, ["schema", "graph", "variables", "blocks"])
  if text(required(object, "schema")) != "cairndom.program.v1" {
    raise Invalid("schema must be cairndom.program.v1")
  }
  let graph = parse_graph(required(object, "graph").stringify())
  let variables = []
  let variable_names : Map[String, Int32] = Map([])
  for value in items(required(object, "variables"), max_variables) {
    let name = text(value)
    if !valid_id(name) || variable_names.contains(name) {
      raise Invalid("invalid or duplicate variable ID: " + name)
    }
    variable_names[name] = variables.length()
    variables.push(name)
  }
  let node_names : Map[String, Int32] = Map([])
  for node, name in graph.nodes {
    node_names[name] = node
  }
  let n = graph.nodes.length()
  let definitions : Array[Array[Int32]] = Array::makei(n, _ => [])
  let uses : Array[Array[Int32]] = Array::makei(n, _ => [])
  let listed = Array::make(n, false)
  let mut associations = 0
  for value in items(required(object, "blocks"), n) {
    let block = fields(value, ["node", "defines", "uses"])
    let node = node_index(node_names, required(block, "node"))
    if listed[node] {
      raise Invalid("duplicate block node: " + graph.nodes[node])
    }
    listed[node] = true
    let defined = program_variable_indices(
      variable_names,
      required(block, "defines"),
    )
    let used = program_variable_indices(variable_names, required(block, "uses"))
    let count = defined.length() + used.length()
    if count > max_program_associations - associations {
      raise Invalid(
        "program limit is at most 16384 definition/use associations",
      )
    }
    associations += count
    definitions[node] = defined
    uses[node] = used
  }
  let program = { graph, variables, definitions, uses, }
  validate_program(program)
  program
}

///|
/// Serialize a validated FlowProgram as cairndom.program.v1. Every graph node
/// gets a block in node order, and facts follow declared variable order. Sorting
/// uses private copies and never mutates the caller's rows. Exports exceeding
/// the parser's 2 MiB input limit are rejected so successful exports round trip.
pub fn program_json_text(program : FlowProgram) -> String raise GraphError {
  validate_program(program)
  let blocks = []
  for node, name in program.graph.nodes {
    let defined = program.definitions[node].copy()
    let used = program.uses[node].copy()
    defined.sort()
    used.sort()
    blocks.push(
      Json::object(
        Map([
          ("node", Json::string(name)),
          (
            "defines",
            Json::array(
              defined.map(variable => Json::string(program.variables[variable])),
            ),
          ),
          (
            "uses",
            Json::array(
              used.map(variable => Json::string(program.variables[variable])),
            ),
          ),
        ]),
      ),
    )
  }
  let output = Json::object(
    Map([
      ("schema", Json::string("cairndom.program.v1")),
      ("graph", graph_json_value(program.graph)),
      (
        "variables",
        Json::array(program.variables.map(name => Json::string(name))),
      ),
      ("blocks", Json::array(blocks)),
    ]),
  ).stringify(indent=2)
  if output.length() > 2_097_152 {
    raise Invalid("serialized program JSON exceeds 2 MiB")
  }
  output
}