///|
/// Pixel-aligned rectangle used for dirty-region tracking.
pub struct PixelRect {
  x : Int
  y : Int
  width : Int
  height : Int
} derive(Eq, Debug)

///|
pub extend PixelRect with Debug::{to_repr}

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

///|
/// Strategy selected for submitting a dirty region to a Surface.
pub(all) enum DirtyPresentPlanMode {
  Empty
  Rects
  Bounds
} derive(Eq, Debug)

///|
pub extend DirtyPresentPlanMode with Debug::{to_repr}

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

///|
/// Scheduler-facing present strategy chosen from a dirty present plan and the
/// full-frame pixel budget.
pub(all) enum DirtyPresentStrategy {
  Skip
  Partial
  Full
} derive(Eq, Debug)

///|
pub extend DirtyPresentStrategy with Debug::{to_repr}

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

///|
/// Scheduler-facing state for a dirty submit snapshot.
pub(all) enum DirtySubmitState {
  Clean
  Present
  DirtyClippedAway
} derive(Eq, Debug)

///|
pub extend DirtySubmitState with Debug::{to_repr}

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

///|
/// A concrete dirty-present plan after clipping to the target bounds and
/// applying a rectangle-count limit.
pub struct DirtyPresentPlan {
  mode : DirtyPresentPlanMode
  rects : Array[PixelRect]
} derive(Debug)

///|
pub extend DirtyPresentPlan with Debug::{to_repr}

///|
/// Dirty state snapshot for schedulers: logical dirty bounds plus the clipped
/// present plan that would be submitted to the current surface.
pub struct DirtySubmitPlan {
  dirty_bounds : PixelRect?
  present_plan : DirtyPresentPlan
} derive(Debug)

///|
pub extend DirtySubmitPlan with Debug::{to_repr}

///|
/// Result of submitting a dirty frame: the scheduler-visible plan snapshot plus
/// the number of rectangles actually presented to the surface.
pub struct DirtySubmitResult {
  submit_plan : DirtySubmitPlan
  presented_rect_count : Int
} derive(Debug)

///|
pub extend DirtySubmitResult with Debug::{to_repr}

///|
pub fn DirtyPresentPlan::get_mode(
  self : DirtyPresentPlan,
) -> DirtyPresentPlanMode {
  self.mode
}

///|
pub fn DirtyPresentPlan::get_rects(self : DirtyPresentPlan) -> Array[PixelRect] {
  copy_pixel_rects(self.rects)
}

///|
pub fn DirtyPresentPlan::get_rect_count(self : DirtyPresentPlan) -> Int {
  self.rects.length()
}

///|
pub fn DirtyPresentPlan::get_present_bounds(
  self : DirtyPresentPlan,
) -> PixelRect? {
  guard !self.is_empty() else { return None }
  pixel_rect_array_bounds(self.rects)
}

///|
pub fn DirtyPresentPlan::get_present_pixel_count(
  self : DirtyPresentPlan,
) -> Int {
  guard !self.is_empty() else { return 0 }
  let mut pixels = 0
  for rect in self.rects {
    pixels = pixels + rect.area()
  }
  pixels
}

///|
pub fn DirtyPresentPlan::get_present_byte_count(self : DirtyPresentPlan) -> Int {
  self.get_present_pixel_count() * 4
}

///|
pub fn DirtyPresentPlan::saves_full_present_pixels(
  self : DirtyPresentPlan,
  width : Int,
  height : Int,
) -> Bool {
  guard !self.is_empty() else { return false }
  let full_pixels = PixelRect::new(x=0, y=0, width~, height~).area()
  full_pixels > 0 && self.get_present_pixel_count() < full_pixels
}

///|
pub fn DirtyPresentPlan::get_present_strategy(
  self : DirtyPresentPlan,
  width : Int,
  height : Int,
) -> DirtyPresentStrategy {
  guard !self.is_empty() else { return Skip }
  if self.saves_full_present_pixels(width, height) {
    Partial
  } else {
    Full
  }
}

