///|
/// Counts overlays by their concrete rendering kind.
pub(all) struct OverlayKindCounts {
  bbox : Int
  mask : Int
  keypoints : Int
  trajectory : Int
  heatmap : Int
  error_region : Int
} derive(Debug, ToJson)

///|
/// Counts diagnostics produced by document validation.
pub(all) struct DiagnosticSeverityCounts {
  info : Int
  warning : Int
  error : Int
} derive(Debug, ToJson)

///|
/// A stable summary of a source layer in a document manifest.
pub(all) struct LayerManifest {
  id : String
  visible : Bool
  opacity : Double
  overlay_count : Int
  overlays : OverlayKindCounts
} derive(Debug, ToJson)

///|
/// A machine-readable, non-mutating summary of a `DebugDocument`.
pub(all) struct DocumentManifest {
  title : String
  image_width : Int
  image_height : Int
  layer_count : Int
  visible_layer_count : Int
  total_overlay_count : Int
  visible_overlay_count : Int
  overlays : OverlayKindCounts
  visible_overlays : OverlayKindCounts
  content_bounds : Rect?
  diagnostics : DiagnosticSeverityCounts
  is_valid : Bool
  layers : Array[LayerManifest]
} derive(Debug, ToJson)

///|
/// Builds a report manifest without changing the document, its layers, or
/// their overlay arrays. Layer entries preserve the document insertion order.
pub fn DebugDocument::manifest(self : DebugDocument) -> DocumentManifest {
  let layers : Array[LayerManifest] = []
  let mut visible_layer_count = 0
  let mut total_overlay_count = 0
  let mut visible_overlay_count = 0
  let mut overlays = empty_overlay_kind_counts()
  let mut visible_overlays = empty_overlay_kind_counts()
  for layer in self.layers {
    let layer_counts = overlay_kind_counts(layer.overlays)
    let overlay_count = layer.overlays.length()
    layers.push({
      id: layer.id,
      visible: layer.visible,
      opacity: layer.opacity,
      overlay_count,
      overlays: layer_counts,
    })
    total_overlay_count += overlay_count
    overlays = add_overlay_kind_counts(overlays, layer_counts)
    if layer.visible {
      visible_layer_count += 1
      visible_overlay_count += overlay_count
      visible_overlays = add_overlay_kind_counts(visible_overlays, layer_counts)
    }
  }
  let diagnostics = diagnostic_severity_counts(self.validate())
  {
    title: self.title,
    image_width: self.image.width,
    image_height: self.image.height,
    layer_count: self.layers.length(),
    visible_layer_count,
    total_overlay_count,
    visible_overlay_count,
    overlays,
    visible_overlays,
    content_bounds: self.content_bounds(),
    diagnostics,
    is_valid: diagnostics.error == 0,
    layers,
  }
}

///|
/// Serializes this manifest using the core `Json` encoder with a stable field
/// order. String values are encoded by `Json::string`, so user input is escaped.
pub fn DocumentManifest::to_json_text(self : DocumentManifest) -> String {
  manifest_to_json(self).stringify()
}

///|
/// Renders a compact CI summary. User-controlled titles and layer IDs are
/// escaped before insertion into Markdown.
pub fn DocumentManifest::to_markdown(self : DocumentManifest) -> String {
  let out = StringBuilder()
  out.write_string("# \{escape_markdown(self.title)}\n\n")
  out.write_string("- Image: \{self.image_width} × \{self.image_height}\n")
  out.write_string(
    "- Layers: \{self.layer_count} total, \{self.visible_layer_count} visible\n",
  )
  out.write_string(
    "- Overlays: \{self.total_overlay_count} total, \{self.visible_overlay_count} visible\n",
  )
  out.write_string(
    "- Validation: \{self.diagnostics.error} errors, \{self.diagnostics.warning} warnings, \{self.diagnostics.info} info\n\n",
  )
  out.write_string("## Layers\n\n")
  out.write_string("| Layer | Visible | Opacity | Overlays |\n")
  out.write_string("| --- | --- | ---: | ---: |\n")
  for layer in self.layers {
    let visible = if layer.visible { "yes" } else { "no" }
    out.write_string(
      "| \{escape_markdown(layer.id)} | \{visible} | \{layer.opacity} | \{layer.overlay_count} |\n",
    )
  }
  out.write_string("\n## Overlay kinds\n\n")
  out.write_string(
    "| BBox | Mask | Keypoints | Trajectory | Heatmap | Error regions |\n",
  )
  out.write_string("| ---: | ---: | ---: | ---: | ---: | ---: |\n")
  out.write_string(
    "| \{self.overlays.bbox} | \{self.overlays.mask} | \{self.overlays.keypoints} | \{self.overlays.trajectory} | \{self.overlays.heatmap} | \{self.overlays.error_region} |\n\n",
  )
  out.write_string("## Content bounds\n\n")
  match self.content_bounds {
    None => out.write_string("None\n")
    Some(bounds) =>
      out.write_string(
        "x=\{bounds.x}, y=\{bounds.y}, width=\{bounds.width}, height=\{bounds.height}\n",
      )
  }
  out.to_string()
}

