///|
/// Rejects malformed backend-independent stack values at their construction
/// boundary, before navigation can depend on physical ordering.
pub(all) suberror StackSnapshotError {
  InvalidLogicalThreadIndex(Int)
  InvalidPhysicalFrameIndex(Int)
  InvalidStackWindow(start~ : Int, count~ : Int)
  InvalidLoadedFrameRange(start~ : Int, count~ : Int)
  LoadedFrameCountMismatch(expected~ : Int, actual~ : Int)
  UnexpectedPhysicalFrameIndex(expected~ : Int, actual~ : Int)
  CompleteStackStartsAfterZero(Int)
  StackExtensionThreadMismatch(expected~ : Int, actual~ : Int)
  StackExtensionMustStartAtZero(Int)
  StackExtensionWentBackwards(previous~ : Int, next~ : Int)
  StackExtensionChangedFrame(Int)
  StackExtensionMadeNoProgress(Int)
} derive(Debug, Eq)

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

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

///|
/// A zero-based logical thread selection inside the current stable stop.
/// Backend-specific thread identifiers never appear in this value.
pub struct ThreadSelection {
  priv index : Int
} derive(Debug, Eq)

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

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

///|
pub fn ThreadSelection::initial() -> ThreadSelection {
  { index: 0, }
}

///|
pub fn ThreadSelection::at(
  index : Int,
) -> ThreadSelection raise StackSnapshotError {
  guard index >= 0 else { raise InvalidLogicalThreadIndex(index) }
  { index, }
}

///|
pub fn ThreadSelection::index(self : ThreadSelection) -> Int {
  self.index
}

///|
/// A zero-based physical frame selection inside the selected thread.
///
/// Hidden runtime/native frames keep their physical positions, so visible
/// frame numbers may contain gaps. Backend-specific stack-frame identifiers
/// never appear in this value.
pub struct FrameSelection {
  priv index : Int
} derive(Debug, Eq)

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

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

///|
pub fn FrameSelection::index(self : FrameSelection) -> Int {
  self.index
}

///|
fn FrameSelection::at(index : Int) -> FrameSelection {
  { index, }
}

///|
/// Application-level classification used by default stack filtering.
///
/// `UnknownFrame` deliberately preserves uncertainty when an adapter does not
/// provide enough information to distinguish runtime and native frames.
pub(all) enum FrameKind {
  MoonBitUserFrame
  MoonBitRuntimeFrame
  NativeFrame
  UnknownFrame
} derive(Debug, Eq)

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

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

///|
fn FrameKind::is_visible_by_default(self : FrameKind) -> Bool {
  self is MoonBitUserFrame
}

///|
/// A backend-independent frame description at one physical stack depth.
pub struct StackFrameSnapshot {
  priv physical_index : Int
  priv raw_name : String
  priv display_name : String
  priv linkage_name : String?
  priv source_path : String?
  priv line : Int?
  priv column : Int?
  priv kind : FrameKind
} derive(Debug, Eq)

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

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

///|
pub fn StackFrameSnapshot::new(
  physical_index : Int,
  raw_name : String,
  display_name : String,
  linkage_name : String?,
  source_path : String?,
  line : Int?,
  column : Int?,
  kind : FrameKind,
) -> StackFrameSnapshot raise StackSnapshotError {
  guard physical_index >= 0 else {
    raise InvalidPhysicalFrameIndex(physical_index)
  }
  {
    physical_index,
    raw_name,
    display_name,
    linkage_name,
    source_path,
    line,
    column,
    kind,
  }
}

///|
pub fn StackFrameSnapshot::physical_index(self : StackFrameSnapshot) -> Int {
  self.physical_index
}

///|
pub fn StackFrameSnapshot::raw_name(self : StackFrameSnapshot) -> String {
  self.raw_name
}

///|
pub fn StackFrameSnapshot::display_name(self : StackFrameSnapshot) -> String {
  self.display_name
}

///|
pub fn StackFrameSnapshot::linkage_name(self : StackFrameSnapshot) -> String? {
  self.linkage_name
}

///|
pub fn StackFrameSnapshot::source_path(self : StackFrameSnapshot) -> String? {
  self.source_path
}

///|
pub fn StackFrameSnapshot::line(self : StackFrameSnapshot) -> Int? {
  self.line
}

///|
pub fn StackFrameSnapshot::column(self : StackFrameSnapshot) -> Int? {
  self.column
}

