//===-- RequestHandler.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 "Handler/RequestHandler.h"
#include "DAP.h"
#include "EventHelper.h"
#include "Handler/ResponseHandler.h"
#include "JSONUtils.h"
#include "LLDBUtils.h"
#include "Protocol/ProtocolBase.h"
#include "Protocol/ProtocolRequests.h"
#include "RunInTerminal.h"
#include "lldb/API/SBDefines.h"
#include "lldb/API/SBEnvironment.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/raw_ostream.h"
#include <mutex>

#if !defined(_WIN32)
#include <unistd.h>
#endif

#ifndef LLDB_DAP_README_URL
#define LLDB_DAP_README_URL                                                    \
  "https://lldb.llvm.org/use/lldbdap.html#debug-console"
#endif

using namespace lldb_dap::protocol;

namespace lldb_dap {

static std::vector<const char *>
MakeArgv(const llvm::ArrayRef<std::string> &strs) {
  // Create and return an array of "const char *", one for each C string in
  // "strs" and terminate the list with a NULL. This can be used for argument
  // vectors (argv) or environment vectors (envp) like those passed to the
  // "main" function in C programs.
  std::vector<const char *> argv;
  for (const auto &s : strs)
    argv.push_back(s.c_str());
  argv.push_back(nullptr);
  return argv;
}

static uint32_t SetLaunchFlag(uint32_t flags, bool flag,
                              lldb::LaunchFlags mask) {
  if (flag)
    flags |= mask;
  else
    flags &= ~mask;

  return flags;
}

static void
SetupIORedirection(const std::vector<std::optional<std::string>> &stdio,
                   lldb::SBLaunchInfo &launch_info) {
  size_t n = std::max(stdio.size(), static_cast<size_t>(3));
  for (size_t i = 0; i < n; i++) {
    std::optional<std::string> path;
    if (stdio.size() <= i)
      path = stdio.back();
    else
      path = stdio[i];
    if (!path)
      continue;
    switch (i) {
    case 0:
      launch_info.AddOpenFileAction(i, path->c_str(), true, false);
      break;
    case 1:
    case 2:
      launch_info.AddOpenFileAction(i, path->c_str(), false, true);
      break;
    default:
      launch_info.AddOpenFileAction(i, path->c_str(), true, true);
      break;
    }
  }
}

static llvm::Error
RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
  if (!dap.clientFeatures.contains(
          protocol::eClientFeatureRunInTerminalRequest))
    return llvm::make_error<DAPError>("Cannot use runInTerminal, feature is "
                                      "not supported by the connected client");

  if (arguments.configuration.program.empty())
    return llvm::make_error<DAPError>(
        "program must be set to when using runInTerminal");

  dap.is_attach = true;
  lldb::SBAttachInfo attach_info;

  llvm::Expected<std::shared_ptr<FifoFile>> comm_file_or_err =
      CreateRunInTerminalCommFile();
  if (!comm_file_or_err)
    return comm_file_or_err.takeError();
  FifoFile &comm_file = *comm_file_or_err.get();

  RunInTerminalDebugAdapterCommChannel comm_channel(comm_file.m_path);

  lldb::pid_t debugger_pid = LLDB_INVALID_PROCESS_ID;
#if !defined(_WIN32)
  debugger_pid = getpid();
#endif

  llvm::json::Object reverse_request = CreateRunInTerminalReverseRequest(
      arguments.configuration.program, arguments.args, arguments.env,
      arguments.cwd, comm_file.m_path, debugger_pid, arguments.stdio,
      arguments.console == protocol::eConsoleExternalTerminal);
  dap.SendReverseRequest<LogFailureResponseHandler>("runInTerminal",
                                                    std::move(reverse_request));

  if (llvm::Expected<lldb::pid_t> pid = comm_channel.GetLauncherPid())
    attach_info.SetProcessID(*pid);
  else
    return pid.takeError();

  std::optional<ScopeSyncMode> scope_sync_mode(dap.debugger);
  lldb::SBError error;
  dap.target.Attach(attach_info, error);

