///|
/// Provider-owned immutable document content for one URI/revision.
pub struct DocumentSnapshot {
  uri : @base_common.Uri
  display_name : String
  language_id : String
  revision : String
  text : String
  priv line_starts : Array[Int32]
} derive(Debug)

///|
/// Provider read result without exceptions.
pub(all) enum DocumentReadResult {
  DocumentLoaded(DocumentSnapshot)
  DocumentNotFound(@base_common.Uri)
  DocumentReadFailed(DocumentError)
} derive(Debug)

///|
/// Push notification from a provider watch or host-owned watcher.
pub(all) enum DocumentChange {
  DocumentInvalidated(uri~ : @base_common.Uri, known_revision~ : String?)
  DocumentUpdated(DocumentSnapshot)
  DocumentDeleted(@base_common.Uri)
  DocumentChangeFailed(uri~ : @base_common.Uri, error~ : DocumentError)
} derive(Debug)

///|
/// Document source contract for the readonly viewer. Providers may coalesce
/// watch updates and can either invalidate a URI or provide a fresh snapshot.
pub(open) trait DocumentProvider {
  async fn read(Self, @base_common.Uri) -> DocumentReadResult
  fn watch(Self, @base_common.Uri, (DocumentChange) -> Unit) -> @base_common.Disposable
  fn close(Self, @base_common.Uri) -> Unit
}

///|
/// Builds a document error, defaulting an empty message to the code label.
pub fn DocumentError::DocumentError(
  code : FileSystemProviderErrorCode,
  message : String,
  uri? : @base_common.Uri,
) -> DocumentError {
  let message = if message == "" { code.to_string() } else { message }
  { code, message, uri, }
}

///|
/// Converts a lower-level filesystem provider failure into a document error.
fn DocumentError::from_file_system_error(
  error : FileSystemProviderError,
) -> DocumentError {
  { code: error.code, message: error.message, uri: error.uri, }
}

///|
/// Builds an immutable raw provider snapshot. EOL bytes/code units are kept
/// exactly as supplied; normalization begins only when a Viewer `TextModel`
/// constructs its `TextSnapshot`.
pub fn DocumentSnapshot::DocumentSnapshot(
  uri : @base_common.Uri,
  display_name : String,
  language_id : String,
  revision : String,
  text : String,
) -> DocumentSnapshot {
  let display_name = if display_name == "" {
    @base_common.basename_or_authority(uri)
  } else {
    display_name
  }
  let language_id = if language_id == "" {
    infer_language_id(display_name)
  } else {
    language_id
  }
  let revision = if revision == "" {
    revision_from_text(text)
  } else {
    revision
  }
  {
    uri,
    display_name,
    language_id,
    revision,
    text,
    line_starts: build_line_starts(text),
  }
}

///|
/// Snapshot length in the repo's JavaScript-compatible UTF-16 indexing model.
fn DocumentSnapshot::length_impl(self : DocumentSnapshot) -> Int32 {
  self.text.length()
}

///|
/// Number of logical lines, including the final empty line after a trailing
/// newline.
fn DocumentSnapshot::line_count_impl(self : DocumentSnapshot) -> Int32 {
  self.line_starts.length()
}

///|
/// Start offset for `line`, clamped into the snapshot's line range.
fn DocumentSnapshot::line_start_offset_impl(
  self : DocumentSnapshot,
  line : Int32,
) -> Int32 {
  let line = line.clamp(min=0, max=self.line_count() - 1)
  self.line_starts[line]
}

///|
/// End offset for `line`, clamped into the snapshot's line range and excluding
/// the line break. CRLF lines exclude both the `\r` and `\n`.
fn DocumentSnapshot::line_end_offset_impl(
  self : DocumentSnapshot,
  line : Int32,
) -> Int32 {
  let line = line.clamp(min=0, max=self.line_count() - 1)
  let raw_end = if line + 1 < self.line_count() {
    self.line_starts[line + 1] - 1
  } else {
    self.length()
  }
  if raw_end > self.line_starts[line] && self.text[raw_end - 1].to_int() == 13 {
    raw_end - 1
  } else {
    raw_end
  }
}

///|
/// Returns a clamped half-open slice of the snapshot text.
fn DocumentSnapshot::slice_impl(
  self : DocumentSnapshot,
  range : @base_common.OffsetRange,
) -> String {
  let start = range.start.clamp(min=0, max=self.length())
  let end = range.end_exclusive.clamp(min=start, max=self.length())
  self.text[start:end].to_owned()
}

///|
/// Zero-based line containing `offset`, with out-of-range offsets clamped to
/// the snapshot.
fn DocumentSnapshot::line_at_offset_impl(
  self : DocumentSnapshot,
  offset : Int32,
) -> Int32 {
  let offset = offset.clamp(min=0, max=self.length())
  for i in 0.. @base_common.Position {
  let offset = offset.clamp(min=0, max=self.length())
  let line = self.line_at_offset(offset)
  { line_number: line + 1, column: offset - self.line_starts[line] + 1, }
}

///|
/// Converts a one-based line/column `Position` to a clamped document offset,
/// undoing the +1 this boundary added in `position_at_offset`.
fn DocumentSnapshot::offset_at_position_impl(
  self : DocumentSnapshot,
  position : @base_common.Position,
) -> Int32 {
  let line = (position.line_number - 1).clamp(min=0, max=self.line_count() - 1)
  let start = self.line_start_offset(line)
  let end = self.line_end_offset(line)
  (start + position.column - 1).clamp(min=start, max=end)
}

///|
/// JSON shape emitted to browser harnesses and remote protocol adapters.
pub impl ToJson for DocumentSnapshot with fn to_json(self) {
  {
    "uri": Json(self.uri.to_string()),
    "displayName": Json(self.display_name),
    "languageId": Json(self.language_id),
    "revision": Json(self.revision),
    "text": Json(self.text),
  }
}

///|
/// JSON shape for document errors.
pub impl ToJson for DocumentError with fn to_json(self) {
  let fields : Map[String, Json] = {
    "code": Json(self.code.to_string()),
    "message": Json(self.message),
    "status": Json(self.status()),
  }
  if self.uri is Some(uri) {
    fields["uri"] = Json(uri.to_string())
    fields["displayName"] = Json(@base_common.basename_or_authority(uri))
  }
  Json(fields)
}

///|
/// HTTP-like status used by the reference server when serializing document
/// failures.
fn DocumentError::status(self : DocumentError) -> Int32 {
  self.to_file_system_error().status()
}

///|
/// Offsets where each line begins in the raw provider text (0-based). Mirrors
/// Monaco's raw `createLineStartsFast` scan (`pieceTreeBase.ts:37`): `\n`, a
/// lonely `\r`, and `\r\n` each end a line, with the pair counted once. This
/// scanner intentionally differs from Viewer `TextSnapshot`, which first
/// normalizes CRLF/CR to LF and then derives its own coordinate table.
fn build_line_starts(text : String) -> Array[Int32] {
  let starts = [0]
  let len = text.length()
  let mut i = 0
  while i < len {
    let code = text[i].to_int()
    if code == 13 {
      if i + 1 < len && text[i + 1].to_int() == 10 {
        // \r\n... case
        starts.push(i + 2)
        i += 2
      } else {
        // \r... case
        starts.push(i + 1)
        i += 1
      }
    } else {
      if code == 10 {
        starts.push(i + 1)
      }
      i += 1
    }
  }
  starts
}

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

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

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

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

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