//===-- InitializeRequestHandler.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 "EventHelper.h"
#include "JSONUtils.h"
#include "Protocol/ProtocolRequests.h"
#include "RequestHandler.h"
#include "lldb/API/SBEvent.h"
#include "lldb/API/SBListener.h"
#include "lldb/API/SBStream.h"

using namespace lldb;
using namespace lldb_dap::protocol;

namespace lldb_dap {

static std::string GetStringFromStructuredData(lldb::SBStructuredData &data,
                                               const char *key) {
  lldb::SBStructuredData keyValue = data.GetValueForKey(key);
  if (!keyValue)
    return std::string();

  const size_t length = keyValue.GetStringValue(nullptr, 0);

  if (length == 0)
    return std::string();

  std::string str(length + 1, 0);
  keyValue.GetStringValue(&str[0], length + 1);
  return str;
}

static uint64_t GetUintFromStructuredData(lldb::SBStructuredData &data,
                                          const char *key) {
  lldb::SBStructuredData keyValue = data.GetValueForKey(key);

  if (!keyValue.IsValid())
    return 0;
  return keyValue.GetUnsignedIntegerValue();
}

void ProgressEventThreadFunction(DAP &dap) {
  lldb::SBListener listener("lldb-dap.progress.listener");
  dap.debugger.GetBroadcaster().AddListener(
      listener, lldb::SBDebugger::eBroadcastBitProgress |
                    lldb::SBDebugger::eBroadcastBitExternalProgress);
  dap.broadcaster.AddListener(listener, eBroadcastBitStopProgressThread);
  lldb::SBEvent event;
  bool done = false;
  while (!done) {
    if (listener.WaitForEvent(1, event)) {
      const auto event_mask = event.GetType();
      if (event.BroadcasterMatchesRef(dap.broadcaster)) {
        if (event_mask & eBroadcastBitStopProgressThread) {
          done = true;
        }
      } else {
        lldb::SBStructuredData data =
            lldb::SBDebugger::GetProgressDataFromEvent(event);

        const uint64_t progress_id =
            GetUintFromStructuredData(data, "progress_id");
        const uint64_t completed = GetUintFromStructuredData(data, "completed");
        const uint64_t total = GetUintFromStructuredData(data, "total");
        const std::string details =
            GetStringFromStructuredData(data, "details");

        if (completed == 0) {
          if (total == UINT64_MAX) {
            // This progress is non deterministic and won't get updated until it
            // is completed. Send the "message" which will be the combined title
            // and detail. The only other progress event for thus
            // non-deterministic progress will be the completed event So there
            // will be no need to update the detail.
            const std::string message =
                GetStringFromStructuredData(data, "message");
            dap.SendProgressEvent(progress_id, message.c_str(), completed,
                                  total);
          } else {
            // This progress is deterministic and will receive updates,
            // on the progress creation event VSCode will save the message in
            // the create packet and use that as the title, so we send just the
            // title in the progressCreate packet followed immediately by a
            // detail packet, if there is any detail.
            const std::string title =
                GetStringFromStructuredData(data, "title");
            dap.SendProgressEvent(progress_id, title.c_str(), completed, total);
            if (!details.empty())
              dap.SendProgressEvent(progress_id, details.c_str(), completed,
                                    total);
          }
        } else {
          // This progress event is either the end of the progress dialog, or an
          // update with possible detail. The "detail" string we send to VS Code
          // will be appended to the progress dialog's initial text from when it
          // was created.
          dap.SendProgressEvent(progress_id, details.c_str(), completed, total);
        }
      }
    }
  }
}

// All events from the debugger, target, process, thread and frames are
// received in this function that runs in its own thread. We are using a
// "FILE *" to output packets back to VS Code and they have mutexes in them
// them prevent multiple threads from writing simultaneously so no locking
// is required.
static void EventThreadFunction(DAP &dap) {
  llvm::set_thread_name(dap.transport.GetClientName() + ".event_handler");
  lldb::SBEvent event;
  lldb::SBListener listener = dap.debugger.GetListener();
  dap.broadcaster.AddListener(listener, eBroadcastBitStopEventThread);
  bool done = false;
  while (!done) {
    if (listener.WaitForEvent(1, event)) {
      const auto event_mask = event.GetType();
      if (lldb::SBProcess::EventIsProcessEvent(event)) {
        lldb::SBProcess process = lldb::SBProcess::GetProcessFromEvent(event);
        if (event_mask & lldb::SBProcess::eBroadcastBitStateChanged) {
          auto state = lldb::SBProcess::GetStateFromEvent(event);
          switch (state) {
          case lldb::eStateInvalid:
            // Not a state event
            break;
          case lldb::eStateUnloaded:
            break;
          case lldb::eStateConnected:
            break;
          case lldb::eStateAttaching:
            break;
          case lldb::eStateLaunching:
            break;
          case lldb::eStateStepping:
            break;
          case lldb::eStateCrashed:
            break;
          case lldb::eStateDetached:
            break;
          case lldb::eStateSuspended:
            break;
          case lldb::eStateStopped:
            // We launch and attach in synchronous mode then the first stop
            // event will not be delivered. If we use "launchCommands" during a
            // launch or "attachCommands" during an attach we might some process
            // stop events which we do not want to send an event for. We will
            // manually send a stopped event in request_configurationDone(...)
            // so don't send any before then.
            if (dap.configuration_done_sent) {
              // Only report a stopped event if the process was not
              // automatically restarted.
              if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
                SendStdOutStdErr(dap, process);
                SendThreadStoppedEvent(dap);
              }
            }
            break;
          case lldb::eStateRunning:
            dap.WillContinue();
            SendContinuedEvent(dap);
            break;
          case lldb::eStateExited:
            lldb::SBStream stream;
            process.GetStatus(stream);
            dap.SendOutput(OutputType::Console, stream.GetData());

            // When restarting, we can get an "exited" event for the process we
            // just killed with the old PID, or even with no PID. In that case
            // we don't have to terminate the session.
            if (process.GetProcessID() == LLDB_INVALID_PROCESS_ID ||
                process.GetProcessID() == dap.restarting_process_id) {
              dap.restarting_process_id = LLDB_INVALID_PROCESS_ID;
            } else {
              // Run any exit LLDB commands the user specified in the
              // launch.json
              dap.RunExitCommands();
              SendProcessExitedEvent(dap, process);
              dap.SendTerminatedEvent();
              done = true;
            }
            break;
          }
        } else if ((event_mask & lldb::SBProcess::eBroadcastBitSTDOUT) ||
                   (event_mask & lldb::SBProcess::eBroadcastBitSTDERR)) {
          SendStdOutStdErr(dap, process);
        }
      } else if (lldb::SBBreakpoint::EventIsBreakpointEvent(event)) {
        if (event_mask & lldb::SBTarget::eBroadcastBitBreakpointChanged) {
          auto event_type =
              lldb::SBBreakpoint::GetBreakpointEventTypeFromEvent(event);
          auto bp = Breakpoint(
              dap, lldb::SBBreakpoint::GetBreakpointFromEvent(event));
          // If the breakpoint was set through DAP, it will have the
          // BreakpointBase::GetBreakpointLabel() label. Regardless
          // of whether  locations were added, removed, or resolved, the
          // breakpoint isn't going away and the reason is always "changed".
          if ((event_type & lldb::eBreakpointEventTypeLocationsAdded ||
               event_type & lldb::eBreakpointEventTypeLocationsRemoved ||
               event_type & lldb::eBreakpointEventTypeLocationsResolved) &&
              bp.MatchesName(BreakpointBase::GetBreakpointLabel())) {
            // As the DAP client already knows the path of this breakpoint, we
            // don't need to send it back as part of the "changed" event. This
            // avoids sending paths that should be source mapped. Note that
            // CreateBreakpoint doesn't apply source mapping and certain
            // implementation ignore the source part of this event anyway.
            llvm::json::Value source_bp = CreateBreakpoint(&bp);
            source_bp.getAsObject()->erase("source");

            llvm::json::Object body;
            body.try_emplace("breakpoint", source_bp);
            body.try_emplace("reason", "changed");

            llvm::json::Object bp_event = CreateEventObject("breakpoint");
            bp_event.try_emplace("body", std::move(body));

            dap.SendJSON(llvm::json::Value(std::move(bp_event)));
          }
        }
      } else if (event.BroadcasterMatchesRef(dap.broadcaster)) {
        if (event_mask & eBroadcastBitStopEventThread) {
          done = true;
        }
      }
    }
  }
}

