/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include "cmDebuggerBreakpointManager.h"

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <utility>

#include <cm3p/cppdap/optional.h>
#include <cm3p/cppdap/session.h>
#include <cm3p/cppdap/types.h>

#include "cmDebuggerSourceBreakpoint.h"
#include "cmListFileCache.h"
#include "cmSystemTools.h"

namespace cmDebugger {

// Resolve a source path to its canonical form so that breakpoint map
// keys match regardless of the case used by the DAP client or by
// CollapseFullPath.  On case-insensitive filesystems (macOS, Windows)
// ToNormalizedPathOnDisk loads the on-disk capitalization while
// preserving symbolic links in logical paths.
static std::string NormalizePath(std::string const& sourcePath)
{
  return cmSystemTools::ToNormalizedPathOnDisk(sourcePath);
}

cmDebuggerBreakpointManager::cmDebuggerBreakpointManager(
  dap::Session* dapSession)
  : DapSession(dapSession)
{
  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints
  DapSession->registerHandler([&](dap::SetBreakpointsRequest const& request) {
    return HandleSetBreakpointsRequest(request);
  });
}

int64_t cmDebuggerBreakpointManager::FindFunctionStartLine(
  std::string const& sourcePath, int64_t line)
{
  auto location =
    find_if(ListFileFunctionLines[sourcePath].begin(),
            ListFileFunctionLines[sourcePath].end(),
            [=](cmDebuggerFunctionLocation loc) {
              return loc.StartLine <= line && loc.EndLine >= line;
            });

  if (location != ListFileFunctionLines[sourcePath].end()) {
    return location->StartLine;
  }

  return 0;
}

int64_t cmDebuggerBreakpointManager::CalibrateBreakpointLine(
  std::string const& sourcePath, int64_t line)
{
  auto location = find_if(
    ListFileFunctionLines[sourcePath].begin(),
    ListFileFunctionLines[sourcePath].end(),
    [=](cmDebuggerFunctionLocation loc) { return loc.StartLine >= line; });

  if (location != ListFileFunctionLines[sourcePath].end()) {
    return location->StartLine;
  }

  if (!ListFileFunctionLines[sourcePath].empty() &&
      ListFileFunctionLines[sourcePath].back().EndLine <= line) {
    // return last function start line for any breakpoints after.
    return ListFileFunctionLines[sourcePath].back().StartLine;
  }

  return 0;
}

dap::SetBreakpointsResponse
cmDebuggerBreakpointManager::HandleSetBreakpointsRequest(
  dap::SetBreakpointsRequest const& request)
{
  std::unique_lock<std::mutex> lock(Mutex);

  dap::SetBreakpointsResponse response;

  auto sourcePath = NormalizePath(request.source.path.value());
  dap::array<dap::SourceBreakpoint> const defaultValue{};
  auto const& breakpoints = request.breakpoints.value(defaultValue);

  if (Breakpoints.find(sourcePath) != Breakpoints.end()) {
    Breakpoints[sourcePath].clear();
  }
  response.breakpoints.resize(breakpoints.size());

  if (ListFileFunctionLines.find(sourcePath) != ListFileFunctionLines.end()) {
    // The file has loaded, we can validate breakpoints.
    for (size_t i = 0; i < breakpoints.size(); i++) {
      int64_t correctedLine =
        CalibrateBreakpointLine(sourcePath, breakpoints[i].line);
      if (correctedLine > 0) {
        Breakpoints[sourcePath].emplace_back(NextBreakpointId++,
                                             correctedLine);
        response.breakpoints[i].id = Breakpoints[sourcePath].back().GetId();
        response.breakpoints[i].line =
          Breakpoints[sourcePath].back().GetLine();
        response.breakpoints[i].verified = true;
      } else {
        response.breakpoints[i].verified = false;
        response.breakpoints[i].line = breakpoints[i].line;
      }
      dap::Source dapSrc;
      dapSrc.path = sourcePath;
      response.breakpoints[i].source = dapSrc;
    }
  } else {
    // The file has not loaded, validate breakpoints later.
    ListFilePendingValidations.emplace(sourcePath);

    for (size_t i = 0; i < breakpoints.size(); i++) {
      Breakpoints[sourcePath].emplace_back(NextBreakpointId++,
                                           breakpoints[i].line);
      response.breakpoints[i].id = Breakpoints[sourcePath].back().GetId();
      response.breakpoints[i].line = Breakpoints[sourcePath].back().GetLine();
      response.breakpoints[i].verified = false;
      dap::Source dapSrc;
      dapSrc.path = sourcePath;
      response.breakpoints[i].source = dapSrc;
    }
  }

  return response;
}

void cmDebuggerBreakpointManager::SourceFileLoaded(
  std::string const& sourcePath,
  std::vector<cmListFileFunction> const& functions)
{
  auto normalizedPath = NormalizePath(sourcePath);

  std::unique_lock<std::mutex> lock(Mutex);
  if (ListFileFunctionLines.find(normalizedPath) !=
      ListFileFunctionLines.end()) {
    // this is not expected.
    return;
  }

  for (cmListFileFunction const& func : functions) {
    ListFileFunctionLines[normalizedPath].emplace_back(
      cmDebuggerFunctionLocation{ func.Line(), func.LineEnd() });
  }

  if (ListFilePendingValidations.find(normalizedPath) ==
      ListFilePendingValidations.end()) {
    return;
  }

  ListFilePendingValidations.erase(normalizedPath);

  for (size_t i = 0; i < Breakpoints[normalizedPath].size(); i++) {
    dap::BreakpointEvent breakpointEvent;
    breakpointEvent.breakpoint.id = Breakpoints[normalizedPath][i].GetId();
    breakpointEvent.breakpoint.line = Breakpoints[normalizedPath][i].GetLine();
    auto source = dap::Source();
    source.path = normalizedPath;
    breakpointEvent.breakpoint.source = source;
    int64_t correctedLine = CalibrateBreakpointLine(
      normalizedPath, Breakpoints[normalizedPath][i].GetLine());
    if (correctedLine != Breakpoints[normalizedPath][i].GetLine()) {
      Breakpoints[normalizedPath][i].ChangeLine(correctedLine);
    }
    breakpointEvent.reason = "changed";
    breakpointEvent.breakpoint.verified = (correctedLine > 0);
    if (breakpointEvent.breakpoint.verified) {
      breakpointEvent.breakpoint.line = correctedLine;
    } else {
      Breakpoints[normalizedPath][i].Invalid();
    }

    DapSession->send(breakpointEvent);
  }
}

std::vector<int64_t> cmDebuggerBreakpointManager::GetBreakpoints(
  std::string const& sourcePath, int64_t line)
{
  auto normalizedPath = NormalizePath(sourcePath);

  std::unique_lock<std::mutex> lock(Mutex);
  auto const& all = Breakpoints[normalizedPath];
  std::vector<int64_t> breakpoints;
  if (all.empty()) {
    return breakpoints;
  }

  auto it = all.begin();

  while ((it = std::find_if(
            it, all.end(), [&](cmDebuggerSourceBreakpoint const& breakpoint) {
              return (breakpoint.GetIsValid() && breakpoint.GetLine() == line);
            })) != all.end()) {
    breakpoints.emplace_back(it->GetId());
    ++it;
  }

  return breakpoints;
}

size_t cmDebuggerBreakpointManager::GetBreakpointCount() const
{
  size_t count = 0;
  for (auto const& pair : Breakpoints) {
    count += pair.second.size();
  }
  return count;
}

void cmDebuggerBreakpointManager::ClearAll()
{
  std::unique_lock<std::mutex> lock(Mutex);
  Breakpoints.clear();
}

} // namespace cmDebugger
