///|
/// One text region on the detection probability map, before it is scaled back
/// to the source image.
pub(all) struct DetBox {
  x : Double
  y : Double
  width : Double
  height : Double
  score : Double
} derive(Debug, Eq, ToJson)

///|
pub extend DetBox with Eq::{not_equal, equal}

///|
pub extend DetBox with ToJson::{to_json}

///|
pub extend DetBox with @debug.Debug::{to_repr}

///|
/// Axis-aligned text region in source pixels.
pub(all) struct GlyphBox {
  x : Int32
  y : Int32
  width : Int32
  height : Int32
  score : Double
} derive(Debug, Eq, ToJson)

///|
pub extend GlyphBox with Eq::{not_equal, equal}

///|
pub extend GlyphBox with ToJson::{to_json}

///|
pub extend GlyphBox with @debug.Debug::{to_repr}

///|
/// Axis-aligned and rotated views of the same probability map, computed in one
/// connected-component pass.
pub(all) struct DetRegions {
  boxes : Array[DetBox]
  quads : Array[DetQuad]
} derive(Debug, Eq, ToJson)

///|
pub extend DetRegions with Eq::{not_equal, equal}

///|
pub extend DetRegions with ToJson::{to_json}

///|
pub extend DetRegions with @debug.Debug::{to_repr}

///|
fn push_stack(stack : Array[Int32], cursor : Int32, value : Int32) -> Int32 {
  if cursor < stack.length() {
    stack[cursor] = value
  } else {
    stack.push(value)
  }
  cursor + 1
}

///|
fn consider(
  stack : Array[Int32],
  cursor : Int32,
  seen : Array[Int32],
  prob : Array[Float],
  thresh : Double,
  x : Int32,
  y : Int32,
  width : Int32,
  height : Int32,
) -> Int32 {
  if x < 0 || y < 0 || x >= width || y >= height {
    return cursor
  }
  let index = y * width + x
  if seen[index] != 0 || prob[index].to_double() < thresh {
    return cursor
  }
  seen[index] = 1
  push_stack(stack, cursor, index)
}

///|
priv struct DetComponent {
  min_x : Int32
  min_y : Int32
  max_x : Int32
  max_y : Int32
  score : Double
  pixels : Array[Int32]
}

///|
fn scan_components(
  prob : Array[Float],
  height~ : Int32,
  width~ : Int32,
  thresh~ : Double,
  box_thresh~ : Double,
  collect_pixels~ : Bool,
) -> Array[DetComponent] raise OcrError {
  if width <= 0 || height <= 0 {
    raise OcrError::Model("probability map size must be positive")
  }
  if prob.length() != width * height {
    raise OcrError::Model("probability map length does not match width*height")
  }
  let seen : Array[Int32] = Array::make(width * height, 0)
  let stack : Array[Int32] = []
  let components : Array[DetComponent] = []
  for y in 0.. 0 {
        cursor = cursor - 1
        let index = stack[cursor]
        let column = index % width
        let row = index / width
        count = count + 1
        if collect_pixels {
          pixels.push(index)
        }
        if column < min_x {
          min_x = column
        }
        if row < min_y {
          min_y = row
        }
        if column > max_x {
          max_x = column
        }
        if row > max_y {
          max_y = row
        }
        sum = sum + prob[index].to_double()
        cursor = consider(
          stack,
          cursor,
          seen,
          prob,
          thresh,
          column + 1,
          row,
          width,
          height,
        )
        cursor = consider(
          stack,
          cursor,
          seen,
          prob,
          thresh,
          column - 1,
          row,
          width,
          height,
        )
        cursor = consider(
          stack,
          cursor,
          seen,
          prob,
          thresh,
          column,
          row + 1,
          width,
          height,
        )
        cursor = consider(
          stack,
          cursor,
          seen,
          prob,
          thresh,
          column,
          row - 1,
          width,
          height,
        )
      }
      if count == 0 {
        continue
      }
      let score = sum / count.to_double()
      if score < box_thresh {
        continue
      }
      components.push({ min_x, min_y, max_x, max_y, score, pixels, })
    }
  }
  components
}

