///|
pub struct ReproducibilityRecord {
  name : String
  first_checksum : UInt
  second_checksum : UInt
  stable : Bool
  annotation_stable : Bool
} derive(Debug, Eq)

///|
pub fn check_plan_reproducibility(plan : ScenePlan) -> ReproducibilityRecord {
  let first = render_plan(plan)
  let second = render_plan(plan)
  {
    name: plan.name,
    first_checksum: scene_checksum(first),
    second_checksum: scene_checksum(second),
    stable: image_checksum(first.image) == image_checksum(second.image),
    annotation_stable: first.boxes == second.boxes &&
    first.mask == second.mask &&
    first.flow == second.flow,
  }
}

///|
pub fn check_suite_reproducibility(
  plans : Array[ScenePlan],
) -> Array[ReproducibilityRecord] {
  let out : Array[ReproducibilityRecord] = []
  for plan in plans {
    out.push(check_plan_reproducibility(plan))
  }
  out
}

///|
pub struct IntegrityReport {
  checked : Int
  stable : Int
  unstable : Int
  duplicate_checksums : Int
  errors : Array[String]
} derive(Debug, Eq)

///|
pub fn IntegrityReport::from_records(
  records : Array[ReproducibilityRecord],
) -> IntegrityReport {
  let mut stable = 0
  let mut unstable = 0
  let mut duplicate = 0
  let errors : Array[String] = []
  for record in records {
    if record.stable && record.annotation_stable {
      stable += 1
    } else {
      unstable += 1
      errors.push(record.name + " is not reproducible")
    }
  }
  for i in 0.. Bool {
  self.unstable == 0
}

///|
pub fn IntegrityReport::to_json(self : IntegrityReport) -> String {
  let sb = StringBuilder()
  sb.write_string("{\"checked\":\{self.checked},\"stable\":\{self.stable},")
  sb.write_string(
    "\"unstable\":\{self.unstable},\"duplicate_checksums\":\{self.duplicate_checksums},",
  )
  sb.write_string("\"valid\":\{self.valid()},\"errors\":[")
  for i, error in self.errors {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string("\"\{escape_json(error)}\"")
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
pub fn audit_default_suite(size : Size) -> IntegrityReport {
  IntegrityReport::from_records(
    check_suite_reproducibility(default_suite(size~)),
  )
}

///|
pub fn validate_scene_annotations(scene : SceneOutput) -> ValidationReport {
  let report = validate_scene(scene)
  if scene.mask.width > 0 &&
    scene.mask.height > 0 &&
    (
      scene.mask.width != scene.image.width ||
      scene.mask.height != scene.image.height
    ) {
    report.error("scene mask dimensions must match image dimensions")
  }
  for box in scene.boxes {
    if box.label.name.length() == 0 {
      report.warning("scene contains an annotation without a label name")
    }
    if box.rect.area() == 0 {
      report.error("scene contains an empty bounding box")
    }
  }
  report
}

///|
pub struct ChecksumEntry {
  name : String
  checksum : UInt
  pixels : Int
  annotations : Int
} derive(Debug, Eq)

///|
pub fn ChecksumEntry::from_scene(scene : SceneOutput) -> ChecksumEntry {
  {
    name: scene.plan.name,
    checksum: scene_checksum(scene),
    pixels: scene.image.width * scene.image.height,
    annotations: scene.boxes.length() +
    scene.flow.length() +
    scene.mask.count(1),
  }
}

///|
pub fn checksum_table(scenes : Array[SceneOutput]) -> Array[ChecksumEntry] {
  let out : Array[ChecksumEntry] = []
  for scene in scenes {
    out.push(ChecksumEntry::from_scene(scene))
  }
  out
}

///|
pub fn checksum_table_json(scenes : Array[SceneOutput]) -> String {
  let entries = checksum_table(scenes)
  let sb = StringBuilder()
  sb.write_string("[")
  for i, entry in entries {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string(
      "{\"name\":\"\{escape_json(entry.name)}\",\"checksum\":\{entry.checksum},\"pixels\":\{entry.pixels},\"annotations\":\{entry.annotations}}",
    )
  }
  sb.write_string("]")
  sb.to_string()
}

///|
pub fn validate_suite(plans : Array[ScenePlan]) -> ValidationReport {
  let report = ValidationReport::new()
  if plans.length() == 0 {
    report.warning("suite contains no scene plans")
  }
  for plan in plans {
    if plan.name.length() == 0 {
      report.error("scene plan name must not be empty")
    }
    let size_report = validate_size(plan.size, plan.name)
    for error in size_report.errors {
      report.error(error)
    }
    for warning in size_report.warnings {
      report.warning(warning)
    }
  }
  report
}

///|
pub fn reproducibility_summary(plans : Array[ScenePlan]) -> String {
  IntegrityReport::from_records(check_suite_reproducibility(plans)).to_json()
}

///|
pub fn compare_scene_outputs(
  first : SceneOutput,
  second : SceneOutput,
) -> String {
  let image_error = image_mae(first.image, second.image)
  let mask_error = if first.mask == second.mask { 0 } else { 1 }
  let box_error = if first.boxes == second.boxes { 0 } else { 1 }
  "{\"image_mae\":\{image_error},\"mask_mismatch\":\{mask_error},\"box_mismatch\":\{box_error}}"
}