  if (error.Fail())
    return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                   "Failed to attach to the target process. %s",
                                   comm_channel.GetLauncherError().c_str());
  // This will notify the runInTerminal launcher that we attached.
  // We have to make this async, as the function won't return until the launcher
  // resumes and reads the data.
  std::future<lldb::SBError> did_attach_message_success =
      comm_channel.NotifyDidAttach();

  // We just attached to the runInTerminal launcher, which was waiting to be
  // attached. We now resume it, so it can receive the didAttach notification
  // and then perform the exec. Upon continuing, the debugger will stop the
  // process right in the middle of the exec. To the user, what we are doing is
  // transparent, as they will only be able to see the process since the exec,
  // completely unaware of the preparatory work.
  dap.target.GetProcess().Continue();

  // Now that the actual target is just starting (i.e. exec was just invoked),
  // we return the debugger to its sync state.
  scope_sync_mode.reset();

  // If sending the notification failed, the launcher should be dead by now and
  // the async didAttach notification should have an error message, so we
  // return it. Otherwise, everything was a success.
  did_attach_message_success.wait();
  error = did_attach_message_success.get();
  if (error.Success())
    return llvm::Error::success();
  return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                 error.GetCString());
}

void BaseRequestHandler::Run(const Request &request) {
  // If this request was cancelled, send a cancelled response.
  if (dap.IsCancelled(request)) {
    Response cancelled{
        /*request_seq=*/request.seq,
        /*command=*/request.command,
        /*success=*/false,
        /*message=*/eResponseMessageCancelled,
    };
    dap.Send(cancelled);
    return;
  }

  lldb::SBMutex lock = dap.GetAPIMutex();
  std::lock_guard<lldb::SBMutex> guard(lock);

  // FIXME: After all the requests have migrated from LegacyRequestHandler >
  // RequestHandler<> we should be able to move this into
  // RequestHandler<>::operator().
  operator()(request);

  // FIXME: After all the requests have migrated from LegacyRequestHandler >
  // RequestHandler<> we should be able to check `debugger.InterruptRequest` and
  // mark the response as cancelled.
}

llvm::Error BaseRequestHandler::LaunchProcess(
    const protocol::LaunchRequestArguments &arguments) const {
  const std::vector<std::string> &launchCommands = arguments.launchCommands;

  // Instantiate a launch info instance for the target.
  auto launch_info = dap.target.GetLaunchInfo();

  // Grab the current working directory if there is one and set it in the
  // launch info.
  if (!arguments.cwd.empty())
    launch_info.SetWorkingDirectory(arguments.cwd.data());

  // Extract any extra arguments and append them to our program arguments for
  // when we launch
  if (!arguments.args.empty())
    launch_info.SetArguments(MakeArgv(arguments.args).data(), true);

  // Pass any environment variables along that the user specified.
  if (!arguments.env.empty()) {
    lldb::SBEnvironment env;
    for (const auto &kv : arguments.env)
      env.Set(kv.first().data(), kv.second.c_str(), true);
    launch_info.SetEnvironment(env, true);
  }

  if (!arguments.stdio.empty() && !arguments.disableSTDIO)
    SetupIORedirection(arguments.stdio, launch_info);

  launch_info.SetDetachOnError(arguments.detachOnError);
  launch_info.SetShellExpandArguments(arguments.shellExpandArguments);

  auto flags = launch_info.GetLaunchFlags();
  flags =
      SetLaunchFlag(flags, arguments.disableASLR, lldb::eLaunchFlagDisableASLR);
  flags = SetLaunchFlag(flags, arguments.disableSTDIO,
                        lldb::eLaunchFlagDisableSTDIO);
  launch_info.SetLaunchFlags(flags | lldb::eLaunchFlagDebug |
                             lldb::eLaunchFlagStopAtEntry);

  {
    // Perform the launch in synchronous mode so that we don't have to worry
    // about process state changes during the launch.
    ScopeSyncMode scope_sync_mode(dap.debugger);

    if (arguments.console != protocol::eConsoleInternal) {
      if (llvm::Error err = RunInTerminal(dap, arguments))
        return err;
    } else if (launchCommands.empty()) {
      lldb::SBError error;
      dap.target.Launch(launch_info, error);
      if (error.Fail())
        return ToError(error);
    } else {
      // Set the launch info so that run commands can access the configured
      // launch details.
      dap.target.SetLaunchInfo(launch_info);
      if (llvm::Error err = dap.RunLaunchCommands(launchCommands))
        return err;

      // The custom commands might have created a new target so we should use
      // the selected target after these commands are run.
      dap.target = dap.debugger.GetSelectedTarget();
    }
  }

  // Make sure the process is launched and stopped at the entry point before
  // proceeding.
  lldb::SBError error =
      dap.WaitForProcessToStop(arguments.configuration.timeout);
  if (error.Fail())
    return ToError(error);

  return llvm::Error::success();
}