///|
fn box_from_component(
  component : DetComponent,
  map_width~ : Int32,
  map_height~ : Int32,
  unclip_ratio~ : Double,
  min_size~ : Int32,
) -> DetBox? {
  let box_w = component.max_x - component.min_x + 1
  let box_h = component.max_y - component.min_y + 1
  if box_w < min_size || box_h < min_size {
    return None
  }
  let region_w = box_w.to_double()
  let region_h = box_h.to_double()
  let perimeter = 2.0 * (region_w + region_h)
  let distance = if perimeter == 0.0 {
    0.0
  } else {
    region_w * region_h * unclip_ratio / perimeter
  }
  let left = component.min_x.to_double() - distance
  let top = component.min_y.to_double() - distance
  let right = left + region_w + distance * 2.0
  let bottom = top + region_h + distance * 2.0
  let x0 = if left < 0.0 { 0.0 } else { left }
  let y0 = if top < 0.0 { 0.0 } else { top }
  let map_w = map_width.to_double()
  let map_h = map_height.to_double()
  let x1 = if right > map_w { map_w } else { right }
  let y1 = if bottom > map_h { map_h } else { bottom }
  if x1 - x0 < 1.0 || y1 - y0 < 1.0 {
    None
  } else {
    Some({
      x: x0,
      y: y0,
      width: x1 - x0,
      height: y1 - y0,
      score: component.score,
    })
  }
}

///|
fn quad_from_component(
  component : DetComponent,
  width~ : Int32,
  unclip_ratio~ : Double,
  min_size~ : Int32,
) -> DetQuad? raise OcrError {
  let points : Array[Point] = []
  for index in component.pixels {
    let left = (index % width).to_double()
    let top = (index / width).to_double()
    points.push({ x: left, y: top, })
    points.push({ x: left + 1.0, y: top, })
    points.push({ x: left + 1.0, y: top + 1.0, })
    points.push({ x: left, y: top + 1.0, })
  }
  let fitted = min_area_rect(points)
  if quad_short_side(fitted) < min_size.to_double() {
    None
  } else {
    Some(unclip_quad({ ..fitted, score: component.score, }, ratio=unclip_ratio))
  }
}

///|
/// Connected-component boxes for a DBNet probability map.
///
/// The map is row-major. Components below `min_size` or `box_thresh` are
/// dropped. The remaining rectangle is expanded by the DB unclip distance
/// `area * unclip_ratio / perimeter` and clipped to the map.
pub fn db_boxes(
  prob : Array[Float],
  height~ : Int32,
  width~ : Int32,
  thresh~ : Double = 0.3,
  box_thresh~ : Double = 0.6,
  unclip_ratio~ : Double = 1.5,
  min_size~ : Int32 = 3,
) -> Array[DetBox] raise OcrError {
  let components = scan_components(
    prob,
    height~,
    width~,
    thresh~,
    box_thresh~,
    collect_pixels=false,
  )
  let boxes : Array[DetBox] = []
  for component in components {
    match
      box_from_component(
        component,
        map_width=width,
        map_height=height,
        unclip_ratio~,
        min_size~,
      ) {
      Some(box) => boxes.push(box)
      None => ()
    }
  }
  boxes
}

///|
/// Connected components as minimum-area rectangles, expanded by the DB unclip
/// distance. Short sides below `min_size` and means below `box_thresh` are
/// dropped. Corners are ordered from the top-left.
pub fn db_quads(
  prob : Array[Float],
  height~ : Int32,
  width~ : Int32,
  thresh~ : Double = 0.3,
  box_thresh~ : Double = 0.6,
  unclip_ratio~ : Double = 1.5,
  min_size~ : Int32 = 3,
) -> Array[DetQuad] raise OcrError {
  let components = scan_components(
    prob,
    height~,
    width~,
    thresh~,
    box_thresh~,
    collect_pixels=true,
  )
  let quads : Array[DetQuad] = []
  for component in components {
    match quad_from_component(component, width~, unclip_ratio~, min_size~) {
      Some(quad) => quads.push(quad)
      None => ()
    }
  }
  quads
}

///|
/// Compute axis-aligned boxes and minimum-area quads in one pass.
///
/// The two arrays use the same component scan, but each keeps its own
/// `min_size` rule: boxes check the bounding rectangle, quads check the fitted
/// rectangle.
pub fn db_regions(
  prob : Array[Float],
  height~ : Int32,
  width~ : Int32,
  thresh~ : Double = 0.3,
  box_thresh~ : Double = 0.6,
  unclip_ratio~ : Double = 1.5,
  min_size~ : Int32 = 3,
) -> DetRegions raise OcrError {
  let components = scan_components(
    prob,
    height~,
    width~,
    thresh~,
    box_thresh~,
    collect_pixels=true,
  )
  let boxes : Array[DetBox] = []
  let quads : Array[DetQuad] = []
  for component in components {
    match
      box_from_component(
        component,
        map_width=width,
        map_height=height,
        unclip_ratio~,
        min_size~,
      ) {
      Some(box) => boxes.push(box)
      None => ()
    }
    match quad_from_component(component, width~, unclip_ratio~, min_size~) {
      Some(quad) => quads.push(quad)
      None => ()
    }
  }
  { boxes, quads, }
}