///|
pub fn DirtyPresentPlan::is_empty(self : DirtyPresentPlan) -> Bool {
  self.mode == Empty || self.rects.length() == 0
}

///|
pub fn DirtyPresentPlan::uses_bounds_fallback(self : DirtyPresentPlan) -> Bool {
  self.mode == Bounds
}

///|
pub fn DirtySubmitPlan::get_dirty_bounds(self : DirtySubmitPlan) -> PixelRect? {
  self.dirty_bounds
}

///|
pub fn DirtySubmitPlan::get_present_plan(
  self : DirtySubmitPlan,
) -> DirtyPresentPlan {
  self.present_plan
}

///|
pub fn DirtySubmitPlan::has_dirty(self : DirtySubmitPlan) -> Bool {
  match self.dirty_bounds {
    Some(_) => true
    None => false
  }
}

///|
pub fn DirtySubmitPlan::needs_present(self : DirtySubmitPlan) -> Bool {
  !self.present_plan.is_empty()
}

///|
pub fn DirtySubmitPlan::get_state(self : DirtySubmitPlan) -> DirtySubmitState {
  if self.needs_present() {
    return Present
  }
  if self.has_dirty() {
    return DirtyClippedAway
  }
  Clean
}

///|
pub fn DirtySubmitPlan::get_present_rect_count(self : DirtySubmitPlan) -> Int {
  self.present_plan.get_rect_count()
}

///|
pub fn DirtySubmitPlan::get_present_pixel_count(self : DirtySubmitPlan) -> Int {
  self.present_plan.get_present_pixel_count()
}

///|
pub fn DirtySubmitPlan::get_present_byte_count(self : DirtySubmitPlan) -> Int {
  self.present_plan.get_present_byte_count()
}

///|
pub fn DirtySubmitPlan::get_present_bounds(
  self : DirtySubmitPlan,
) -> PixelRect? {
  self.present_plan.get_present_bounds()
}

///|
pub fn DirtySubmitPlan::saves_full_present_pixels(
  self : DirtySubmitPlan,
  width : Int,
  height : Int,
) -> Bool {
  self.present_plan.saves_full_present_pixels(width, height)
}

///|
pub fn DirtySubmitPlan::get_present_strategy(
  self : DirtySubmitPlan,
  width : Int,
  height : Int,
) -> DirtyPresentStrategy {
  self.present_plan.get_present_strategy(width, height)
}

///|
pub fn DirtySubmitPlan::uses_bounds_fallback(self : DirtySubmitPlan) -> Bool {
  self.present_plan.uses_bounds_fallback()
}

///|
pub fn DirtySubmitResult::get_submit_plan(
  self : DirtySubmitResult,
) -> DirtySubmitPlan {
  self.submit_plan
}

///|
pub fn DirtySubmitResult::get_dirty_bounds(
  self : DirtySubmitResult,
) -> PixelRect? {
  self.submit_plan.get_dirty_bounds()
}

///|
pub fn DirtySubmitResult::get_state(
  self : DirtySubmitResult,
) -> DirtySubmitState {
  self.submit_plan.get_state()
}

///|
pub fn DirtySubmitResult::has_dirty(self : DirtySubmitResult) -> Bool {
  self.submit_plan.has_dirty()
}

///|
pub fn DirtySubmitResult::needs_present(self : DirtySubmitResult) -> Bool {
  self.submit_plan.needs_present()
}

///|
pub fn DirtySubmitResult::was_clipped_away(self : DirtySubmitResult) -> Bool {
  self.get_state() == DirtyClippedAway
}

///|
pub fn DirtySubmitResult::uses_bounds_fallback(
  self : DirtySubmitResult,
) -> Bool {
  self.submit_plan.uses_bounds_fallback()
}

///|
pub fn DirtySubmitResult::did_present(self : DirtySubmitResult) -> Bool {
  self.presented_rect_count > 0
}

