///|
/// A position in the editor, the viewer-used subset of Monaco/VS Code's `Position`
/// (`vs/editor/common/core/position.ts`). One-based on both axes:
/// `line_number` starts at 1 and `column` starts at 1 (the first character on a
/// line sits between column 1 and column 2). Field names use the repo's
/// snake_case convention (`line_number`), matching `Range::start_line_number`.
pub(all) struct Position {
  line_number : Int32
  column : Int32
} derive(Eq, Debug)

///|
/// A range of offsets (0-based), half-open `[start, end_exclusive)`. A subset
/// port of Monaco's `OffsetRange` (`core/ranges/offsetRange.ts`) containing the
/// operations used by the viewer; field names use the repo's snake_case
/// (`end_exclusive`). The name `Range` is reserved for
/// Monaco's line/column range (see `monaco_range.mbt`).
pub struct OffsetRange {
  start : Int32
  end_exclusive : Int32
} derive(Eq, Debug)

///|
/// Builds a position without normalization; callers decide the coordinate
/// space before conversion through a snapshot.
pub fn Position::Position(line_number : Int32, column : Int32) -> Position {
  { line_number, column, }
}

///|
/// Whether this position equals `other`. Mirrors Monaco's `Position.equals`.
pub fn Position::equals(self : Position, other : Position) -> Bool {
  self.line_number == other.line_number && self.column == other.column
}

///|
/// Whether this position is strictly before `other` (equal positions return
/// false). Mirrors Monaco's `Position.isBefore`.
pub fn Position::is_before(self : Position, other : Position) -> Bool {
  if self.line_number < other.line_number {
    return true
  }
  if other.line_number < self.line_number {
    return false
  }
  self.column < other.column
}

///|
/// Whether this position is before or equal to `other`. Mirrors Monaco's
/// `Position.isBeforeOrEqual`.
pub fn Position::is_before_or_equal(self : Position, other : Position) -> Bool {
  if self.line_number < other.line_number {
    return true
  }
  if other.line_number < self.line_number {
    return false
  }
  self.column <= other.column
}

///|
/// Builds a half-open offset range. Monaco's constructor throws a
/// `BugIndicatingError` when `start > end_exclusive`; the readonly viewer avoids
/// panics, so this normalizes by swapping instead. For valid inputs
/// (`start <= end_exclusive`) the swap is a no-op, so every other method keeps
/// Monaco's behavior.
pub fn OffsetRange::OffsetRange(
  start : Int32,
  end_exclusive : Int32,
) -> OffsetRange {
  if start <= end_exclusive {
    { start, end_exclusive, }
  } else {
    { start: end_exclusive, end_exclusive: start, }
  }
}

///|
/// `[start, end_exclusive)`. Mirrors Monaco's static `OffsetRange.fromTo`.
pub fn OffsetRange::from_to(
  start : Int32,
  end_exclusive : Int32,
) -> OffsetRange {
  OffsetRange(start, end_exclusive)
}

///|
/// `[0, length)`. Mirrors Monaco's static `OffsetRange.ofLength`.
pub fn OffsetRange::of_length(length : Int32) -> OffsetRange {
  OffsetRange(0, length)
}

///|
/// The empty range `[offset, offset)`. Mirrors Monaco's `OffsetRange.emptyAt`.
pub fn OffsetRange::empty_at(offset : Int32) -> OffsetRange {
  OffsetRange(offset, offset)
}

///|
/// Whether the range contains no offsets. Mirrors Monaco's `isEmpty` getter.
pub fn OffsetRange::is_empty(self : OffsetRange) -> Bool {
  self.start == self.end_exclusive
}

///|
/// The number of offsets in the range. Mirrors Monaco's `length` getter.
pub fn OffsetRange::length(self : OffsetRange) -> Int32 {
  self.end_exclusive - self.start
}

///|
/// Whether this range equals `other`. Mirrors Monaco's `equals`.
pub fn OffsetRange::equals(self : OffsetRange, other : OffsetRange) -> Bool {
  self.start == other.start && self.end_exclusive == other.end_exclusive
}

///|
/// Whether `offset` falls inside the half-open range. Mirrors Monaco's
/// `contains`.
pub fn OffsetRange::contains(self : OffsetRange, offset : Int32) -> Bool {
  self.start <= offset && offset < self.end_exclusive
}

///|
/// Inserts `range` into `sorted_ranges`, merging it with any ranges it touches
/// or overlaps so the array stays sorted and non-overlapping. Mirrors Monaco's
/// static `OffsetRange.addRange`.
pub fn OffsetRange::add_range(
  range : OffsetRange,
  sorted_ranges : Array[OffsetRange],
) -> Unit {
  let i = for i = 0; i < sorted_ranges.length() &&
             sorted_ranges[i].end_exclusive < range.start; {
    continue i + 1
  } nobreak {
    i
  }
  let j = for j = i; j < sorted_ranges.length() &&
             sorted_ranges[j].start <= range.end_exclusive; {
    continue j + 1
  } nobreak {
    j
  }
  if i == j {
    sorted_ranges.insert(i, range)
  } else {
    let start = range.start.min(sorted_ranges[i].start)
    let end = range.end_exclusive.max(sorted_ranges[j - 1].end_exclusive)
    for _ in 0..<(j - i) {
      sorted_ranges.remove(i) |> ignore
    }
    sorted_ranges.insert(i, OffsetRange(start, end))
  }
}

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

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

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

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