//===-- CompletionsHandler.cpp --------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "DAP.h"
#include "LLDBUtils.h"
#include "Protocol/ProtocolRequests.h"
#include "Protocol/ProtocolTypes.h"
#include "RequestHandler.h"
#include "lldb/API/SBStringList.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/ConvertUTF.h"

using namespace llvm;
using namespace lldb_dap;
using namespace lldb;
using namespace lldb_dap::protocol;

namespace lldb_dap {
/// Gets the position in the UTF8 string where the specified line started.
static size_t GetLineStartPos(StringRef text, uint32_t line) {
  if (line == 0) // Invalid line.
    return StringRef::npos;

  if (line == 1)
    return 0;

  uint32_t cur_line = 1;
  size_t pos = 0;

  while (cur_line < line) {
    const size_t new_line_pos = text.find('\n', pos);

    if (new_line_pos == StringRef::npos)
      return new_line_pos;

    pos = new_line_pos + 1;
    // text may end with a new line
    if (pos >= text.size())
      return StringRef::npos;

    cur_line++;
  }

  assert(pos < text.size());
  return pos;
}

static std::optional<size_t> GetCursorPos(StringRef text, uint32_t line,
                                          uint32_t utf16_codeunits) {
  if (text.empty())
    return std::nullopt;

  const size_t line_start_pos = GetLineStartPos(text, line);
  if (line_start_pos == StringRef::npos)
    return std::nullopt;

  const StringRef completion_line =
      text.substr(line_start_pos, text.find('\n', line_start_pos));
  if (completion_line.empty())
    return std::nullopt;

  const std::optional<size_t> cursor_pos_opt =
      UTF16CodeunitToBytes(completion_line, utf16_codeunits);
  if (!cursor_pos_opt)
    return std::nullopt;

  const size_t cursor_pos = line_start_pos + *cursor_pos_opt;
  return cursor_pos;
}

static size_t GetPartialTokenCodeUnits(StringRef line, size_t cursor_pos) {
  line = line.substr(0, cursor_pos);
  const size_t idx = line.rfind(' ');

  const size_t byte_offset = (idx == StringRef::npos) ? 0 : idx + 1;
  const StringRef byte_token = line.substr(byte_offset);
  SmallVector<UTF16, 20> utf16_token;

  if (convertUTF8ToUTF16String(byte_token, utf16_token))
    return utf16_token.size();
  return byte_token.size(); // fallback to back to byte offset.
}

/// Returns a list of possible completions for a given caret position and text.
///
/// Clients should only call this request if the corresponding capability
/// `supportsCompletionsRequest` is true.
Expected<CompletionsResponseBody>
CompletionsRequestHandler::Run(const CompletionsArguments &args) const {
  std::string text = args.text;
  const uint32_t line = args.line;
  // column starts at 1.
  const uint32_t utf16_codeunits = args.column - 1;

  const auto cursor_pos_opt = GetCursorPos(text, line, utf16_codeunits);
  if (!cursor_pos_opt)
    return CompletionsResponseBody{};

  size_t cursor_pos = *cursor_pos_opt;

  // If we have a frame, try to set the context for variable completions.
  lldb::SBFrame frame = dap.GetLLDBFrame(args.frameId);
  if (frame.IsValid()) {
    lldb::SBThread frame_thread = frame.GetThread();
    frame_thread.GetProcess().SetSelectedThread(frame_thread);
    frame_thread.SetSelectedFrame(frame.GetFrameID());
  }

  const StringRef escape_prefix = dap.configuration.commandEscapePrefix;
  const bool had_escape_prefix = StringRef(text).starts_with(escape_prefix);
  const ReplMode repl_mode = dap.DetectReplMode(frame, text, true);
  // Handle the cursor_pos change introduced by stripping out the
  // `command_escape_prefix`.
  if (had_escape_prefix) {
    if (cursor_pos < escape_prefix.size())
      return CompletionsResponseBody{};

    cursor_pos -= escape_prefix.size();
  }

  const size_t partial_token_cu = GetPartialTokenCodeUnits(text, cursor_pos);
  // While the user is typing then we likely have an incomplete input and cannot
  // reliably determine the precise intent (command vs variable), try completing
  // the text as both a command and variable expression, if applicable.
  const std::string expr_prefix = "expression -- ";
  const std::array<std::tuple<ReplMode, std::string, uint64_t>, 2> exprs = {
      {std::make_tuple(ReplMode::Command, text, cursor_pos),
       std::make_tuple(ReplMode::Variable, expr_prefix + text,
                       cursor_pos + expr_prefix.size())}};

  CompletionsResponseBody response;
  std::vector<CompletionItem> &targets = response.targets;
  lldb::SBCommandInterpreter interpreter = dap.debugger.GetCommandInterpreter();
  for (const auto &[mode, line, cursor] : exprs) {
    if (repl_mode != ReplMode::Auto && repl_mode != mode)
      continue;

    lldb::SBStringList matches;
    lldb::SBStringList descriptions;
    if (!interpreter.HandleCompletionWithDescriptions(
            line.c_str(), cursor, 0, 50, matches, descriptions))
      continue;

    // The first element is the common substring after the cursor position for
    // all the matches. The rest of the elements are the matches so ignore the
    // first result.
    for (uint32_t i = 1; i < matches.GetSize(); i++) {
      const StringRef match = matches.GetStringAtIndex(i);
      const StringRef description = descriptions.GetStringAtIndex(i);

      CompletionItem item;
      item.label = match;
      if (!description.empty())
        item.detail = description;
      // lldb returned completions includes the partial typed token
      // overwrite it.
      item.length = partial_token_cu;

      targets.emplace_back(std::move(item));
    }
  }

  return response;
}

} // namespace lldb_dap
