///|
/// Editor-facing hover/definition/completion queries built on top of `ToolingIndex`.

///|
/// Shared query context for hover, definition, and completion requests.
priv struct ToolingQuery {
  index : ToolingIndex
}

///|
priv enum ToolingReferenceTarget {
  Binding(String, String, @compiler.SourceSpan)
  Member(String, String, String, @compiler.SourceSpan)
}

///|
/// Compute diagnostics for a document without spinning up the JSON-RPC server.
pub fn diagnostics(
  source : String,
  filename? : String = "Component.mbtv",
) -> Array[ToolingDiagnostic] {
  build_tooling_index(source, filename).diagnostics
}

///|
/// Resolve hover contents at a UTF-16 line/character position.
pub fn hover_at(
  source : String,
  line : Int,
  character : Int,
  filename? : String = "Component.mbtv",
) -> ToolingHover? {
  ToolingQuery::new(source, filename).hover(line, character)
}

///|
/// Resolve the definition target for the token at the given UTF-16 position.
pub fn definition_at(
  source : String,
  line : Int,
  character : Int,
  filename? : String = "Component.mbtv",
) -> ToolingDefinition? {
  ToolingQuery::new(source, filename).definition(line, character)
}

///|
/// Compute completion items using template-ref, member, and general directive contexts.
pub fn complete_at(
  source : String,
  line : Int,
  character : Int,
  filename? : String = "Component.mbtv",
) -> Array[ToolingCompletionItem] {
  ToolingQuery::new(source, filename).complete(line, character)
}

///|
/// Resolve all references for the token at the given UTF-16 position.
pub fn references_at(
  source : String,
  line : Int,
  character : Int,
  include_declaration? : Bool = true,
  filename? : String = "Component.mbtv",
) -> Array[ToolingLocation] {
  ToolingQuery::new(source, filename).references(
    line, character, include_declaration,
  )
}

///|
/// Build a reusable query context for one source document.
fn ToolingQuery::new(source : String, filename : String) -> ToolingQuery {
  { index: build_tooling_index(source, filename) }
}

///|
/// Resolve hover contents at a UTF-16 line/character position.
fn ToolingQuery::hover(
  self : ToolingQuery,
  line : Int,
  character : Int,
) -> ToolingHover? {
  let document = self.index.document
  let offset = document.position_to_offset(line, character)
  match self.find_hover_target(offset) {
    Some((contents, span)) =>
      Some({ contents, location: document.location_from_span(span) })
    None => None
  }
}

///|
/// Resolve the definition target for the token at the given UTF-16 position.
fn ToolingQuery::definition(
  self : ToolingQuery,
  line : Int,
  character : Int,
) -> ToolingDefinition? {
  let document = self.index.document
  let offset = document.position_to_offset(line, character)
  match self.find_definition_target(offset) {
    Some((name, span)) =>
      Some({ name, location: document.location_from_span(span) })
    None => None
  }
}

///|
/// Compute completion items using template-ref, member, and general directive contexts.
fn ToolingQuery::complete(
  self : ToolingQuery,
  line : Int,
  character : Int,
) -> Array[ToolingCompletionItem] {
  let offset = self.index.document.position_to_offset(line, character)
  match self.resolved_template_ref_completion_items(offset) {
    Some(items) => items
    None =>
      match self.member_completion_items(offset) {
        Some(items) => items
        None => self.general_completion_items(offset)
      }
  }
}

///|
/// Resolve all references for the token at the given UTF-16 position.
fn ToolingQuery::references(
  self : ToolingQuery,
  line : Int,
  character : Int,
  include_declaration : Bool,
) -> Array[ToolingLocation] {
  let offset = self.index.document.position_to_offset(line, character)
  match self.find_reference_target(offset) {
    Some(target) => self.reference_locations(target, include_declaration)
    None => []
  }
}

