///|
/// Machine readable rendering of plans, audits and safe slots as JSON.
///
/// The emitter is a small purpose built writer instead of a generic
/// serialisation library, because the package must stay dependency free and the
/// output has to be byte for byte reproducible on every backend.
///
/// Layout rules, applied everywhere:
///
/// - two spaces of indentation per nesting level,
/// - one field or element per line, with a comma between siblings and none
///   after the last one,
/// - an empty array renders inline as `[]`,
/// - every document ends with exactly one newline.
///
/// Every builder takes the indentation depth of the value it produces, so a
/// nested object closes at the depth it opened at and a comma between siblings
/// can never be forgotten.

///|
/// Lower case hexadecimal digit of a value in `0-15`.
fn hex_digit(value : Int) -> String {
  let digits : Array[String] = [
    "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f",
  ]
  digits[value]
}

///|
/// Escape one character for the body of a JSON string.
fn escape_char(character : Char) -> String {
  match character {
    '"' => "\\\""
    '\\' => "\\\\"
    '\n' => "\\n"
    '\r' => "\\r"
    '\t' => "\\t"
    // 0x08 backspace
    '\u{0008}' => "\\b"
    // 0x0c form feed
    '\u{000C}' => "\\f"
    _ =>
      if character.to_int() < 0x20 {
        let code = character.to_int()
        let builder = StringBuilder()
        builder.write_string("\\u00")
        builder.write_string(hex_digit(code / 16))
        builder.write_string(hex_digit(code % 16))
        builder.to_string()
      } else {
        character.to_string()
      }
  }
}

///|
/// Escape a string for the body of a JSON string.
///
/// `"` becomes `\"`, `\` becomes `\\`, a newline `\n`, a carriage return `\r`, a
/// tab `\t`, a backspace `\b` and a form feed `\f`. Any other character below
/// `0x20` becomes `\u00XX` with two lower case hexadecimal digits. Everything
/// from `0x20` upwards passes through unchanged, so UTF-8 text such as `订单`
/// stays readable and is not turned into `\u` escapes.
pub fn json_escape(text : String) -> String {
  let builder = StringBuilder()
  for character in text {
    builder.write_string(escape_char(character))
  }
  builder.to_string()
}

///|
/// The indentation of one nesting level, as a string.
fn indent_text(depth : Int) -> String {
  let builder = StringBuilder()
  for index = 0; index < depth; index = index + 1 {
    builder.write_string("  ")
  }
  builder.to_string()
}

///|
/// A JSON string literal, with the body escaped.
fn json_text(value : String) -> String {
  "\"" + json_escape(value) + "\""
}

///|
/// One `"key": value` line at the given depth.
fn json_field(depth : Int, key : String, value : String) -> String {
  indent_text(depth) + json_text(key) + ": " + value
}

///|
/// An object from already rendered field lines.
///
/// `depth` is the depth of the braces; the field lines carry their own
/// indentation and are expected to sit one level deeper. An empty field list
/// renders inline as `{}`.
fn json_object(depth : Int, fields : Array[String]) -> String {
  if fields.length() == 0 {
    return "{}"
  }
  let builder = StringBuilder()
  builder.write_string("{\n")
  for index = 0; index < fields.length(); index = index + 1 {
    builder.write_string(fields[index])
    if index + 1 < fields.length() {
      builder.write_char(',')
    }
    builder.write_char('\n')
  }
  builder.write_string(indent_text(depth))
  builder.write_char('}')
  builder.to_string()
}

///|
/// An array from already rendered elements.
///
/// `depth` is the depth of the brackets; every element is placed on its own line
/// one level deeper. An empty array renders inline as `[]`.
fn json_array(depth : Int, elements : Array[String]) -> String {
  if elements.length() == 0 {
    return "[]"
  }
  let builder = StringBuilder()
  builder.write_string("[\n")
  for index = 0; index < elements.length(); index = index + 1 {
    builder.write_string(indent_text(depth + 1))
    builder.write_string(elements[index])
    if index + 1 < elements.length() {
      builder.write_char(',')
    }
    builder.write_char('\n')
  }
  builder.write_string(indent_text(depth))
  builder.write_char(']')
  builder.to_string()
}

///|
/// An array of strings, one per line.
fn json_text_array(depth : Int, values : Array[String]) -> String {
  let elements : Array[String] = []
  for value in values {
    elements.push(json_text(value))
  }
  json_array(depth, elements)
}

///|
/// One window object.
///
/// The field order is `id`, `service`, `kind`, `start`, `end`, `minutes`,
/// `risk`, `depends`. `start` and `end` are rendered with `format_instant`,
/// `minutes` is the raw length in minutes, `kind` and `risk` use their lower
/// case `label()` and `depends` keeps the input order of the dependencies. A
/// freeze carries no dependencies of its own, so its `depends` array is empty.
fn json_window_object(depth : Int, window : Window) -> String {
  json_object(depth, [
    json_field(depth + 1, "id", json_text(window.id)),
    json_field(depth + 1, "service", json_text(window.service)),
    json_field(depth + 1, "kind", json_text(window.kind.label())),
    json_field(depth + 1, "start", json_text(format_instant(window.start))),
    json_field(depth + 1, "end", json_text(format_instant(window.end))),
    json_field(depth + 1, "minutes", window.duration().to_string()),
    json_field(depth + 1, "risk", json_text(window.risk.label())),
    json_field(depth + 1, "depends", json_text_array(depth + 1, window.depends)),
  ])
}