///|
pub fn DirtySubmitResult::get_presented_rect_count(
  self : DirtySubmitResult,
) -> Int {
  self.presented_rect_count
}

///|
pub fn DirtySubmitResult::get_present_pixel_count(
  self : DirtySubmitResult,
) -> Int {
  self.submit_plan.get_present_pixel_count()
}

///|
pub fn DirtySubmitResult::get_present_byte_count(
  self : DirtySubmitResult,
) -> Int {
  self.submit_plan.get_present_byte_count()
}

///|
pub fn DirtySubmitResult::get_present_bounds(
  self : DirtySubmitResult,
) -> PixelRect? {
  self.submit_plan.get_present_bounds()
}

///|
pub fn DirtySubmitResult::saves_full_present_pixels(
  self : DirtySubmitResult,
  width : Int,
  height : Int,
) -> Bool {
  self.submit_plan.saves_full_present_pixels(width, height)
}

///|
pub fn DirtySubmitResult::get_present_strategy(
  self : DirtySubmitResult,
  width : Int,
  height : Int,
) -> DirtyPresentStrategy {
  self.submit_plan.get_present_strategy(width, height)
}

///|
pub fn PixelRect::new(
  x~ : Int,
  y~ : Int,
  width~ : Int,
  height~ : Int,
) -> PixelRect {
  { x, y, width, height, }
}

///|
pub fn PixelRect::right(self : PixelRect) -> Int {
  self.x + self.width
}

///|
pub fn PixelRect::bottom(self : PixelRect) -> Int {
  self.y + self.height
}

///|
pub fn PixelRect::is_empty(self : PixelRect) -> Bool {
  self.width <= 0 || self.height <= 0
}

///|
pub fn PixelRect::area(self : PixelRect) -> Int {
  if self.is_empty() {
    return 0
  }
  self.width * self.height
}

///|
pub fn PixelRect::contains(self : PixelRect, x : Int, y : Int) -> Bool {
  !self.is_empty() &&
  x >= self.x &&
  x < self.right() &&
  y >= self.y &&
  y < self.bottom()
}

///|
pub fn PixelRect::intersects(self : PixelRect, other : PixelRect) -> Bool {
  match self.intersection(other) {
    Some(_) => true
    None => false
  }
}

///|
pub fn PixelRect::intersection(
  self : PixelRect,
  other : PixelRect,
) -> PixelRect? {
  let left = self.x.max(other.x)
  let top = self.y.max(other.y)
  let right = self.right().min(other.right())
  let bottom = self.bottom().min(other.bottom())
  let width = right - left
  let height = bottom - top
  guard width > 0 && height > 0 else { return None }
  Some(PixelRect::new(x=left, y=top, width~, height~))
}

///|
pub fn PixelRect::union(self : PixelRect, other : PixelRect) -> PixelRect {
  if self.is_empty() {
    return other
  }
  if other.is_empty() {
    return self
  }
  let left = self.x.min(other.x)
  let top = self.y.min(other.y)
  let right = self.right().max(other.right())
  let bottom = self.bottom().max(other.bottom())
  PixelRect::new(x=left, y=top, width=right - left, height=bottom - top)
}

///|
pub fn PixelRect::clip_to(self : PixelRect, bounds : PixelRect) -> PixelRect? {
  self.intersection(bounds)
}

///|
fn pixel_rect_array_bounds(rects : Array[PixelRect]) -> PixelRect? {
  let mut has_bounds = false
  let mut bounds = PixelRect::new(x=0, y=0, width=0, height=0)
  for rect in rects {
    if !rect.is_empty() {
      if has_bounds {
        bounds = bounds.union(rect)
      } else {
        bounds = rect
        has_bounds = true
      }
    }
  }
  if has_bounds {
    Some(bounds)
  } else {
    None
  }
}

///|
fn copy_pixel_rects(rects : Array[PixelRect]) -> Array[PixelRect] {
  let copied = []
  for rect in rects {
    copied.push(rect)
  }
  copied
}