///|
pub fn StackFrameSnapshot::kind(self : StackFrameSnapshot) -> FrameKind {
  self.kind
}

///|
/// A requested physical stack range. It is independent of DAP pagination and
/// can be satisfied from a backend cache.
pub struct StackWindow {
  priv start : Int
  priv count : Int
} derive(Debug, Eq)

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

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

///|
pub fn StackWindow::new(
  start : Int,
  count : Int,
) -> StackWindow raise StackSnapshotError {
  guard start >= 0 && count > 0 else {
    raise InvalidStackWindow(start~, count~)
  }
  { start, count, }
}

///|
pub fn StackWindow::initial() -> StackWindow {
  { start: 0, count: 20, }
}

///|
pub fn StackWindow::start(self : StackWindow) -> Int {
  self.start
}

///|
pub fn StackWindow::count(self : StackWindow) -> Int {
  self.count
}

///|
/// Result of moving through frames visible under the default filter.
pub enum FrameNavigationResult {
  FrameAvailable(FrameSelection)
  MoreFramesRequired(start_frame~ : Int)
  VisibleFrameBoundary
} derive(Debug, Eq)

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

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

///|
/// A possibly partial stack snapshot for one logical thread.
///
/// The loaded range is `[loaded_start, loaded_start + loaded_count)`. A
/// `total_frames_hint` is advisory only: adapters may report an unstable total.
/// `complete` is the backend's positive assertion that no physical frame lies
/// outside the loaded range.
pub struct StackSnapshot {
  priv thread : ThreadSelection
  priv frames : Array[StackFrameSnapshot]
  priv loaded_start : Int
  priv loaded_count : Int
  priv total_frames_hint : Int?
  priv complete : Bool
} derive(Debug, Eq)

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

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

///|
pub fn StackSnapshot::new(
  thread : ThreadSelection,
  frames : Array[StackFrameSnapshot],
  loaded_start : Int,
  loaded_count : Int,
  total_frames_hint : Int?,
  complete : Bool,
) -> StackSnapshot raise StackSnapshotError {
  guard loaded_start >= 0 && loaded_count >= 0 else {
    raise InvalidLoadedFrameRange(start=loaded_start, count=loaded_count)
  }
  guard frames.length() == loaded_count else {
    raise LoadedFrameCountMismatch(
      expected=loaded_count,
      actual=frames.length(),
    )
  }
  for offset, frame in frames {
    let expected = loaded_start + offset
    guard frame.physical_index() == expected else {
      raise UnexpectedPhysicalFrameIndex(
        expected~,
        actual=frame.physical_index(),
      )
    }
  }
  guard !complete || loaded_start == 0 else {
    raise CompleteStackStartsAfterZero(loaded_start)
  }
  {
    thread,
    frames: frames.copy(),
    loaded_start,
    loaded_count,
    total_frames_hint,
    complete,
  }
}

///|
pub fn StackSnapshot::thread(self : StackSnapshot) -> ThreadSelection {
  self.thread
}

///|
pub fn StackSnapshot::frames(
  self : StackSnapshot,
) -> ReadOnlyArray[StackFrameSnapshot] {
  ReadOnlyArray::from_array(self.frames)
}

///|
pub fn StackSnapshot::loaded_start(self : StackSnapshot) -> Int {
  self.loaded_start
}

///|
pub fn StackSnapshot::loaded_count(self : StackSnapshot) -> Int {
  self.loaded_count
}

///|
pub fn StackSnapshot::loaded_end(self : StackSnapshot) -> Int {
  self.loaded_start + self.loaded_count
}

///|
pub fn StackSnapshot::total_frames_hint(self : StackSnapshot) -> Int? {
  self.total_frames_hint
}

///|
pub fn StackSnapshot::is_complete(self : StackSnapshot) -> Bool {
  self.complete
}

///|
/// Validates a cumulative prefix returned after requesting a later window.
/// Existing physical frames are immutable, and an incomplete extension must
/// add at least one frame.
pub fn StackSnapshot::validate_extension(
  self : StackSnapshot,
  next : StackSnapshot,
) -> Unit raise StackSnapshotError {
  guard next.thread() == self.thread() else {
    raise StackExtensionThreadMismatch(
      expected=self.thread().index(),
      actual=next.thread().index(),
    )
  }
  guard next.loaded_start() == 0 else {
    raise StackExtensionMustStartAtZero(next.loaded_start())
  }
  guard next.loaded_count() >= self.loaded_count() else {
    raise StackExtensionWentBackwards(
      previous=self.loaded_count(),
      next=next.loaded_count(),
    )
  }
  for physical_index in 0.. self.loaded_count() || next.is_complete() else {
    raise StackExtensionMadeNoProgress(self.loaded_count())
  }
}