///|
/// One diagnostic object.
fn json_diagnostic_object(depth : Int, diagnostic : Diagnostic) -> String {
  json_object(depth, [
    json_field(depth + 1, "severity", json_text(diagnostic.severity.label())),
    json_field(depth + 1, "line", diagnostic.line.to_string()),
    json_field(depth + 1, "code", json_text(diagnostic.code)),
    json_field(depth + 1, "detail", json_text(diagnostic.detail)),
  ])
}

///|
/// One overlap object.
fn json_overlap_object(depth : Int, overlap : Overlap) -> String {
  json_object(depth, [
    json_field(depth + 1, "first", json_text(overlap.first)),
    json_field(depth + 1, "second", json_text(overlap.second)),
    json_field(depth + 1, "minutes", overlap.minutes.to_string()),
    json_field(depth + 1, "impact", json_text(overlap.impact.label())),
  ])
}

///|
/// One freeze conflict object.
fn json_conflict_object(depth : Int, conflict : FreezeConflict) -> String {
  json_object(depth, [
    json_field(depth + 1, "window", json_text(conflict.window_id)),
    json_field(depth + 1, "freeze", json_text(conflict.freeze_id)),
    json_field(depth + 1, "minutes", conflict.minutes.to_string()),
  ])
}

///|
/// One dependency issue object.
fn json_issue_object(depth : Int, issue : DependencyIssue) -> String {
  json_object(depth, [
    json_field(depth + 1, "window", json_text(issue.window_id)),
    json_field(depth + 1, "dependency", json_text(issue.dependency)),
    json_field(depth + 1, "problem", json_text(issue.problem.label())),
    json_field(depth + 1, "severity", json_text(issue.severity.label())),
    json_field(depth + 1, "detail", json_text(issue.detail)),
  ])
}

///|
/// One safe slot object.
fn json_slot_object(depth : Int, slot : Slot) -> String {
  json_object(depth, [
    json_field(depth + 1, "start", json_text(format_instant(slot.start))),
    json_field(depth + 1, "end", json_text(format_instant(slot.end))),
    json_field(depth + 1, "minutes", slot.duration().to_string()),
  ])
}

///|
/// An array of window objects at the given depth.
fn json_window_array(depth : Int, windows : Array[Window]) -> String {
  let elements : Array[String] = []
  for window in windows {
    elements.push(json_window_object(depth + 1, window))
  }
  json_array(depth, elements)
}

///|
/// An array of diagnostic objects at the given depth.
fn json_diagnostic_array(
  depth : Int,
  diagnostics : Array[Diagnostic],
) -> String {
  let elements : Array[String] = []
  for diagnostic in diagnostics {
    elements.push(json_diagnostic_object(depth + 1, diagnostic))
  }
  json_array(depth, elements)
}

///|
/// Render a plan as JSON: an object with `windows` and `diagnostics`.
///
/// Windows appear in `sort_windows_by_start` order, which is stable for equal
/// start minutes, so the same plan always renders the same bytes. Diagnostics
/// keep their input order, because their line numbers are only meaningful in
/// that order.
pub fn render_plan_json(plan : Plan) -> String {
  json_object(0, [
    json_field(
      1,
      "windows",
      json_window_array(1, sort_windows_by_start(plan.windows)),
    ),
    json_field(1, "diagnostics", json_diagnostic_array(1, plan.diagnostics)),
  ]) +
  "\n"
}

///|
/// Render an audit as JSON.
///
/// The summary fields come first, then the collections. `changes` holds the
/// planned changes and `freezes` the change freezes, each sorted by start; the
/// remaining arrays keep the order the audit produced them in, so an overlap, a
/// conflict or an issue stays attributable to the same pair of records.
pub fn render_report_json(report : AuditReport) -> String {
  let overlap_elements : Array[String] = []
  for overlap in report.overlaps {
    overlap_elements.push(json_overlap_object(2, overlap))
  }
  let conflict_elements : Array[String] = []
  for conflict in report.freeze_conflicts {
    conflict_elements.push(json_conflict_object(2, conflict))
  }
  let issue_elements : Array[String] = []
  for issue in report.dependency_issues {
    issue_elements.push(json_issue_object(2, issue))
  }
  json_object(0, [
    json_field(1, "status", json_text(report.status())),
    json_field(
      1,
      "changes",
      json_window_array(1, sort_windows_by_start(report.plan.changes())),
    ),
    json_field(
      1,
      "freezes",
      json_window_array(1, sort_windows_by_start(report.plan.freezes())),
    ),
    json_field(1, "errors", report.error_count().to_string()),
    json_field(1, "warnings", report.warning_count().to_string()),
    json_field(1, "overlaps", json_array(1, overlap_elements)),
    json_field(1, "freeze_conflicts", json_array(1, conflict_elements)),
    json_field(1, "dependency_issues", json_array(1, issue_elements)),
    json_field(1, "blocked_minutes", report.blocked_minutes.to_string()),
    json_field(
      1,
      "diagnostics",
      json_diagnostic_array(1, report.plan.diagnostics),
    ),
  ]) +
  "\n"
}

///|
/// Render safe slots as a JSON array of `{start, end, minutes}` objects.
///
/// The input order is preserved: `safe_slots` already returns the intervals in
/// chronological order, and re-sorting here would hide a caller's own ordering.
pub fn render_slots_json(slots : Array[Slot]) -> String {
  let elements : Array[String] = []
  for slot in slots {
    elements.push(json_slot_object(1, slot))
  }
  json_array(0, elements) + "\n"
}