void BaseRequestHandler::PrintWelcomeMessage() const {
  std::string message;
  llvm::raw_string_ostream OS(message);

#ifdef LLDB_DAP_WELCOME_MESSAGE
  dap.SendOutput(eOutputCategoryConsole, LLDB_DAP_WELCOME_MESSAGE);
#endif

  // Trying to provide a brief but helpful welcome message for users to better
  // understand how the debug console repl works.
  OS << "To get started with the debug console try ";
  switch (dap.repl_mode) {
  case ReplMode::Auto:
    OS << "\"<variable>\", \"<lldb-cmd>\" or \"help [<lldb-cmd>]\"\r\n";
    break;
  case ReplMode::Command:
    OS << "\"<lldb-cmd>\" or \"help [<lldb-cmd>]\".\r\n";
    break;
  case ReplMode::Variable:
    OS << "\"<variable>\" or \"" << dap.configuration.commandEscapePrefix
       << "help [<lldb-cmd>]\".\r\n";
    break;
  }

  OS << "For more information visit " LLDB_DAP_README_URL ".\r\n";

  dap.SendOutput(OutputType::Console, message);
}

void BaseRequestHandler::PrintIntroductionMessage() const {
  std::string msg;
  llvm::raw_string_ostream os(msg);
  if (dap.target && dap.target.GetExecutable()) {
    std::string path = GetSBFileSpecPath(dap.target.GetExecutable());
    os << llvm::formatv("Executable binary set to '{0}' ({1}).\r\n", path,
                        dap.target.GetTriple());
  }
  if (dap.target.GetProcess()) {
    os << llvm::formatv("Attached to process {0}.\r\n",
                        dap.target.GetProcess().GetProcessID());
  }
  dap.SendOutput(OutputType::Console, msg);
}

bool BaseRequestHandler::HasInstructionGranularity(
    const llvm::json::Object &arguments) const {
  if (std::optional<llvm::StringRef> value = arguments.getString("granularity"))
    return value == "instruction";
  return false;
}

void BaseRequestHandler::BuildErrorResponse(
    llvm::Error err, protocol::Response &response) const {
  // Handle the ErrorSuccess case.
  if (!err) {
    response.success = true;
    return;
  }

  response.success = false;

  llvm::handleAllErrors(
      std::move(err),
      [&](const NotStoppedError &err) {
        response.message = lldb_dap::protocol::eResponseMessageNotStopped;
      },
      [&](const DAPError &err) {
        protocol::ErrorMessage error_message;
        error_message.sendTelemetry = false;
        error_message.format = err.getMessage();
        error_message.showUser = err.getShowUser();
        error_message.id = err.convertToErrorCode().value();
        error_message.url = err.getURL();
        error_message.urlLabel = err.getURLLabel();
        protocol::ErrorResponseBody body;
        body.error = error_message;

        response.body = body;
      },
      [&](const llvm::ErrorInfoBase &err) {
        protocol::ErrorMessage error_message;
        error_message.showUser = true;
        error_message.sendTelemetry = false;
        error_message.format = err.message();
        error_message.id = err.convertToErrorCode().value();
        protocol::ErrorResponseBody body;
        body.error = error_message;

        response.body = body;
      });
}

void BaseRequestHandler::SendError(llvm::Error err,
                                   protocol::Response &response) const {
  BuildErrorResponse(std::move(err), response);
  Send(response);
}

void BaseRequestHandler::SendSuccess(
    protocol::Response &response, std::optional<llvm::json::Value> body) const {
  response.success = true;
  if (body)
    response.body = std::move(*body);

  Send(response);
}

void BaseRequestHandler::Send(protocol::Response &response) const {
  // Mark the request as 'cancelled' if the debugger was interrupted while
  // evaluating this handler.
  if (dap.debugger.InterruptRequested()) {
    response.success = false;
    response.message = protocol::eResponseMessageCancelled;
    response.body = std::nullopt;
  }

  dap.Send(response);
}

} // namespace lldb_dap
