///|
priv struct LspDocument {
  text : String
  version : Int
  opened : Bool
}

///|
/// Transport-independent LSP 3.17 engine. Files enter only through client messages.
struct LspServer {
  documents : Map[String, LspDocument]
  workspace_files : Map[String, String]
  mut initialized : Bool
  mut shutting_down : Bool
  mut exiting : Bool
  mut literal : Bool
}

///|
pub fn new_lsp_server() -> LspServer {
  {
    documents: Map([]),
    workspace_files: Map([]),
    initialized: false,
    shutting_down: false,
    exiting: false,
    literal: false,
  }
}

///|
pub fn LspServer::should_exit(self : LspServer) -> Bool {
  self.exiting
}

///|
pub fn LspServer::exit_code(self : LspServer) -> Int {
  if self.shutting_down {
    0
  } else {
    1
  }
}

///|
fn lsp_string(fields : Map[String, Json], key : String) -> String raise {
  guard fields.get(key) is Some(String(text)) else {
    raise Failure("Expected string field " + key)
  }
  text
}

///|
fn lsp_object(
  fields : Map[String, Json],
  key : String,
) -> Map[String, Json] raise {
  guard fields.get(key) is Some(Object(object)) else {
    raise Failure("Expected object field " + key)
  }
  object
}

///|
fn lsp_int(fields : Map[String, Json], key : String) -> Int raise {
  guard fields.get(key) is Some(Number(n, ..)) &&
    n >= -2147483648.0 &&
    n <= 2147483647.0 &&
    n == n.to_int().to_double() else {
    raise Failure("Expected integer " + key)
  }
  n.to_int()
}

///|
pub fn lsp_position(text : String, offset : Int) -> Json {
  let chars = text.to_array()
  let mut line = 0
  let mut column = 0
  for i in 0.. 65535 { 2 } else { 1 }
    }
  }
  Json::object({
    "line": Json::number(line.to_double()),
    "character": Json::number(column.to_double()),
  })
}

///|
fn lsp_offset(text : String, position : Map[String, Json]) -> Int raise {
  let wanted_line = lsp_int(position, "line")
  let wanted_column = lsp_int(position, "character")
  if wanted_line < 0 || wanted_column < 0 {
    raise Failure("Negative LSP position")
  }
  let chars = text.to_array()
  let mut line = 0
  let mut column = 0
  let mut i = 0
  while i <= chars.length() {
    if line == wanted_line && column == wanted_column {
      return i
    }
    if i == chars.length() {
      break
    }
    let c = chars[i]
    if c == '\r' || c == '\n' {
      if line == wanted_line {
        raise Failure("Position exceeds line length")
      }
      if c == '\r' && chars.get(i + 1) == Some('\n') {
        i += 1
      }
      line += 1
      column = 0
    } else {
      column += if c.to_int() > 65535 { 2 } else { 1 }
      if line == wanted_line && column > wanted_column {
        raise Failure("Position splits a Unicode surrogate pair")
      }
    }
    i += 1
  }
  raise Failure("Position exceeds document")
}

///|
fn lsp_range(text : String, start : Int, end : Int) -> Json {
  Json::object({
    "start": lsp_position(text, start),
    "end": lsp_position(text, end),
  })
}

///|
fn lsp_virtual_uri(uri : String) -> String raise {
  if !uri.has_prefix("file://") {
    raise Failure("References require file:// URIs")
  }
  let raw = uri[7:].to_owned()
  let chars = raw.to_array()
  let output = @buffer.Buffer()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' {
      guard i + 2 < chars.length() &&
        kv3_hex(chars[i + 1]) is Some(a) &&
        kv3_hex(chars[i + 2]) is Some(b) else {
        raise Failure("Invalid URI encoding")
      }
      output.write_byte((a * 16 + b).to_byte())
      i += 3
    } else {
      output.write_bytes(@utf8.encode(chars[i].to_string()[:])[:])
      i += 1
    }
  }
  let decoded = @utf8.decode(output.to_bytes()[:])
  if decoded.contains("\u0000") {
    raise Failure("NUL in URI")
  }
  normalize_reference(decoded)
}

///|
fn LspServer::put(
  self : LspServer,
  uri : String,
  document : LspDocument,
) -> Unit raise {
  if uri.length() > 32768 || uri.contains("\u0000") {
    raise Failure("Invalid document URI")
  }
  if !self.documents.contains(uri) && self.documents.length() >= 64 {
    raise Failure("Workspace exceeds 64 documents")
  }
  let mut total = document.text.char_length()
  for name, previous in self.documents {
    if name != uri {
      total += previous.text.char_length()
    }
  }
  if total > 16777216 || document.text.char_length() > 8388608 {
    raise Failure("LSP document character budget exceeded")
  }
  self.documents[uri] = document
}

///|
fn lsp_is_kv3(uri : String, text : String) -> Bool {
  uri.to_lower().has_suffix(".kv3") ||
  text.trim_start().to_owned().has_prefix("