///|
// COCO boundary quantization and event encoding adapted from rleFrPoly in
// cocodataset/cocoapi/common/maskApi.c. See THIRD_PARTY_NOTICES.md (BSD-2-Clause).
// Boundary samples are streamed: no dense image or boundary-point array.
priv struct PolygonPoint {
  x : Int64
  y : Int64
} derive(Eq)

///|
fn polygon_coordinate(
  value : Double,
  location : String,
) -> Int64 raise EvalError {
  let scaled = value * 5.0 + 0.5
  if !finite(value) ||
    !finite(scaled) ||
    scaled < -2147483648.0 ||
    scaled > 2147483647.0 {
    raise InvalidInput(
      location + " must be finite and fit COCO scaled Int coordinates",
    )
  }
  scaled.trunc().to_int64()
}

///|
fn polygon_event(
  previous : PolygonPoint,
  current : PolygonPoint,
  height : Int32,
  width : Int32,
) -> Int32? {
  if previous.x == current.x {
    return None
  }
  let boundary_x = if current.x < previous.x {
    current.x
  } else {
    current.x - 1L
  }
  let x = (boundary_x.to_double() + 0.5) / 5.0 - 0.5
  if x.floor() != x || x < 0.0 || x > width.to_double() - 1.0 {
    return None
  }
  let y = ((previous.y.min(current.y).to_double() + 0.5) / 5.0 - 0.5)
    .max(0.0)
    .min(height.to_double())
    .ceil()
  Some((x.to_int64() * height.to_int64() + y.to_int64()).to_int())
}

///|
fn rasterize_polygon(
  height : Int32,
  width : Int32,
  points : Array[PolygonPoint],
) -> CocoRle raise EvalError {
  let pixels = rle_pixel_count(height, width).to_int()
  if points.length() < 3 {
    return CocoRle::new(height, width, [pixels])
  }
  let events : Array[Int32] = []
  let mut previous : PolygonPoint? = None
  for index in 0..= dy && first.x > last.x) || (dx < dy && first.y > last.y)
    let start = if flip { last } else { first }
    let end = if flip { first } else { last }
    let steps = dx.max(dy).to_int()
    let slope = if dx >= dy {
      (end.y - start.y).to_double() / dx.to_double()
    } else {
      (end.x - start.x).to_double() / dy.to_double()
    }
    for offset in 0..<=steps {
      let distance = (if flip { steps - offset } else { offset }).to_int64()
      let current = if dx >= dy {
        PolygonPoint::{
          x: start.x + distance,
          y: (start.y.to_double() + slope * distance.to_double() + 0.5)
          .trunc()
          .to_int64(),
        }
      } else {
        PolygonPoint::{
          x: (start.x.to_double() + slope * distance.to_double() + 0.5)
          .trunc()
          .to_int64(),
          y: start.y + distance,
        }
      }
      match previous {
        Some(point) =>
          match polygon_event(point, current, height, width) {
            Some(event) => events.push(event)
            None => ()
          }
        None => ()
      }
      previous = Some(current)
    }
  }
  events.push(pixels)
  events.sort()
  let counts = [events[0]]
  let mut index = 1
  while index < events.length() {
    let span = events[index] - events[index - 1]
    if span > 0 {
      counts.push(span)
      index += 1
    } else {
      // Coincident boundary crossings toggle twice and cancel.
      index += 1
      if index < events.length() {
        counts[counts.length() - 1] += events[index] - events[index - 1]
        index += 1
      }
    }
  }
  CocoRle::new(height, width, counts)
}

///|
fn polygon_union(
  left : CocoRle,
  right : CocoRle,
  remaining : Int32,
) -> (CocoRle, Int32) raise EvalError {
  let counts = [0]
  let mut active = false
  let mut left_index = 0
  let mut right_index = 0
  let mut left_remaining = left.counts[0]
  let mut right_remaining = right.counts[0]
  let mut work = 0
  while left_index < left.counts.length() && right_index < right.counts.length() {
    work += 1
    if work > remaining {
      raise InvalidInput("polygon RLE union exceeds 2000000 run steps per mask")
    }
    if left_remaining == 0 {
      left_index += 1
      if left_index < left.counts.length() {
        left_remaining = left.counts[left_index]
      }
      continue
    }
    if right_remaining == 0 {
      right_index += 1
      if right_index < right.counts.length() {
        right_remaining = right.counts[right_index]
      }
      continue
    }
    let foreground = left_index % 2 == 1 || right_index % 2 == 1
    let span = left_remaining.min(right_remaining)
    if foreground == active {
      counts[counts.length() - 1] += span
    } else {
      counts.push(span)
      active = foreground
    }
    left_remaining -= span
    right_remaining -= span
  }
  (CocoRle::new(left.height, left.width, counts), work)
}

