///|
fn required_field(
  object : Map[String, Json],
  name : String,
) -> Json raise TopologyError {
  match object.get(name) {
    Some(value) => value
    None => raise TopologyError("missing field: " + name)
  }
}

///|
fn number_field(
  object : Map[String, Json],
  name : String,
) -> Double raise TopologyError {
  @json.from_json(required_field(object, name)) catch {
    _ => raise TopologyError(name + " must be a number")
  }
}

///|
fn integer_field(
  object : Map[String, Json],
  name : String,
  fallback : Int,
) -> Int raise TopologyError {
  match object.get(name) {
    None => fallback
    Some(Number(n, ..)) => {
      if !finite(n) ||
        n != @math.floor(n) ||
        n < -2147483648.0 ||
        n > 2147483647.0 {
        raise TopologyError(name + " must be a 32-bit integer")
      }
      n.to_int()
    }
    Some(_) => raise TopologyError(name + " must be an integer")
  }
}

///|
fn rows_field(
  object : Map[String, Json],
  name : String,
) -> Array[Array[Double]] raise TopologyError {
  @json.from_json(required_field(object, name)) catch {
    _ => raise TopologyError(name + " must be an array of number arrays")
  }
}

///|
/// Parse a bounded JSON request, construct a filtration, then analyze it.
/// Supported kinds: points, matrix, grid, series. Unknown fields are errors.
pub fn analyze_json(text : String) -> Analysis raise TopologyError {
  if text.length() > 1000000 {
    raise TopologyError("JSON request exceeds one million characters")
  }
  let value = @json.parse(text) catch {
    _ => raise TopologyError("invalid JSON request")
  }
  let object = match value {
    Object(fields) => fields
    _ => raise TopologyError("request must be a JSON object")
  }
  let kind : String = @json.from_json(required_field(object, "kind")) catch {
    _ => raise TopologyError("kind must be a string")
  }
  let threshold = number_field(object, "threshold")
  let budget = integer_field(object, "max_cells", 4000)
  let allowed = match kind {
    "points" => ["kind", "threshold", "max_cells", "points"]
    "matrix" => ["kind", "threshold", "max_cells", "matrix"]
    "grid" => ["kind", "threshold", "max_cells", "grid"]
    "series" =>
      ["kind", "threshold", "max_cells", "series", "dimension", "lag", "stride"]
    _ =>
      raise TopologyError(
        "unknown kind; expected points, matrix, grid or series",
      )
  }
  for field in object.keys() {
    if !allowed.contains(field) {
      raise TopologyError("unknown field for " + kind + ": " + field)
    }
  }
  let filtration = match kind {
    "points" => rips(rows_field(object, "points"), threshold~, max_cells=budget)
    "matrix" =>
      rips_matrix(rows_field(object, "matrix"), threshold~, max_cells=budget)
    "grid" =>
      cubical_grid(rows_field(object, "grid"), threshold~, max_cells=budget)
    "series" => {
      let series : Array[Double] = @json.from_json(
        required_field(object, "series"),
      ) catch {
        _ => raise TopologyError("series must be an array of numbers")
      }
      let points = delay_embed(
        series,
        dimension=integer_field(object, "dimension", 2),
        lag=integer_field(object, "lag", 1),
        stride=integer_field(object, "stride", 1),
      )
      rips(points, threshold~, max_cells=budget)
    }
    _ => raise TopologyError("unreachable kind")
  }
  analyze(filtration)
}

///|
/// Portable report schema. null death means alive at the input cutoff.
/// Representative cell indices refer to the accompanying cells array.
pub fn report_json(analysis : Analysis) -> Json {
  let intervals : Array[Json] = []
  for interval in analysis.intervals {
    let death : Json = match interval.death {
      None => Json::null()
      Some(value) => value.to_json()
    }
    let death_cell : Json = match interval.death_cell {
      None => Json::null()
      Some(value) => value.to_json()
    }
    intervals.push({
      "dimension": interval.dimension,
      "birth": interval.birth,
      "death": death,
      "birth_cell": interval.birth_cell,
      "death_cell": death_cell,
      "representative": interval.representative,
    })
  }
  let cells : Array[Json] = []
  for cell in analysis.filtration.cells {
    cells.push({
      "key": cell.key,
      "dimension": cell.dimension,
      "value": cell.value,
      "boundary": cell.boundary,
      "vertices": cell.vertices,
    })
  }
  {
    "schema_version": 1,
    "project": "MoonTopoLens",
    "coefficients": "GF(2)",
    "kind": analysis.filtration.kind,
    "cutoff": analysis.filtration.cutoff,
    "vertex_count": analysis.filtration.vertex_count,
    "cell_count": cells.length(),
    "column_additions": analysis.column_additions,
    "positions": analysis.filtration.positions,
    "position_meaning": "display projection: first two point coordinates; grid x and negative row; matrix has none",
    "summary": summary_json(analysis),
    "betti_events": betti_events_json(analysis),
    "essential_meaning": "alive at input cutoff; not proof of infinite lifetime",
    "representative_meaning": "cycle at birth; not guaranteed shortest",
    "intervals": intervals,
    "cells": cells,
  }
}