///|
fn pixel_rects_can_merge(a : PixelRect, b : PixelRect) -> Bool {
  guard !a.is_empty() && !b.is_empty() else { return false }
  if a.intersects(b) {
    return true
  }
  let horizontal_touch = a.right() == b.x || b.right() == a.x
  let vertical_touch = a.bottom() == b.y || b.bottom() == a.y
  (horizontal_touch && ranges_overlap(a.y, a.bottom(), b.y, b.bottom())) ||
  (vertical_touch && ranges_overlap(a.x, a.right(), b.x, b.right()))
}

///|
fn ranges_overlap(
  a_start : Int,
  a_end : Int,
  b_start : Int,
  b_end : Int,
) -> Bool {
  a_start.max(b_start) < a_end.min(b_end)
}

///|
/// Accumulates pixel-aligned dirty rectangles for incremental rendering.
pub struct DirtyRegion {
  mut rects : Array[PixelRect]
} derive(Debug)

///|
pub extend DirtyRegion with Debug::{to_repr}

///|
pub fn DirtyRegion::new() -> DirtyRegion {
  { rects: [], }
}

///|
pub fn DirtyRegion::is_empty(self : DirtyRegion) -> Bool {
  self.rects.length() == 0
}

///|
pub fn DirtyRegion::clear(self : DirtyRegion) -> Unit {
  self.rects = []
}

///|
pub fn DirtyRegion::get_rect_count(self : DirtyRegion) -> Int {
  self.rects.length()
}

///|
pub fn DirtyRegion::get_rects(self : DirtyRegion) -> Array[PixelRect] {
  copy_pixel_rects(self.rects)
}

///|
pub fn DirtyRegion::add_rect(self : DirtyRegion, rect : PixelRect) -> Unit {
  guard !rect.is_empty() else { return }
  let mut merged = rect
  let mut pending = self.rects
  let mut changed = true
  while changed {
    changed = false
    let remaining = []
    for existing in pending {
      if pixel_rects_can_merge(merged, existing) {
        merged = merged.union(existing)
        changed = true
      } else {
        remaining.push(existing)
      }
    }
    pending = remaining
  }
  pending.push(merged)
  self.rects = pending
}

///|
pub fn DirtyRegion::add_clipped_rect(
  self : DirtyRegion,
  rect : PixelRect,
  bounds : PixelRect,
) -> Unit {
  match rect.clip_to(bounds) {
    Some(clipped) => self.add_rect(clipped)
    None => ()
  }
}

///|
pub fn DirtyRegion::add_all(
  self : DirtyRegion,
  width : Int,
  height : Int,
) -> Unit {
  self.add_rect(PixelRect::new(x=0, y=0, width~, height~))
}

///|
pub fn DirtyRegion::bounds(self : DirtyRegion) -> PixelRect? {
  pixel_rect_array_bounds(self.rects)
}

///|
pub fn DirtyRegion::present_rects(
  self : DirtyRegion,
  bounds : PixelRect,
  max_rects : Int,
) -> Array[PixelRect] {
  self.present_plan(bounds, max_rects).get_rects()
}

///|
pub fn DirtyRegion::present_plan(
  self : DirtyRegion,
  bounds : PixelRect,
  max_rects : Int,
) -> DirtyPresentPlan {
  let clipped = []
  for rect in self.rects {
    match rect.clip_to(bounds) {
      Some(value) => clipped.push(value)
      None => ()
    }
  }
  guard clipped.length() > 0 else { return { mode: Empty, rects: [], } }
  guard max_rects <= 0 || clipped.length() <= max_rects else {
    let region = DirtyRegion::new()
    for rect in clipped {
      region.add_rect(rect)
    }
    match region.bounds() {
      Some(value) => { mode: Bounds, rects: [value], }
      None => { mode: Empty, rects: [], }
    }
  }
  { mode: Rects, rects: clipped, }
}