//===----------------------------------------------------------------------===//
//
// 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 "Plugins/Platform/WebAssembly/PlatformWasm.h"
#include "Plugins/Platform/WebAssembly/PlatformWasmRemoteGDBServer.h"
#include "Plugins/Process/wasm/ProcessWasm.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/ProcessLaunchInfo.h"
#include "lldb/Host/common/TCPSocket.h"
#include "lldb/Interpreter/OptionValueProperties.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Listener.h"
#include "lldb/Utility/Log.h"
#include "llvm/ADT/StringExtras.h"

using namespace lldb;
using namespace lldb_private;

LLDB_PLUGIN_DEFINE(PlatformWasm)

namespace {
#define LLDB_PROPERTIES_platformwasm
#include "PlatformWasmProperties.inc"

enum {
#define LLDB_PROPERTIES_platformwasm
#include "PlatformWasmPropertiesEnum.inc"
};

class PluginProperties : public Properties {
public:
  PluginProperties() {
    m_collection_sp = std::make_shared<OptionValueProperties>(
        PlatformWasm::GetPluginNameStatic());
    m_collection_sp->Initialize(g_platformwasm_properties);
  }

  FileSpec GetRuntimePath() const {
    return GetPropertyAtIndexAs<FileSpec>(ePropertyRuntimePath, {});
  }

  Args GetRuntimeArgs() const {
    Args result;
    m_collection_sp->GetPropertyAtIndexAsArgs(ePropertyRuntimeArgs, result);
    return result;
  }

  llvm::StringRef GetPortArg() const {
    return GetPropertyAtIndexAs<llvm::StringRef>(ePropertyPortArg, {});
  }
};

} // namespace

static PluginProperties &GetGlobalProperties() {
  static PluginProperties g_settings;
  return g_settings;
}

llvm::StringRef PlatformWasm::GetPluginDescriptionStatic() {
  return "Platform for debugging Wasm";
}

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

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

void PlatformWasm::DebuggerInitialize(Debugger &debugger) {
  if (!PluginManager::GetSettingForPlatformPlugin(debugger,
                                                  GetPluginNameStatic())) {
    PluginManager::CreateSettingForPlatformPlugin(
        debugger, GetGlobalProperties().GetValueProperties(),
        "Properties for the wasm platform plugin.",
        /*is_global_property=*/true);
  }
}

PlatformSP PlatformWasm::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>");

  bool create = force;
  if (!create && arch && arch->IsValid()) {
    const llvm::Triple &triple = arch->GetTriple();
    switch (triple.getArch()) {
    case llvm::Triple::wasm32:
    case llvm::Triple::wasm64:
      create = true;
      break;
    default:
      break;
    }
  }

  LLDB_LOG(log, "create = {0}", create);
  return create ? PlatformSP(new PlatformWasm()) : PlatformSP();
}

std::vector<ArchSpec>
PlatformWasm::GetSupportedArchitectures(const ArchSpec &process_host_arch) {
  return {ArchSpec("wasm32-unknown-unknown-wasm"),
          ArchSpec("wasm64-unknown-unknown-wasm")};
}

static auto get_arg_range(const Args &args) {
  return llvm::make_range(args.GetArgumentArrayRef().begin(),
                          args.GetArgumentArrayRef().end());
}

lldb::ProcessSP PlatformWasm::Attach(ProcessAttachInfo &attach_info,
                                     Debugger &debugger, Target *target,
                                     Status &status) {
  if (m_remote_platform_sp)
    return m_remote_platform_sp->Attach(attach_info, debugger, target, status);

  status = Status::FromErrorString(
      "attaching is only supported when connected to a remote Wasm platform");
  return nullptr;
}

lldb::ProcessSP PlatformWasm::DebugProcess(ProcessLaunchInfo &launch_info,
                                           Debugger &debugger, Target &target,
                                           Status &error) {
  if (m_remote_platform_sp)
    return m_remote_platform_sp->DebugProcess(launch_info, debugger, target,
                                              error);

  Log *log = GetLog(LLDBLog::Platform);

  const PluginProperties &properties = GetGlobalProperties();

  FileSpec runtime = properties.GetRuntimePath();
  FileSystem::Instance().ResolveExecutableLocation(runtime);

  if (!FileSystem::Instance().Exists(runtime)) {
    error = Status::FromErrorStringWithFormatv(
        "WebAssembly runtime does not exist: {0}", runtime.GetPath());
    return nullptr;
  }

  uint16_t port = 0;
  {
    // Get the next available port by binding a socket to port 0.
    TCPSocket listen_socket(true);
    error = listen_socket.Listen("localhost:0", /*backlog=*/5);
    if (error.Fail())
      return nullptr;
    port = listen_socket.GetLocalPortNumber();
  }

  if (error.Fail())
    return nullptr;

  Args args({runtime.GetPath(),
             llvm::formatv("{0}{1}", properties.GetPortArg(), port).str()});
  args.AppendArguments(properties.GetRuntimeArgs());
  args.AppendArguments(launch_info.GetArguments());

  launch_info.SetArguments(args, true);
  launch_info.SetLaunchInSeparateProcessGroup(true);
  launch_info.GetFlags().Clear(eLaunchFlagDebug);

  auto exit_code = std::make_shared<std::optional<int>>();
  launch_info.SetMonitorProcessCallback(
      [=](lldb::pid_t pid, int signal, int status) {
        LLDB_LOG(
            log,
            "WebAssembly runtime exited: pid = {0}, signal = {1}, status = {2}",
            pid, signal, status);
        exit_code->emplace(status);
      });

  // This is automatically done for host platform in
  // Target::FinalizeFileActions, but we're not a host platform.
  llvm::Error Err = launch_info.SetUpPtyRedirection();
  LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}");

  LLDB_LOG(log, "{0}", get_arg_range(launch_info.GetArguments()));
  error = Host::LaunchProcess(launch_info);
  if (error.Fail())
    return nullptr;

  ProcessSP process_sp = target.CreateProcess(
      launch_info.GetListener(), wasm::ProcessWasm::GetPluginNameStatic(),
      nullptr, true);
  if (!process_sp) {
    error = Status::FromErrorString("failed to create WebAssembly process");
    return nullptr;
  }

  process_sp->HijackProcessEvents(launch_info.GetHijackListener());

  error = process_sp->ConnectRemote(
      llvm::formatv("connect://localhost:{0}", port).str());
  if (error.Fail()) {
    // If we know the runtime has exited, that's a better error message than
    // failing to connect.
    if (*exit_code)
      error = Status::FromError(llvm::joinErrors(
          llvm::createStringError(llvm::formatv(
              "WebAssembly runtime exited with exit code {0}", **exit_code)),
          error.takeError()));

    return nullptr;
  }
#ifndef _WIN32
  if (launch_info.GetPTY().GetPrimaryFileDescriptor() !=
      PseudoTerminal::invalid_fd)
    process_sp->SetSTDIOFileDescriptor(
        launch_info.GetPTY().ReleasePrimaryFileDescriptor());
#endif
  return process_sp;
}

Status PlatformWasm::ConnectRemote(Args &args) {
  if (IsHost())
    return Status::FromErrorString(
        "can't connect to the host platform, always connected");

  if (!m_remote_platform_sp)
    m_remote_platform_sp = PlatformSP(new PlatformWasmRemoteGDBServer());

  return m_remote_platform_sp->ConnectRemote(args);
}