///|
/// Complete members on `resolve_template_ref(ref).member` expressions.
fn ToolingQuery::resolved_template_ref_completion_items(
  self : ToolingQuery,
  offset : Int,
) -> Array[ToolingCompletionItem]? {
  let context = self.resolved_template_ref_completion_context(offset)
  match context {
    Some((binding_name, prefix)) => {
      let items : Array[ToolingCompletionItem] = []
      for binding in self.index.bindings {
        if binding.access_mode != ResolveTemplateRef ||
          binding.binding_name != binding_name {
          continue
        }
        for field in binding.fields {
          if not(field.name.has_prefix(prefix)) {
            continue
          }
          items.push({
            label: field.name,
            kind: binding.contract_kind,
            detail: render_field_detail(binding.contract_kind, field),
            insert_text: field.name,
          })
        }
        return Some(items)
      }
      Some([])
    }
    None => None
  }
}

///|
/// Detect the completion context for `resolve_template_ref(binding).prefix`.
fn ToolingQuery::resolved_template_ref_completion_context(
  self : ToolingQuery,
  offset : Int,
) -> (String, String)? {
  let document = self.index.document
  let chars = document.chars
  let limit = if offset < chars.length() { offset } else { chars.length() }
  let mut start = limit
  while start > 0 && is_identifier_char(chars[start - 1]) {
    start -= 1
  }
  let prefix = String::unsafe_substring(document.source, start~, end=limit)
  if start == 0 || chars[start - 1] != '.' {
    return None
  }
  let mut cursor = start - 1
  while cursor > 0 && (chars[cursor - 1] == ' ' || chars[cursor - 1] == '\t') {
    cursor -= 1
  }
  if cursor == 0 || chars[cursor - 1] != ')' {
    return None
  }
  let close_paren = cursor - 1
  let open_paren = match document.matching_open_paren_backward(close_paren) {
    Some(index) => index
    None => return None
  }
  let arg_span = match
    document.trim_span_whitespace(open_paren + 1, close_paren) {
    Some(span) => span
    None => return None
  }
  let binding_name = document.span_text(arg_span)
  if not(ToolingDocument::identifier_literal(binding_name)) {
    return None
  }
  cursor = open_paren
  while cursor > 0 && (chars[cursor - 1] == ' ' || chars[cursor - 1] == '\t') {
    cursor -= 1
  }
  let name_end = cursor
  let mut name_start = name_end
  while name_start > 0 {
    let ch = chars[name_start - 1]
    if is_identifier_char(ch) || ch == '.' || ch == '@' {
      name_start -= 1
    } else {
      break
    }
  }
  let callee = String::unsafe_substring(
    document.source,
    start=name_start,
    end=name_end,
  )
  if not(callee.has_suffix("resolve_template_ref")) {
    return None
  }
  Some((binding_name, prefix))
}

///|
/// Find the best hover payload for a source offset.
fn ToolingQuery::find_hover_target(
  self : ToolingQuery,
  offset : Int,
) -> (String, @compiler.SourceSpan)? {
  for entry in self.index.members {
    if contains_offset(entry.span, offset) {
      return Some((member_hover_text(entry), entry.span))
    }
  }
  for binding in self.index.bindings {
    if contains_offset(binding.span, offset) {
      return Some((binding_hover_text(binding), binding.span))
    }
  }
  for ref_ in self.index.refs {
    if contains_offset(ref_.member_span, offset) {
      match
        find_binding(self.index.bindings, ref_.contract_kind, ref_.binding_name) {
        Some(binding) =>
          match
            find_member(
              self.index.members,
              binding.binding_name,
              ref_.member_name,
            ) {
            Some(entry) =>
              return Some((member_hover_text(entry), ref_.member_span))
            None => ()
          }
        None => ()
      }
    }
    if contains_offset(ref_.binding_span, offset) {
      match
        find_binding(self.index.bindings, ref_.contract_kind, ref_.binding_name) {
        Some(binding) =>
          return Some((binding_hover_text(binding), ref_.binding_span))
        None => ()
      }
    }
  }
  None
}

