//===----------------------------------------------------------------------===//
//
// 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 "PlatformWebInspectorWasm.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/ProcessLaunchInfo.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/ErrorExtras.h"

#include <chrono>
#include <csignal>
#include <thread>

using namespace lldb;
using namespace lldb_private;

LLDB_PLUGIN_DEFINE(PlatformWebInspectorWasm)

static constexpr llvm::StringLiteral kServerBinary =
    "/System/Cryptexes/App/usr/libexec/webinspector-wasm-lldb-platform";
static constexpr uint8_t kConnectAttempts = 5;
static constexpr auto kConnectDelay = std::chrono::milliseconds(100);

PlatformWebInspectorWasm::PlatformWebInspectorWasm() {
  if (llvm::Error err = EnsureConnected()) {
    LLDB_LOG_ERROR(GetLog(LLDBLog::Platform), std::move(err),
                   "EnsureConnected failed: {0}");
  }
}

llvm::StringRef PlatformWebInspectorWasm::GetPluginDescriptionStatic() {
  return "Platform for debugging Wasm via WebInspector";
}

void PlatformWebInspectorWasm::Initialize() {
  PluginManager::RegisterPlugin(GetPluginNameStatic(),
                                GetPluginDescriptionStatic(),
                                PlatformWebInspectorWasm::CreateInstance);
}

void PlatformWebInspectorWasm::Terminate() {
  PluginManager::UnregisterPlugin(PlatformWebInspectorWasm::CreateInstance);
}

PlatformSP PlatformWebInspectorWasm::CreateInstance(bool force,
                                                    const ArchSpec *arch) {
  Log *log = GetLog(LLDBLog::Platform);
  LLDB_LOG(log, "force = {0}, arch = ({1}, {2})", force,
           arch ? arch->GetArchitectureName() : "<null>",
           arch ? arch->GetTriple().getTriple() : "<null>");
  return force ? PlatformSP(new PlatformWebInspectorWasm()) : PlatformSP();
}

PlatformWebInspectorWasm::~PlatformWebInspectorWasm() {
  if (m_server_pid != LLDB_INVALID_PROCESS_ID)
    Host::Kill(m_server_pid, SIGTERM);
}

llvm::Error PlatformWebInspectorWasm::LaunchPlatformServer() {
  Log *log = GetLog(LLDBLog::Platform);

  if (!FileSystem::Instance().Exists(FileSpec(kServerBinary)))
    return llvm::createStringErrorV("platform binary not found: {0}",
                                    kServerBinary);

  // Find two free TCP ports.
  llvm::Expected<uint16_t> expected_platform_port = FindFreeTCPPort();
  if (!expected_platform_port)
    return expected_platform_port.takeError();
  uint16_t platform_port = *expected_platform_port;

  llvm::Expected<uint16_t> expected_debugserver_port = FindFreeTCPPort();
  if (!expected_debugserver_port)
    return expected_debugserver_port.takeError();
  uint16_t debugserver_port = *expected_debugserver_port;

  ProcessLaunchInfo launch_info;
  launch_info.SetExecutableFile(FileSpec(kServerBinary),
                                /*add_exe_file_as_first_arg=*/true);
  Args args;
  args.AppendArgument(kServerBinary);
  args.AppendArgument("--platform");
  args.AppendArgument(llvm::utostr(platform_port));
  args.AppendArgument("--debugserver");
  args.AppendArgument(llvm::utostr(debugserver_port));
  launch_info.SetArguments(args, /*first_arg_is_executable=*/true);
  launch_info.SetLaunchInSeparateProcessGroup(true);
  launch_info.GetFlags().Clear(eLaunchFlagDebug);

  launch_info.SetMonitorProcessCallback(
      [log](lldb::pid_t pid, int signal, int status) {
        LLDB_LOG(log,
                 "Platform exited: pid = {0}, signal = "
                 "{1}, status = {2}",
                 pid, signal, status);
      });

  LLDB_LOG(log, "{0}", GetArgRange(launch_info.GetArguments()));

  Status status = Host::LaunchProcess(launch_info);
  if (status.Fail())
    return status.takeError();

  m_server_pid = launch_info.GetProcessID();
  LLDB_LOG(log, "Platform launched: pid = {0}", m_server_pid);

  Args connect_args;
  connect_args.AppendArgument(
      llvm::formatv("connect://localhost:{0}", platform_port).str());

  // The platform may need some time to bind a socket to the requested port.
  for (uint8_t attempt = 0; attempt < kConnectAttempts; attempt++) {
    status = PlatformWasm::ConnectRemote(connect_args);
    if (status.Success())
      return llvm::Error::success();

    LLDB_LOG(
        log,
        "[{0}/{1}] platform not yet listening on port {2}: trying again in {3}",
        attempt, kConnectAttempts, platform_port, kConnectDelay);
    std::this_thread::sleep_for(kConnectDelay);
  }
  return status.takeError();
}

llvm::Error PlatformWebInspectorWasm::EnsureConnected() {
  if (m_remote_platform_sp && m_remote_platform_sp->IsConnected())
    return llvm::Error::success();
  return LaunchPlatformServer();
}

Status PlatformWebInspectorWasm::ConnectRemote(Args &args) {
  if (args.GetArgumentCount() == 0)
    return Status::FromError(LaunchPlatformServer());
  return PlatformWasm::ConnectRemote(args);
}

ProcessSP PlatformWebInspectorWasm::Attach(ProcessAttachInfo &attach_info,
                                           Debugger &debugger, Target *target,
                                           Status &status) {
  status = Status::FromError(EnsureConnected());
  if (status.Fail())
    return nullptr;
  return PlatformWasm::Attach(attach_info, debugger, target, status);
}

uint32_t PlatformWebInspectorWasm::FindProcesses(
    const ProcessInstanceInfoMatch &match_info,
    ProcessInstanceInfoList &proc_infos) {
  if (llvm::Error err = EnsureConnected()) {
    LLDB_LOG_ERROR(GetLog(LLDBLog::Platform), std::move(err),
                   "EnsureConnected failed: {0}");
    return 0;
  }
  return PlatformWasm::FindProcesses(match_info, proc_infos);
}

bool PlatformWebInspectorWasm::GetProcessInfo(lldb::pid_t pid,
                                              ProcessInstanceInfo &proc_info) {
  if (llvm::Error err = EnsureConnected()) {
    LLDB_LOG_ERROR(GetLog(LLDBLog::Platform), std::move(err),
                   "EnsureConnected failed: {0}");
    return false;
  }
  if (PlatformWasm::GetProcessInfo(pid, proc_info))
    return true;
  // The webinspector-wasm gdb-server only implements bulk process
  // enumeration, not per-pid lookup. Fall back to FindProcesses when the
  // per-pid query returns nothing.
  ProcessInstanceInfoMatch match;
  ProcessInstanceInfoList all;
  PlatformWasm::FindProcesses(match, all);
  for (const ProcessInstanceInfo &info : all) {
    if (info.GetProcessID() == pid) {
      proc_info = info;
      return true;
    }
  }
  return false;
}

lldb::ProcessSP
PlatformWebInspectorWasm::DebugProcess(ProcessLaunchInfo &launch_info,
                                       Debugger &debugger, Target &target,
                                       Status &error) {
  error = Status::FromErrorStringWithFormatv("{0} does not support launching",
                                             GetPluginNameStatic());
  return nullptr;
}