///|
pub fn StackSnapshot::is_loaded(
  self : StackSnapshot,
  physical_index : Int,
) -> Bool {
  physical_index >= self.loaded_start && physical_index < self.loaded_end()
}

///|
pub fn StackSnapshot::frame(
  self : StackSnapshot,
  physical_index : Int,
) -> StackFrameSnapshot? {
  for frame in self.frames {
    if frame.physical_index() == physical_index {
      return Some(frame)
    }
  }
  None
}

///|
pub fn StackSnapshot::visible_frames(
  self : StackSnapshot,
) -> Array[StackFrameSnapshot] {
  self.frames.filter(frame => frame.kind().is_visible_by_default())
}

///|
/// Chooses the nearest loaded MoonBit user frame, retaining its physical
/// index. A partial stack with no user frame remains unselected so its caller
/// can load the next window. Only a complete non-empty stack falls back to its
/// first frame for diagnostics.
pub fn StackSnapshot::default_selection(
  self : StackSnapshot,
) -> FrameSelection? {
  for frame in self.frames {
    if frame.kind().is_visible_by_default() {
      return Some(FrameSelection::at(frame.physical_index()))
    }
  }
  guard self.complete else { return None }
  guard self.frames.length() > 0 else { return None }
  Some(FrameSelection::at(self.frames[0].physical_index()))
}

///|
pub fn StackSnapshot::selection(
  self : StackSnapshot,
  physical_index : Int,
) -> FrameSelection? {
  self.frame(physical_index).map(_ => FrameSelection::at(physical_index))
}

///|
pub fn StackSnapshot::visible_caller(
  self : StackSnapshot,
  selection : FrameSelection,
) -> FrameNavigationResult {
  for frame in self.frames {
    if frame.physical_index() > selection.index() &&
      frame.kind().is_visible_by_default() {
      return FrameAvailable(FrameSelection::at(frame.physical_index()))
    }
  }
  if self.complete {
    VisibleFrameBoundary
  } else {
    MoreFramesRequired(start_frame=self.loaded_end())
  }
}

///|
pub fn StackSnapshot::visible_callee(
  self : StackSnapshot,
  selection : FrameSelection,
) -> FrameNavigationResult {
  let mut candidate : FrameSelection? = None
  for frame in self.frames {
    if frame.physical_index() >= selection.index() {
      break
    }
    if frame.kind().is_visible_by_default() {
      candidate = Some(FrameSelection::at(frame.physical_index()))
    }
  }
  match candidate {
    Some(frame) => FrameAvailable(frame)
    None if self.loaded_start > 0 => MoreFramesRequired(start_frame=0)
    None => VisibleFrameBoundary
  }
}

///|
pub struct StopSnapshot {
  priv reason : String
  priv function_name : String
  priv source_path : String?
  priv line : Int?
  priv column : Int?
}

///|
/// Creates an application-level stopped-location snapshot.
pub fn StopSnapshot::new(
  reason : String,
  function_name : String,
  source_path : String?,
  line : Int?,
  column : Int?,
) -> StopSnapshot {
  { reason, function_name, source_path, line, column, }
}

///|
pub fn StopSnapshot::reason(self : StopSnapshot) -> String {
  self.reason
}

///|
pub fn StopSnapshot::function_name(self : StopSnapshot) -> String {
  self.function_name
}

///|
pub fn StopSnapshot::source_path(self : StopSnapshot) -> String? {
  self.source_path
}

///|
pub fn StopSnapshot::line(self : StopSnapshot) -> Int? {
  self.line
}

///|
pub fn StopSnapshot::column(self : StopSnapshot) -> Int? {
  self.column
}

///|
pub enum RunOutcome {
  Stopped(StopSnapshot)
  Exited(Int?)
}

///|
pub fn RunOutcome::stopped(snapshot : StopSnapshot) -> RunOutcome {
  Stopped(snapshot)
}

///|
pub fn RunOutcome::exited(exit_code : Int?) -> RunOutcome {
  Exited(exit_code)
}