///|
fn kv3_symbol_kind(value : KV3Value) -> Int32 {
  match kv3_unflag(value) {
    KV3Object(_) => 19
    KV3Array(_) => 18
    KV3String(_) => 15
    KV3Integer(_) | KV3Float(_) => 16
    KV3Bool(_) => 17
    KV3Null => 21
    _ => 13
  }
}

///|
fn kv3_summary(value : KV3Value) -> String {
  let prefix = match value {
    KV3Flag(flag, _) => flag + ": "
    _ => ""
  }
  let text = match kv3_unflag(value) {
    KV3Object(entries) => "object (" + entries.length().to_string() + " fields)"
    KV3Array(values) => "array (" + values.length().to_string() + " elements)"
    KV3Blob(bytes) => "blob (" + bytes.length().to_string() + " bytes)"
    KV3String(s) =>
      "string: " +
      (if s.length() > 160 { s[:160].to_owned() + "..." } else { s })
    KV3Integer(n) => "integer: " + n
    KV3Float(n) =>
      "float: " +
      (if n.length() > 160 { n[:160].to_owned() + "..." } else { n })
    KV3Bool(b) => "bool: " + b.to_string()
    _ => "null"
  }
  prefix + text
}

///|
fn lsp_kv3_symbol(
  source : KV3SourceDocument,
  value : KV3Value,
  path : Array[Int32],
  name : String,
  coordinates : LspCoordinates,
  budget : Ref[Int32],
) -> Json {
  budget.val -= 1
  let span = source.spans[index_key(path)]
  let container = kv3_unflag(value) is KV3Object(_) ||
    kv3_unflag(value) is KV3Array(_)
  let (start, end) = source.keys
    .get(index_key(path))
    .unwrap_or(
      (
        if container {
          span.body
        } else {
          span.start
        },
        if container {
          span.body + 1
        } else {
          span.end
        },
      ),
    )
  let children : Array[Json] = []
  match kv3_unflag(value) {
    KV3Object(entries) =>
      for i in 0..
      for i in 0.. ()
  }
  Json::object({
    "name": Json::string(if name.is_empty() { "(empty key)" } else { name }),
    "detail": Json::string(kv3_summary(value)),
    "kind": Json::number(kv3_symbol_kind(value).to_double()),
    "range": coordinates.range(start, span.end),
    "selectionRange": coordinates.range(start, end),
    "children": Json::array(children),
  })
}

///|
fn kv3_path_names(root : KV3Value, indices : Array[Int32]) -> Array[String] {
  let names : Array[String] = []
  let mut value = root
  for i in indices {
    match kv3_unflag(value) {
      KV3Object(entries) => {
        names.push(entries[i].key)
        value = entries[i].value
      }
      KV3Array(values) => {
        names.push(i.to_string())
        value = values[i]
      }
      _ => ()
    }
  }
  names
}

///|
fn LspServer::kv3_navigation(
  self : LspServer,
  rpc_method : String,
  params : Map[String, Json],
) -> Json raise {
  let uri = lsp_string(lsp_object(params, "textDocument"), "uri")
  let document = self.documents[uri]
  let source = parse_kv3_preserving(document.text)
  let coordinates = lsp_coordinates(document.text)
  match rpc_method {
    "textDocument/definition" =>
      self.resource_links(
        uri,
        Some(lsp_offset(document.text, lsp_object(params, "position"))),
      )
    "textDocument/documentLink" => self.resource_links(uri, None)
    "textDocument/documentSymbol" =>
      Json::array([
        lsp_kv3_symbol(source, source.doc.root, [], "KV3", coordinates, {
          val: 5000,
        }),
      ])
    "textDocument/foldingRange" => {
      let spans = source.spans.values().collect()
      spans.sort_by((a, b) => a.body.compare(b.body))
      let ranges : Array[Json] = []
      for span in spans {
        if source.chars[span.body] != '{' && source.chars[span.body] != '[' {
          continue
        }
        guard coordinates.position(span.body) is Object(start) &&
          coordinates.position(span.end - 1) is Object(end) else {
          continue
        }
        let first = lsp_int(start, "line")
        let last = lsp_int(end, "line") - 1
        if last > first {
          ranges.push(
            Json::object({
              "startLine": Json::number(first.to_double()),
              "endLine": Json::number(last.to_double()),
              "kind": Json::string("region"),
            }),
          )
        }
        if ranges.length() >= 5000 {
          break
        }
      }
      Json::array(ranges)
    }
    "textDocument/completion" =>
      Json::array(
        [
          "true", "false", "null", "resource:\"\"", "resource_name:\"\"", "panorama:\"\"",
          "soundevent:\"\"", "subclass:\"\"", "entity_name:\"\"", "localize:\"\"",
        ].map(label => {
          Json::object({
            "label": Json::string(label),
            "kind": Json::number(14),
            "insertText": Json::string(label),
          })
        }),
      )
    "textDocument/hover" => {
      let offset = lsp_offset(document.text, lsp_object(params, "position"))
      let mut chosen : String? = None
      let mut width = document.text.char_length() + 1
      for key, span in source.spans {
        let (start, end) = source.keys
          .get(key)
          .unwrap_or((span.start, span.end))
        if (
            (offset >= span.start && offset < span.end) ||
            (offset >= start && offset < end)
          ) &&
          span.end - start < width {
          chosen = Some(key)
          width = span.end - start
        }
      }
      guard chosen is Some(key) else { return Json::null() }
      let indices : Array[Int32] = if key.is_empty() {
        []
      } else {
        key
        .split("/")
        .map(s => s.to_owned())
        .collect()
        .map(s => @strconv.parse_int(s[:]))
      }
      let value = kv3_at(source.doc.root, indices)
      let span = source.spans[key]
      let (start, end) = source.keys.get(key).unwrap_or((span.start, span.end))
      let mut resource_detail = ""
      if value is KV3Flag("resource", KV3String(_)) {
        for reference in source.resources(roots=self.resource_roots) {
          if reference.start == span.start {
            resource_detail = "\nResource: " +
              (match reference.target {
                Some(path) =>
                  self.resource_states.get(path).unwrap_or("unchecked") +
                  "\n" +
                  path
                None => reference.status
              })
            break
          }
        }
      }
      Json::object({
        "contents": Json::object({
          "kind": Json::string("plaintext"),
          "value": Json::string(
            "KV3 path: " +
            Json::array(
              kv3_path_names(source.doc.root, indices).map(s => Json::string(s)),
            ).stringify() +
            "\n" +
            kv3_summary(value) +
            resource_detail,
          ),
        }),
        "range": coordinates.range(start, end),
      })
    }
    _ => Json::array([])
  }
}