///|
/// Rasterizes COCO polygon rings and unions them into one instance mask.
/// Rings contain >=3 x/y pairs; winding does not create holes. Empty ring lists
/// are invalid. Degenerate rings may yield empty masks. Uses COCO's 5x boundary
/// quantization. Limits: 100000 total vertices, 2000000 boundary samples and
/// 2000000 union run steps per mask; scaled coordinates fit signed 32-bit Int.
pub fn CocoRle::from_polygons(
  height : Int32,
  width : Int32,
  polygons : Array[Array[Double]],
) -> CocoRle raise EvalError {
  let _ = rle_pixel_count(height, width)
  if polygons.is_empty() {
    raise InvalidInput("segmentation polygons must contain at least one ring")
  }
  let rings : Array[Array[PolygonPoint]] = []
  let mut vertices = 0
  let mut samples = 0L
  for ring_index, coordinates in polygons {
    let location = "segmentation[" + ring_index.to_string() + "]"
    if coordinates.length() < 6 || coordinates.length() % 2 != 0 {
      raise InvalidInput(location + " must contain at least three x/y pairs")
    }
    if coordinates.length() / 2 > 100000 - vertices {
      raise InvalidInput(
        "segmentation exceeds 100000 polygon vertices per mask",
      )
    }
    vertices += coordinates.length() / 2
    let points : Array[PolygonPoint] = []
    for index in 0..<(coordinates.length() / 2) {
      let point = PolygonPoint::{
        x: polygon_coordinate(
          coordinates[index * 2],
          location + "[" + (index * 2).to_string() + "]",
        ),
        y: polygon_coordinate(
          coordinates[index * 2 + 1],
          location + "[" + (index * 2 + 1).to_string() + "]",
        ),
      }
      if points.is_empty() || points[points.length() - 1] != point {
        points.push(point)
      }
    }
    if points.length() > 1 && points[0] == points[points.length() - 1] {
      points.pop() |> ignore
    }
    for index in 0.. 2000000L {
        raise InvalidInput(
          "segmentation exceeds 2000000 polygon boundary samples per mask",
        )
      }
    }
    rings.push(points)
  }
  let mut result = rasterize_polygon(height, width, rings[0])
  let mut union_work = 0
  for index in 1.. CocoRle raise EvalError {
  let mask = match value {
    Array(values) => {
      let polygons : Array[Array[Double]] = []
      let mut vertices = 0
      for index, ring in values {
        let location = "segmentation[" + index.to_string() + "]"
        let entries = match ring {
          Array(entries) => entries
          _ => raise InvalidInput(location + " must be an array of numbers")
        }
        if entries.length() / 2 > 100000 - vertices {
          raise InvalidInput(
            "segmentation exceeds 100000 polygon vertices per mask",
          )
        }
        vertices += entries.length() / 2
        let coordinates : Array[Double] = []
        for coordinate_index, entry in entries {
          match entry {
            Number(value, ..) => coordinates.push(value)
            _ =>
              raise InvalidInput(
                location +
                "[" +
                coordinate_index.to_string() +
                "] must be a number",
              )
          }
        }
        polygons.push(coordinates)
      }
      CocoRle::from_polygons(height, width, polygons)
    }
    _ => parse_rle_value(value)
  }
  if mask.height != height || mask.width != width {
    raise InvalidInput("segmentation RLE dimensions differ from image")
  }
  mask
}

///|
/// Parses a COCO segmentation RLE object or array of polygon rings, using the
/// declared image dimensions. RLE sizes must agree; polygon rings are unioned.
pub fn parse_coco_segmentation(
  source : String,
  height : Int32,
  width : Int32,
) -> CocoRle raise EvalError {
  let value = @json.parse(source) catch {
    _ => raise InvalidInput("segmentation is not valid JSON")
  }
  parse_segmentation_value(value, height, width)
}