/// Initialize request; value of command field is 'initialize'.
llvm::Expected<InitializeResponseBody> InitializeRequestHandler::Run(
    const InitializeRequestArguments &arguments) const {
  dap.clientFeatures = arguments.supportedFeatures;

  // Do not source init files until in/out/err are configured.
  dap.debugger = lldb::SBDebugger::Create(false);
  dap.debugger.SetInputFile(dap.in);

  llvm::Expected<int> out_fd = dap.out.GetWriteFileDescriptor();
  if (!out_fd)
    return out_fd.takeError();
  dap.debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));

  llvm::Expected<int> err_fd = dap.err.GetWriteFileDescriptor();
  if (!err_fd)
    return err_fd.takeError();
  dap.debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));

  auto interp = dap.debugger.GetCommandInterpreter();

  // The sourceInitFile option is not part of the DAP specification. It is an
  // extension used by the test suite to prevent sourcing `.lldbinit` and
  // changing its behavior.
  if (arguments.lldbExtSourceInitFile.value_or(true)) {
    dap.debugger.SkipLLDBInitFiles(false);
    dap.debugger.SkipAppInitFiles(false);
    lldb::SBCommandReturnObject init;
    interp.SourceInitFileInGlobalDirectory(init);
    interp.SourceInitFileInHomeDirectory(init);
  }

  if (llvm::Error err = dap.RunPreInitCommands())
    return err;

  dap.PopulateExceptionBreakpoints();
  auto cmd = dap.debugger.GetCommandInterpreter().AddMultiwordCommand(
      "lldb-dap", "Commands for managing lldb-dap.");
  if (arguments.supportedFeatures.contains(
          eClientFeatureStartDebuggingRequest)) {
    cmd.AddCommand(
        "start-debugging", new StartDebuggingRequestHandler(dap),
        "Sends a startDebugging request from the debug adapter to the client "
        "to start a child debug session of the same type as the caller.");
  }
  cmd.AddCommand(
      "repl-mode", new ReplModeRequestHandler(dap),
      "Get or set the repl behavior of lldb-dap evaluation requests.");
  cmd.AddCommand("send-event", new SendEventRequestHandler(dap),
                 "Sends an DAP event to the client.");

  if (arguments.supportedFeatures.contains(eClientFeatureProgressReporting))
    dap.progress_event_thread =
        std::thread(ProgressEventThreadFunction, std::ref(dap));

  // Start our event thread so we can receive events from the debugger, target,
  // process and more.
  dap.event_thread = std::thread(EventThreadFunction, std::ref(dap));

  return dap.GetCapabilities();
}

} // namespace lldb_dap