///|
/// Scale a detection box back to source pixels.
pub fn map_det_box(
  box : DetBox,
  ratio_w~ : Double,
  ratio_h~ : Double,
  width~ : Int32,
  height~ : Int32,
) -> GlyphBox? {
  if ratio_w == 0.0 || ratio_h == 0.0 || width <= 0 || height <= 0 {
    return None
  }
  let left = (box.x / ratio_w).floor().to_int()
  let top = (box.y / ratio_h).floor().to_int()
  let right = ((box.x + box.width) / ratio_w).ceil().to_int()
  let bottom = ((box.y + box.height) / ratio_h).ceil().to_int()
  let x0 = if left < 0 { 0 } else { left }
  let y0 = if top < 0 { 0 } else { top }
  let x1 = if right > width { width } else { right }
  let y1 = if bottom > height { height } else { bottom }
  if x1 <= x0 || y1 <= y0 {
    None
  } else {
    Some({ x: x0, y: y0, width: x1 - x0, height: y1 - y0, score: box.score, })
  }
}

///|
fn cmp_int(left : Int32, right : Int32) -> Int32 {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
fn same_line(line : Array[GlyphBox], box : GlyphBox) -> Bool {
  if line.length() == 0 {
    return false
  }
  let mut top = line[0].y
  let mut bottom = line[0].y + line[0].height
  for item in line {
    if item.y < top {
      top = item.y
    }
    let item_bottom = item.y + item.height
    if item_bottom > bottom {
      bottom = item_bottom
    }
  }
  let overlap_top = if top > box.y { top } else { box.y }
  let overlap_bottom = if bottom < box.y + box.height {
    bottom
  } else {
    box.y + box.height
  }
  let overlap = overlap_bottom - overlap_top
  if overlap <= 0 {
    return false
  }
  let line_height = bottom - top
  let shorter = if line_height < box.height { line_height } else { box.height }
  shorter > 0 && overlap * 2 >= shorter
}

///|
/// Group boxes into reading lines. Lines run top to bottom, and boxes inside a
/// line run left to right.
pub fn group_lines(boxes : Array[GlyphBox]) -> Array[Array[GlyphBox]] {
  let ordered : Array[GlyphBox] = []
  for box in boxes {
    ordered.push(box)
  }
  ordered.sort_by((left, right) => {
    let by_y = cmp_int(left.y, right.y)
    if by_y != 0 {
      by_y
    } else {
      cmp_int(left.x, right.x)
    }
  })
  let lines : Array[Array[GlyphBox]] = []
  for box in ordered {
    if lines.length() == 0 || !same_line(lines[lines.length() - 1], box) {
      lines.push([box])
    } else {
      lines[lines.length() - 1].push(box)
    }
  }
  for line in lines {
    line.sort_by((left, right) => cmp_int(left.x, right.x))
  }
  lines
}

///|
/// Group placed quadrilaterals with the same reading-order rule as `group_lines`.
pub fn group_placed(items : Array[PlacedText]) -> Array[Array[PlacedText]] {
  let ordered : Array[PlacedText] = []
  for item in items {
    ordered.push(item)
  }
  ordered.sort_by((left, right) => {
    let by_y = cmp_int(left.bounds.y, right.bounds.y)
    if by_y != 0 {
      by_y
    } else {
      cmp_int(left.bounds.x, right.bounds.x)
    }
  })
  let lines : Array[Array[PlacedText]] = []
  for item in ordered {
    if lines.length() == 0 {
      lines.push([item])
    } else {
      let current : Array[GlyphBox] = []
      for placed in lines[lines.length() - 1] {
        current.push(placed.bounds)
      }
      if same_line(current, item.bounds) {
        lines[lines.length() - 1].push(item)
      } else {
        lines.push([item])
      }
    }
  }
  for line in lines {
    line.sort_by((left, right) => cmp_int(left.bounds.x, right.bounds.x))
  }
  lines
}