///|
priv struct ModelDocument {
  format : String
  version : Int32
  alphabet : Array[String]
  initial : Int32
  transitions : Array[Array[Transition]]
} derive(@json.FromJson)

///|
pub impl ToJson for Machine with fn to_json(self) {
  Json::object({
    "format": Json("moonstatelens.mealy"),
    "version": Json(1),
    "alphabet": Json(self.alphabet),
    "initial": Json(self.initial),
    "transitions": Json(self.transitions),
  })
}

///|
pub extend Machine with ToJson::{to_json}

///|
/// Decode a versioned complete model, then apply all constructor invariants.
pub fn Machine::from_json(value : Json) -> Machine raise {
  let document : ModelDocument = @json.from_json(value)
  if document.format != "moonstatelens.mealy" || document.version != 1 {
    raise InvalidArgument("unsupported model format or version")
  }
  Machine::new(
    document.alphabet,
    document.transitions,
    initial=document.initial,
  )
}

///|
/// Parse a bounded JSON document (limit counts UTF-16 code units, not bytes).
pub fn decode_model(
  text : String,
  max_code_units~ : Int32 = 16777216,
) -> Machine raise {
  if max_code_units < 1 {
    raise InvalidArgument("JSON text limit must be positive")
  }
  if text.length() > max_code_units {
    raise LimitExceeded("model-json-length")
  }
  Machine::from_json(@json.parse(text))
}

///|
/// Encode a model in the versioned interchange format.
pub fn Machine::encode(self : Machine) -> String {
  Json(self).stringify(indent=2)
}

///|
pub impl ToJson for LearningResult with fn to_json(self) {
  Json::object({
    "format": Json("moonstatelens.learning"),
    "version": Json(1),
    "model": self.model.map(model => Json(model)).unwrap_or(Json::null()),
    "status": Json(self.status),
    "statistics": Json(self.statistics),
    "rounds": Json(self.rounds),
    "counterexamples": Json(self.counterexamples),
  })
}

///|
pub extend LearningResult with ToJson::{to_json}

///|
/// DOT uses quoted JSON string escapes for textual labels, so quotes,
/// backslashes and line breaks cannot escape into graph statements.
pub fn Machine::to_dot(self : Machine) -> String {
  let lines = [
    "digraph Mealy {",
    "  rankdir=LR;",
    "  start [shape=point];",
    "  start -> s" + self.initial.to_string() + ";",
  ]
  for state, row in self.transitions {
    lines.push(
      "  s" +
      state.to_string() +
      " [label=" +
      Json("q" + state.to_string()).stringify() +
      "];",
    )
    for column, edge in row {
      let label = self.alphabet[column] + " / " + edge.output
      lines.push(
        "  s" +
        state.to_string() +
        " -> s" +
        edge.target.to_string() +
        " [label=" +
        Json(label).stringify() +
        "];",
      )
    }
  }
  lines.push("}")
  lines.join("\n") + "\n"
}