///|
/// An instance mask with COCO annotation area (which may differ from pixel area).
pub struct MaskGroundTruth {
  value : GroundTruth
}

///|
pub fn MaskGroundTruth::new(
  id : Int64,
  image_id : Int32,
  category_id : Int32,
  mask : CocoRle,
  area~ : Double,
  iscrowd~ : Bool = false,
  ignore~ : Bool = false,
) -> MaskGroundTruth raise EvalError {
  let value = GroundTruth::new(
    id,
    image_id,
    category_id,
    Box::from_xywh(
      0.0,
      0.0,
      mask.width().to_double(),
      mask.height().to_double(),
    ),
    area~,
    iscrowd~,
    ignore~,
  )
  { value: { ..value, mask: Some(mask), }, }
}

///|
pub fn MaskGroundTruth::id(self : MaskGroundTruth) -> Int64 {
  self.value.id
}

///|
pub fn MaskGroundTruth::image_id(self : MaskGroundTruth) -> Int32 {
  self.value.image_id
}

///|
pub fn MaskGroundTruth::category_id(self : MaskGroundTruth) -> Int32 {
  self.value.category_id
}

///|
pub fn MaskGroundTruth::mask(self : MaskGroundTruth) -> CocoRle {
  self.value.mask.unwrap()
}

///|
pub fn MaskGroundTruth::area(self : MaskGroundTruth) -> Double {
  self.value.area
}

///|
pub fn MaskGroundTruth::iscrowd(self : MaskGroundTruth) -> Bool {
  self.value.iscrowd
}

///|
pub fn MaskGroundTruth::ignore(self : MaskGroundTruth) -> Bool {
  self.value.ignore
}

///|
/// A scored mask. Detection area is always the RLE foreground pixel count.
pub struct MaskDetection {
  value : Detection
}

///|
pub fn MaskDetection::new(
  id : Int32,
  image_id : Int32,
  category_id : Int32,
  mask : CocoRle,
  score : Double,
) -> MaskDetection raise EvalError {
  let value = Detection::new(
    id,
    image_id,
    category_id,
    Box::from_xywh(
      0.0,
      0.0,
      mask.width().to_double(),
      mask.height().to_double(),
    ),
    score,
  )
  { value: { ..value, mask: Some(mask), }, }
}

///|
pub fn MaskDetection::id(self : MaskDetection) -> Int32 {
  self.value.id
}

///|
pub fn MaskDetection::image_id(self : MaskDetection) -> Int32 {
  self.value.image_id
}

///|
pub fn MaskDetection::category_id(self : MaskDetection) -> Int32 {
  self.value.category_id
}

///|
pub fn MaskDetection::mask(self : MaskDetection) -> CocoRle {
  self.value.mask.unwrap()
}

///|
pub fn MaskDetection::score(self : MaskDetection) -> Double {
  self.value.score
}

///|
/// Mask inputs are separated from bounding boxes to prevent mixed geometry.
pub struct MaskDataset {
  value : Dataset
}

///|
fn validate_mask_canvas(
  mask : CocoRle,
  image : Image,
  location : String,
) -> Unit raise EvalError {
  if mask.width() != image.width || mask.height() != image.height {
    raise InvalidInput(
      location + " mask dimensions differ from image " + image.id.to_string(),
    )
  }
}

///|
pub fn MaskDataset::new(
  images : Array[Image],
  categories : Array[Category],
  truths : Array[MaskGroundTruth],
) -> MaskDataset raise EvalError {
  let value = Dataset::new(
    images,
    categories,
    truths.map(fn(item) { item.value }),
  )
  let images_by_id : Map[Int32, Image] = Map([])
  for image in value.images {
    images_by_id[image.id] = image
  }
  for truth in truths {
    validate_mask_canvas(
      truth.mask(),
      images_by_id[truth.image_id()],
      "ground-truth " + truth.id().to_string(),
    )
  }
  { value, }
}

///|
pub fn MaskDataset::images(self : MaskDataset) -> Array[Image] {
  self.value.images()
}

///|
pub fn MaskDataset::categories(self : MaskDataset) -> Array[Category] {
  self.value.categories()
}

///|
pub fn MaskDataset::truths(self : MaskDataset) -> Array[MaskGroundTruth] {
  self.value.truths.map(fn(value) { { value, } })
}

///|
/// COCO segm defaults: ten IoUs, three area bins, maxDets 1/10/100, 101 recall points.
pub fn evaluate_masks(
  dataset : MaskDataset,
  predictions : Array[MaskDetection],
) -> EvaluationReport raise EvalError {
  evaluate_masks_with_config(dataset, predictions, EvalConfig::coco())
}

///|
/// Optional diagnostics and PR curves use mask overlap. COCO default ignore
/// handling uses iscrowd; respect_explicit remains an opt-in extension.
/// max_iou_cache_pairs bounds cached DT/GT pairs per category, in [0,16777216].
/// Zero disables caching. Oversized matrices use exact on-demand overlap;
/// this execution setting does not bound total process memory or change metrics.
pub fn evaluate_masks_with_config(
  dataset : MaskDataset,
  predictions : Array[MaskDetection],
  config : EvalConfig,
  max_iou_cache_pairs~ : Int32 = 262144,
) -> EvaluationReport raise EvalError {
  validate_iou_cache_limit(max_iou_cache_pairs)
  let images : Map[Int32, Image] = Map([])
  let categories : Map[Int32, Bool] = Map([])
  for image in dataset.value.images {
    images[image.id] = image
  }
  for category in dataset.value.categories {
    categories[category.id] = true
  }
  for index, prediction in predictions {
    let location = "detections[" + index.to_string() + "]"
    match images.get(prediction.image_id()) {
      Some(image) => validate_mask_canvas(prediction.mask(), image, location)
      None =>
        raise InvalidInput(location + ".image_id references unknown image")
    }
    if !categories.contains(prediction.category_id()) {
      raise InvalidInput(location + ".category_id references unknown category")
    }
  }
  let mask_config = if config.ignore_policy == "coco_bbox" {
    { ..config, ignore_policy: "coco_segm", }
  } else {
    config
  }
  evaluate_internal(
    dataset.value,
    predictions.map(fn(item) { item.value }),
    mask_config,
    iou_type="segm",
    max_iou_cache_pairs~,
  )
}

///|
pub fn compare_mask_predictions(
  dataset : MaskDataset,
  baseline : Array[MaskDetection],
  candidate : Array[MaskDetection],
) -> ComparisonReport raise EvalError {
  compare_mask_predictions_with_config(
    dataset,
    baseline,
    candidate,
    EvalConfig::coco(),
  )
}

///|
/// Both evaluations use the same per-category cache budget, sequentially.
pub fn compare_mask_predictions_with_config(
  dataset : MaskDataset,
  baseline : Array[MaskDetection],
  candidate : Array[MaskDetection],
  config : EvalConfig,
  max_iou_cache_pairs~ : Int32 = 262144,
) -> ComparisonReport raise EvalError {
  compare_reports(
    evaluate_masks_with_config(dataset, baseline, config, max_iou_cache_pairs~),
    evaluate_masks_with_config(dataset, candidate, config, max_iou_cache_pairs~),
  )
}