///|
/// Reads the COCO compressed counts string (signed 5-bit groups, ASCII 48–111).
/// Counts from the fourth run onward are differences from two runs earlier.
/// At most seven characters per run are accepted for Int-sized masks.
pub fn CocoRle::from_compressed(
  height : Int32,
  width : Int32,
  encoded : String,
) -> CocoRle raise EvalError {
  let pixels = rle_pixel_count(height, width)
  let counts : Array[Int32] = []
  let mut offset = 0
  let mut total = 0L
  while offset < encoded.length() {
    let start = offset
    let mut value = 0L
    let mut shift = 0
    while true {
      if offset >= encoded.length() {
        raise InvalidInput("mask RLE counts truncated at character \{offset}")
      }
      let code = encoded[offset].to_int() - 48
      if code < 0 || code > 63 {
        raise InvalidInput("mask RLE counts invalid character at \{offset}")
      }
      if shift >= 35 {
        raise InvalidInput("mask RLE counts run too long at character \{start}")
      }
      value = value | ((code & 31).to_int64() << shift)
      shift += 5
      offset += 1
      if (code & 32) == 0 {
        if (code & 16) != 0 {
          value -= 1L << shift
        }
        break
      }
    }
    if counts.length() > 2 {
      value += counts[counts.length() - 2].to_int64()
    }
    if value < 0L || value > pixels - total {
      raise InvalidInput("mask RLE counts invalid run at character \{start}")
    }
    total += value
    counts.push(value.to_int())
  }
  CocoRle::new(height, width, counts)
}

///|
/// Exports the stored run sequence in COCO compressed counts format.
pub fn CocoRle::to_compressed(self : CocoRle) -> String {
  let encoded = StringBuilder()
  for index, count in self.counts {
    let mut value = count.to_int64()
    if index > 2 {
      value -= self.counts[index - 2].to_int64()
    }
    while true {
      let mut code = (value & 31L).to_int()
      value = value >> 5
      let more = if (code & 16) != 0 { value != -1L } else { value != 0L }
      if more {
        code = code | 32
      }
      encoded.write_char((code + 48).to_char().unwrap())
      if !more {
        break
      }
    }
  }
  encoded.to_string()
}

///|
fn rle_json_integer(value : Json, location : String) -> Int32 raise EvalError {
  match value {
    Number(number, ..) if finite(number) &&
      number >= 0.0 &&
      number <= 2147483647.0 &&
      number.trunc() == number => number.to_int()
    _ =>
      raise InvalidInput(location + " must be a nonnegative Int-sized integer")
  }
}

///|
/// Parses one COCO segmentation RLE object: size is [height, width] and counts
/// is either an integer run array or a compressed string. Polygons are rejected.
/// Size and array counts must decode to nonnegative Int-sized integer values.
pub fn parse_coco_rle(source : String) -> CocoRle raise EvalError {
  let value = @json.parse(source) catch {
    _ => raise InvalidInput("mask RLE is not valid JSON")
  }
  parse_rle_value(value)
}

///|
fn parse_rle_value(value : Json) -> CocoRle raise EvalError {
  guard value is Object(fields) else {
    raise InvalidInput("mask RLE must be an object with size and counts")
  }
  let size = match fields.get("size") {
    Some(Array(values)) if values.length() == 2 => values
    _ => raise InvalidInput("mask RLE size must be [height, width]")
  }
  let height = rle_json_integer(size[0], "mask RLE size[0]")
  let width = rle_json_integer(size[1], "mask RLE size[1]")
  // Validate dimensions before allocating or decoding counts.
  let _ = rle_pixel_count(height, width)
  match fields.get("counts") {
    Some(String(encoded)) => CocoRle::from_compressed(height, width, encoded)
    Some(Array(values)) => {
      let counts = []
      for index, value in values {
        counts.push(rle_json_integer(value, "mask RLE counts[\{index}]"))
      }
      CocoRle::new(height, width, counts)
    }
    _ =>
      raise InvalidInput(
        "mask RLE counts must be a run array or compressed string",
      )
  }
}