///|
/// Resolve the declaration span referenced by a source offset.
fn ToolingQuery::find_definition_target(
  self : ToolingQuery,
  offset : Int,
) -> (String, @compiler.SourceSpan)? {
  for entry in self.index.members {
    if contains_offset(entry.span, offset) {
      return Some((entry.name, entry.span))
    }
  }
  for binding in self.index.bindings {
    if contains_offset(binding.span, offset) {
      return Some((binding.binding_name, binding.span))
    }
  }
  for ref_ in self.index.refs {
    if contains_offset(ref_.member_span, offset) {
      match
        find_binding(self.index.bindings, ref_.contract_kind, ref_.binding_name) {
        Some(binding) =>
          match
            find_member(
              self.index.members,
              binding.binding_name,
              ref_.member_name,
            ) {
            Some(entry) => return Some((entry.name, entry.span))
            None => ()
          }
        None => ()
      }
    }
    if contains_offset(ref_.binding_span, offset) {
      match
        find_binding(self.index.bindings, ref_.contract_kind, ref_.binding_name) {
        Some(binding) => return Some((binding.binding_name, binding.span))
        None => ()
      }
    }
  }
  None
}

///|
/// Complete members on direct contract bindings such as `props.foo`.
fn ToolingQuery::member_completion_items(
  self : ToolingQuery,
  offset : Int,
) -> Array[ToolingCompletionItem]? {
  let context = self.member_completion_context(offset)
  match context {
    Some((binding_name, prefix)) => {
      let items : Array[ToolingCompletionItem] = []
      for binding in self.index.bindings {
        if binding.access_mode != Direct || binding.binding_name != binding_name {
          continue
        }
        for field in binding.fields {
          if not(field.name.has_prefix(prefix)) {
            continue
          }
          items.push({
            label: field.name,
            kind: binding.contract_kind,
            detail: render_field_detail(binding.contract_kind, field),
            insert_text: field.name,
          })
        }
      }
      Some(items)
    }
    None => None
  }
}

///|
/// Fallback completion path for directives, events, bindings, and macros.
fn ToolingQuery::general_completion_items(
  self : ToolingQuery,
  offset : Int,
) -> Array[ToolingCompletionItem] {
  let prefix = self.index.document.current_token_prefix(offset)
  if prefix.has_prefix("@") {
    return event_completion_items(prefix)
  }
  if prefix.has_prefix("v-on:") {
    return long_event_completion_items(prefix)
  }
  if prefix.has_prefix(":") {
    return binding_completion_items(prefix)
  }
  if prefix.has_prefix("v-bind:") {
    return long_binding_completion_items(prefix)
  }
  if prefix.has_prefix("v-slot") || prefix.has_prefix("#") {
    return slot_completion_items(prefix)
  }
  if prefix.has_prefix("v-") {
    return directive_completion_items(prefix)
  }
  if prefix.has_prefix("client:") {
    return island_completion_items(prefix, "client", [
      "client:load", "client:idle", "client:visible", "client:media", "client:only",
    ])
  }
  if prefix.has_prefix("server:") {
    return island_completion_items(prefix, "server", ["server:defer"])
  }
  macro_completion_items(prefix)
}

///|
/// Detect `binding.member` completion contexts at the given offset.
fn ToolingQuery::member_completion_context(
  self : ToolingQuery,
  offset : Int,
) -> (String, String)? {
  let document = self.index.document
  let chars = document.chars
  let limit = if offset < chars.length() { offset } else { chars.length() }
  let mut start = limit
  while start > 0 && is_identifier_char(chars[start - 1]) {
    start -= 1
  }
  let prefix = String::unsafe_substring(document.source, start~, end=limit)
  if start == 0 || chars[start - 1] != '.' {
    return None
  }
  let binding_end = start - 1
  let mut binding_start = binding_end
  while binding_start > 0 && is_identifier_char(chars[binding_start - 1]) {
    binding_start -= 1
  }
  let binding_name = String::unsafe_substring(
    document.source,
    start=binding_start,
    end=binding_end,
  )
  Some((binding_name, prefix))
}

