///|
pub struct ValidationReport {
  mut valid : Bool
  errors : Array[String]
  warnings : Array[String]
} derive(Debug, Eq)

///|
pub fn ValidationReport::new() -> ValidationReport {
  { valid: true, errors: [], warnings: [] }
}

///|
fn ValidationReport::error(self : ValidationReport, message : String) -> Unit {
  self.valid = false
  self.errors.push(message)
}

///|
fn ValidationReport::warning(self : ValidationReport, message : String) -> Unit {
  self.warnings.push(message)
}

///|
pub fn ValidationReport::message_count(self : ValidationReport) -> Int {
  self.errors.length() + self.warnings.length()
}

///|
pub fn ValidationReport::to_json(self : ValidationReport) -> String {
  let sb = StringBuilder()
  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("],\"warnings\":[")
  for i, warning in self.warnings {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string("\"\{escape_json(warning)}\"")
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
pub fn validate_size(size : Size, name : String) -> ValidationReport {
  let report = ValidationReport::new()
  if size.width <= 0 {
    report.error("\{name}: width must be positive")
  }
  if size.height <= 0 {
    report.error("\{name}: height must be positive")
  }
  if size.width * size.height > 100000000 {
    report.warning("\{name}: large allocation may be expensive")
  }
  report
}

///|
pub fn validate_image(image : GrayImage, name : String) -> ValidationReport {
  let report = validate_size(image.size(), name)
  if image.pixels.length() != max_i(0, image.width * image.height) {
    report.error("\{name}: pixel buffer length does not match dimensions")
  }
  for value in image.pixels {
    if value < 0 || value > 255 {
      report.error("\{name}: pixel value is outside the 8-bit range")
      break
    }
  }
  report
}

///|
pub fn validate_mask(mask : Mask, name : String) -> ValidationReport {
  let report = validate_size(
    Size::new(width=mask.width, height=mask.height),
    name,
  )
  if mask.values.length() != max_i(0, mask.width * mask.height) {
    report.error("\{name}: mask buffer length does not match dimensions")
  }
  report
}

///|
pub fn validate_box(box : Box2D, size : Size) -> ValidationReport {
  let report = ValidationReport::new()
  if box.label.id < 0 {
    report.error("box label id must be non-negative")
  }
  if box.rect.is_empty() {
    report.error("box must have positive area")
  }
  if box.rect.x < 0 ||
    box.rect.y < 0 ||
    box.rect.right() > size.width ||
    box.rect.bottom() > size.height {
    report.warning("box extends outside image bounds")
  }
  report
}

///|
pub fn validate_scene(scene : SceneOutput) -> ValidationReport {
  let report = validate_image(scene.image, "scene.image")
  let mask_report = validate_mask(scene.mask, "scene.mask")
  for error in mask_report.errors {
    report.error(error)
  }
  for warning in mask_report.warnings {
    report.warning(warning)
  }
  for box in scene.boxes {
    let box_report = validate_box(box, scene.image.size())
    for error in box_report.errors {
      report.error(error)
    }
    for warning in box_report.warnings {
      report.warning(warning)
    }
  }
  for vector in scene.flow {
    if !scene.image.in_bounds(vector.x, vector.y) {
      report.warning("flow vector origin is outside image bounds")
    }
  }
  report
}

///|
pub fn validate_manifest(manifest : DatasetManifest) -> ValidationReport {
  let report = ValidationReport::new()
  if manifest.name.length() == 0 {
    report.error("manifest name must not be empty")
  }
  if manifest.version.length() == 0 {
    report.error("manifest version must not be empty")
  }
  if !manifest.has_unique_names() {
    report.error("manifest asset names must be unique")
  }
  for asset in manifest.assets {
    if asset.name.length() == 0 {
      report.error("asset name must not be empty")
    }
    if asset.image_path.length() == 0 {
      report.error("asset image path must not be empty")
    }
    if asset.annotation_path.length() == 0 {
      report.warning("asset has no annotation path")
    }
  }
  report
}

///|
pub fn image_checksum(image : GrayImage) -> UInt {
  let mut checksum : UInt = 2166136261
  for value in image.pixels {
    checksum = (checksum ^ value.reinterpret_as_uint()) * 16777619
  }
  checksum
}

///|
pub fn mask_checksum(mask : Mask) -> UInt {
  let mut checksum : UInt = 2166136261
  for value in mask.values {
    checksum = (checksum ^ value.reinterpret_as_uint()) * 16777619
  }
  checksum
}

///|
pub fn scene_checksum(scene : SceneOutput) -> UInt {
  let mut checksum = image_checksum(scene.image)
  checksum = (checksum ^ mask_checksum(scene.mask)) * 16777619
  for box in scene.boxes {
    checksum = (checksum ^ box.rect.x.reinterpret_as_uint()) * 16777619
    checksum = (checksum ^ box.rect.y.reinterpret_as_uint()) * 16777619
    checksum = (checksum ^ box.rect.width.reinterpret_as_uint()) * 16777619
    checksum = (checksum ^ box.rect.height.reinterpret_as_uint()) * 16777619
  }
  checksum
}

///|
pub fn manifest_checksum(manifest : DatasetManifest) -> UInt {
  let mut checksum : UInt = 2166136261
  for asset in manifest.assets {
    checksum = (checksum ^ asset.name.length().reinterpret_as_uint()) * 16777619
    checksum = (checksum ^ asset.seed) * 16777619
  }
  checksum
}