///|
fn empty_overlay_kind_counts() -> OverlayKindCounts {
  { bbox: 0, mask: 0, keypoints: 0, trajectory: 0, heatmap: 0, error_region: 0 }
}

///|
fn overlay_kind_counts(overlays : Array[Overlay]) -> OverlayKindCounts {
  let mut bbox = 0
  let mut mask = 0
  let mut keypoints = 0
  let mut trajectory = 0
  let mut heatmap = 0
  let mut error_region = 0
  for overlay in overlays {
    match overlay {
      BBox(..) => bbox += 1
      Mask(..) => mask += 1
      Keypoints(..) => keypoints += 1
      Trajectory(..) => trajectory += 1
      Heatmap(..) => heatmap += 1
      ErrorRegion(..) => error_region += 1
    }
  }
  { bbox, mask, keypoints, trajectory, heatmap, error_region }
}

///|
fn add_overlay_kind_counts(
  left : OverlayKindCounts,
  right : OverlayKindCounts,
) -> OverlayKindCounts {
  {
    bbox: left.bbox + right.bbox,
    mask: left.mask + right.mask,
    keypoints: left.keypoints + right.keypoints,
    trajectory: left.trajectory + right.trajectory,
    heatmap: left.heatmap + right.heatmap,
    error_region: left.error_region + right.error_region,
  }
}

///|
fn diagnostic_severity_counts(
  diagnostics : Array[Diagnostic],
) -> DiagnosticSeverityCounts {
  let mut info = 0
  let mut warning = 0
  let mut error = 0
  for diagnostic in diagnostics {
    match diagnostic.level {
      Info => info += 1
      Warning => warning += 1
      Error => error += 1
    }
  }
  { info, warning, error }
}

///|
fn manifest_to_json(manifest : DocumentManifest) -> Json {
  Json::object({
    "title": Json::string(manifest.title),
    "image": Json::object({
      "width": Json::number(manifest.image_width.to_double()),
      "height": Json::number(manifest.image_height.to_double()),
    }),
    "layer_count": Json::number(manifest.layer_count.to_double()),
    "visible_layer_count": Json::number(
      manifest.visible_layer_count.to_double(),
    ),
    "total_overlay_count": Json::number(
      manifest.total_overlay_count.to_double(),
    ),
    "visible_overlay_count": Json::number(
      manifest.visible_overlay_count.to_double(),
    ),
    "overlays": overlay_kind_counts_to_json(manifest.overlays),
    "visible_overlays": overlay_kind_counts_to_json(manifest.visible_overlays),
    "content_bounds": optional_rect_to_json(manifest.content_bounds),
    "diagnostics": diagnostic_severity_counts_to_json(manifest.diagnostics),
    "is_valid": Json::boolean(manifest.is_valid),
    "layers": Json::array(manifest.layers.map(layer_manifest_to_json)),
  })
}

///|
fn layer_manifest_to_json(layer : LayerManifest) -> Json {
  Json::object({
    "id": Json::string(layer.id),
    "visible": Json::boolean(layer.visible),
    "opacity": Json::number(layer.opacity),
    "overlay_count": Json::number(layer.overlay_count.to_double()),
    "overlays": overlay_kind_counts_to_json(layer.overlays),
  })
}

///|
fn overlay_kind_counts_to_json(counts : OverlayKindCounts) -> Json {
  Json::object({
    "bbox": Json::number(counts.bbox.to_double()),
    "mask": Json::number(counts.mask.to_double()),
    "keypoints": Json::number(counts.keypoints.to_double()),
    "trajectory": Json::number(counts.trajectory.to_double()),
    "heatmap": Json::number(counts.heatmap.to_double()),
    "error_region": Json::number(counts.error_region.to_double()),
  })
}

///|
fn optional_rect_to_json(bounds : Rect?) -> Json {
  match bounds {
    None => Json::null()
    Some(rect) =>
      Json::object({
        "x": Json::number(rect.x),
        "y": Json::number(rect.y),
        "width": Json::number(rect.width),
        "height": Json::number(rect.height),
      })
  }
}

///|
fn diagnostic_severity_counts_to_json(
  counts : DiagnosticSeverityCounts,
) -> Json {
  Json::object({
    "info": Json::number(counts.info.to_double()),
    "warning": Json::number(counts.warning.to_double()),
    "error": Json::number(counts.error.to_double()),
  })
}

///|
fn escape_markdown(value : String) -> String {
  value
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\n", new="\\n")
  .replace_all(old="&", new="&")
  .replace_all(old="<", new="<")
  .replace_all(old=">", new=">")
  .replace_all(old="|", new="\\|")
  .replace_all(old="*", new="\\*")
  .replace_all(old="_", new="\\_")
  .replace_all(old="[", new="\\[")
  .replace_all(old="]", new="\\]")
  .replace_all(old="`", new="\\`")
  .replace_all(old="#", new="\\#")
  .replace_all(old="\"", new="\\\"")
}