///|
/// Resolve the binding/member target used by go-to-references.
fn ToolingQuery::find_reference_target(
  self : ToolingQuery,
  offset : Int,
) -> ToolingReferenceTarget? {
  for entry in self.index.members {
    if contains_offset(entry.span, offset) {
      return Some(
        Member(entry.contract_kind, entry.binding_name, entry.name, entry.span),
      )
    }
  }
  for binding in self.index.bindings {
    if contains_offset(binding.span, offset) {
      return Some(
        Binding(binding.contract_kind, binding.binding_name, binding.span),
      )
    }
  }
  for ref_ in self.index.refs {
    if contains_offset(ref_.member_span, offset) {
      match
        find_binding(self.index.bindings, ref_.contract_kind, ref_.binding_name) {
        Some(binding) =>
          match
            find_member(
              self.index.members,
              binding.binding_name,
              ref_.member_name,
            ) {
            Some(entry) =>
              return Some(
                Member(
                  binding.contract_kind,
                  binding.binding_name,
                  entry.name,
                  entry.span,
                ),
              )
            None => ()
          }
        None => ()
      }
    }
    if contains_offset(ref_.binding_span, offset) {
      match
        find_binding(self.index.bindings, ref_.contract_kind, ref_.binding_name) {
        Some(binding) =>
          return Some(
            Binding(binding.contract_kind, binding.binding_name, binding.span),
          )
        None => ()
      }
    }
  }
  None
}

///|
/// Collect reference locations for one resolved binding or member target.
fn ToolingQuery::reference_locations(
  self : ToolingQuery,
  target : ToolingReferenceTarget,
  include_declaration : Bool,
) -> Array[ToolingLocation] {
  let locations : Array[ToolingLocation] = []
  match target {
    Binding(contract_kind, binding_name, span) => {
      if include_declaration {
        push_unique_location(
          locations,
          self.index.document.location_from_span(span),
        )
      }
      for ref_ in self.index.refs {
        if ref_.contract_kind != contract_kind {
          continue
        }
        match
          find_binding(
            self.index.bindings,
            ref_.contract_kind,
            ref_.binding_name,
          ) {
          Some(binding) if binding.binding_name == binding_name =>
            push_unique_location(
              locations,
              self.index.document.location_from_span(ref_.binding_span),
            )
          _ => ()
        }
      }
    }
    Member(contract_kind, binding_name, member_name, span) => {
      if include_declaration {
        push_unique_location(
          locations,
          self.index.document.location_from_span(span),
        )
      }
      for ref_ in self.index.refs {
        if ref_.contract_kind != contract_kind ||
          ref_.member_name != member_name {
          continue
        }
        match
          find_binding(
            self.index.bindings,
            ref_.contract_kind,
            ref_.binding_name,
          ) {
          Some(binding) if binding.binding_name == binding_name =>
            push_unique_location(
              locations,
              self.index.document.location_from_span(ref_.member_span),
            )
          _ => ()
        }
      }
    }
  }
  locations
}

///|
/// Deduplicate reference locations by filename and source span.
fn push_unique_location(
  locations : Array[ToolingLocation],
  next_ : ToolingLocation,
) -> Unit {
  for location in locations {
    if location.filename == next_.filename && location.span == next_.span {
      return
    }
  }
  locations.push(next_)
}

///|
/// Render hover text for one contract field entry.
fn member_hover_text(entry : ToolingMemberDef) -> String {
  render_field_detail(entry.contract_kind, {
    name: entry.name,
    signature: entry.signature,
    type_identifiers: [],
    type_shape: @compiler_common.SingleValue,
    is_optional: false,
  })
}

///|
/// Render hover text for an entire binding, including template-ref metadata.
fn binding_hover_text(binding : ToolingBindingDef) -> String {
  if binding.access_mode == ResolveTemplateRef {
    let type_name = binding.type_name.unwrap_or("@js_dom.Element")
    let fields : Array[String] = []
    for field in binding.fields {
      fields.push(render_field_detail(binding.contract_kind, field))
    }
    if fields.is_empty() {
      return "template ref `" + binding.binding_name + "` : " + type_name
    }
    return "template ref `" +
      binding.binding_name +
      "` : " +
      type_name +
      "\n" +
      fields.join("\n")
  }
  let fields : Array[String] = []
  for field in binding.fields {
    fields.push(render_field_detail(binding.contract_kind, field))
  }
  "define" +
  uppercase_first(binding.contract_kind) +
  " binding `" +
  binding.binding_name +
  "`\n" +
  fields.join("\n")
}

///|
/// Convert a macro field into the user-facing detail string shown in LSP results.
fn render_field_detail(
  contract_kind : String,
  field : @compiler.MacroField,
) -> String {
  contract_kind + " " + field.name + " : " + field.signature.unwrap_or("String")
}