///|
/// Monaco's `VerticalRevealType` (`vs/editor/common/viewEvents.ts`): how a
/// reveal aligns the target range inside the viewport.
pub(all) enum VerticalRevealType {
  Simple
  Center
  CenterIfOutsideViewport
  NearTop
  NearTopIfOutsideViewport
  Top
  Bottom
} derive(Eq, Debug)

///|
/// The minimal `scroll_top` that brings the 1-based **view**-line range
/// `[start_line, end_line]` into the viewport, aligned per `vertical_type`.
/// A 1:1 port of Monaco's `ViewLines._computeScrollTopToRevealRange`
/// (`vs/editor/browser/viewParts/viewLines/viewLines.ts:693`).
///
/// Pure model state: works entirely in view coordinates, so it stays
/// `moon test`-able without a browser. Model→view conversion happens at the
/// `Viewer` boundary before this is called (with word-wrap on, model line ≠
/// view line).
///
/// Scope cuts vs. Monaco (all confirmed absent in the viewer):
/// - No `cursorSurroundingLines` / sticky-scroll scroll-off padding, so
///   `paddingTop = paddingBottom = 0` (`viewLines.ts:720`–`:736`). Only the
///   behavioral `+1 line` bottom pad for `Simple` / `Bottom` survives
///   (`:737`–`:742`) only when `minimal_reveal=false`. Mouse-source regular
///   reveals add the source's one-line top pad; minimal cursor reveals add no
///   top/bottom padding.
/// - No multi-selection. The box is always a single range, so an oversized box
///   reveals its top (Monaco's single-range branch, `:754`) instead of returning
///   the `-1` abort sentinel — that sentinel is reserved for the oversized
///   multi-cursor case the viewer never exposes. This port therefore does not
///   produce `-1.0`; `Viewer::send_reveal_range` still guards on it defensively.
///
/// The returned value is unclamped (it may be negative, e.g. a `NearTop` reveal
/// near the document top); `Scrollable::set_position` clamps it into the valid
/// scroll range, mirroring Monaco's `validateScrollPosition` step.
pub fn ViewLayout::compute_scroll_top_to_reveal_range(
  self : ViewLayout,
  start_line : Int32,
  end_line : Int32,
  vertical_type : VerticalRevealType,
  source~ : String = "",
  minimal_reveal~ : Bool = false,
) -> Double {
  // The source uses the future viewport so consecutive animated reveal
  // requests compose against their already-requested destination.
  let viewport_start_y = self.future_position().scroll_top
  let viewport_height = self.dimensions().height
  let viewport_end_y = viewport_start_y + viewport_height
  let line_height = self.line_height.to_double()
  let mut box_start_y = self.vertical_offset_for_line(start_line)
  let mut box_end_y = self.vertical_offset_for_line(end_line) + line_height
  let should_ignore_scroll_off = source == "mouse" || minimal_reveal
  if should_ignore_scroll_off && !minimal_reveal {
    // Reveal one more line above for a non-minimal mouse drag.
    box_start_y -= line_height
  }
  if !minimal_reveal {
    match vertical_type {
      Simple | Bottom => box_end_y += line_height
      _ => ()
    }
  }
  if box_end_y - box_start_y > viewport_height {
    // The box is larger than the viewport: a single range, so reveal its top.
    box_start_y
  } else {
    match vertical_type {
      NearTop | NearTopIfOutsideViewport =>
        if vertical_type is NearTopIfOutsideViewport &&
          viewport_start_y <= box_start_y &&
          box_end_y <= viewport_end_y {
          // Box is already in the viewport... do nothing.
          viewport_start_y
        } else {
          // A gap that is 20% of the viewport, with a minimum of 5 lines.
          let desired_gap_above = (5.0 * line_height).max(viewport_height * 0.2)
          // Scroll just above the box with the desired gap, but never push the
          // box out of the viewport.
          let desired_scroll_top = box_start_y - desired_gap_above
          let min_scroll_top = box_end_y - viewport_height
          min_scroll_top.max(desired_scroll_top)
        }
      Center | CenterIfOutsideViewport =>
        if vertical_type is CenterIfOutsideViewport &&
          viewport_start_y <= box_start_y &&
          box_end_y <= viewport_end_y {
          // Box is already in the viewport... do nothing.
          viewport_start_y
        } else {
          // Box is outside the viewport... center it.
          let box_middle_y = (box_start_y + box_end_y) / 2.0
          0.0.max(box_middle_y - viewport_height / 2.0)
        }
      _ =>
        compute_minimum_scrolling(
          viewport_start_y,
          viewport_end_y,
          box_start_y,
          box_end_y,
          reveal_at_start=vertical_type is Top,
          reveal_at_end=vertical_type is Bottom,
        )
    }
  }
}

///|
/// Monaco's `ViewLines.HORIZONTAL_EXTRA_PX` (`viewLines.ts:101`): extra room
/// revealed to the left of a horizontal box so the target is not flush against
/// the viewport edge.
let horizontal_extra_px : Double = 30.0

///|
/// Monaco's `EditorOption.revealHorizontalRightPadding` default (`editorOptions.ts`
/// `EditorIntOption(..., 15, 0, 1000)`): extra room revealed to the right of a
/// horizontal box.
let reveal_horizontal_right_padding : Double = 15.0

///|
/// The minimal `scroll_left` that brings a horizontal box into view, the pure
/// half of Monaco's `ViewLines._computeScrollLeftToReveal` (`viewLines.ts:784`).
/// The DOM measurement (`measure_line_selection`) lives on `Viewer`; `ranges`
/// are its `(left, width)` client rects in content coordinates (0 = line start).
/// Returns `None` when there is nothing to measure (the line is not rendered, or
/// the range collapsed to a zero-width caret the measurement helper drops).
///
/// `vertical_scrollbar_width` is passed in because the scrollbar geometry is a
/// rendering concern owned by `Viewer`, not by the pure layout core.
pub fn ViewLayout::compute_scroll_left_to_reveal(
  self : ViewLayout,
  ranges : Array[(Double, Double)],
  vertical_scrollbar_width~ : Double,
  minimal_reveal~ : Bool = false,
) -> Double? {
  guard ranges is [(first_left, first_width), ..] else { return None }
  let (box_start_x, box_end_x) = for
    range in ranges[1:]
    box_start_x = first_left.round(),
    box_end_x = (first_left + first_width).round() {
    let (left, width) = range
    continue box_start_x.min(left.round()),
      box_end_x.max((left + width).round())
  } nobreak {
    (box_start_x, box_end_x)
  }
  let (box_start_x, box_end_x) = if minimal_reveal {
    (box_start_x, box_end_x)
  } else {
    (
      0.0.max(box_start_x - horizontal_extra_px),
      box_end_x + reveal_horizontal_right_padding,
    )
  }
  // Horizontal measurement uses the current viewport; only the vertical
  // reveal composition above consults the future animated destination.
  let viewport_start_x = self.position().scroll_left
  let viewport_end_x = viewport_start_x +
    self.dimensions().width -
    vertical_scrollbar_width
  Some(
    compute_minimum_scrolling(
      viewport_start_x, viewport_end_x, box_start_x, box_end_x,
    ),
  )
}

///|
/// The minimal scroll that fits a box into a viewport, Monaco's shared
/// `ViewLines._computeMinimumScrolling` (`viewLines.ts:838`), used by both the
/// vertical and horizontal reveal math. `reveal_at_start` / `reveal_at_end`
/// force the box to the viewport's leading / trailing edge (the `Top` / `Bottom`
/// reveal types).
///
/// Replicates Monaco's `| 0` integer truncation of all four pixel inputs for
/// pixel-parity: MoonBit's `Double::to_int` truncates toward zero like JS `| 0`
/// across the 32-bit scroll range the viewer operates in.
fn compute_minimum_scrolling(
  viewport_start : Double,
  viewport_end : Double,
  box_start : Double,
  box_end : Double,
  reveal_at_start~ : Bool = false,
  reveal_at_end~ : Bool = false,
) -> Double {
  let viewport_start = viewport_start.to_int().to_double()
  let viewport_end = viewport_end.to_int().to_double()
  let box_start = box_start.to_int().to_double()
  let box_end = box_end.to_int().to_double()
  let viewport_length = viewport_end - viewport_start
  let box_length = box_end - box_start
  if box_length < viewport_length {
    // The box would fit in the viewport.
    if reveal_at_start {
      box_start
    } else if reveal_at_end {
      0.0.max(box_end - viewport_length)
    } else if box_start < viewport_start {
      // The box is above the viewport.
      box_start
    } else if box_end > viewport_end {
      // The box is below the viewport.
      0.0.max(box_end - viewport_length)
    } else {
      viewport_start
    }
  } else {
    // The box would not fit in the viewport: reveal the beginning of the box.
    box_start
  }
}

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

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