// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "Common/HostDisassembler.h"

#include <span>

#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>

#if defined(HAVE_LLVM)
#include <llvm-c/Disassembler.h>
#include <llvm-c/Target.h>
#elif defined(_M_X86_64)
#include <disasm.h>  // Bochs
#endif

#if defined(HAVE_LLVM)
class HostDisassemblerLLVM final : public HostDisassembler
{
public:
  explicit HostDisassemblerLLVM(const char* host_disasm, const char* cpu = "",
                                std::size_t inst_size = 0);
  ~HostDisassemblerLLVM();

private:
  LLVMDisasmContextRef m_llvm_context;
  std::size_t m_instruction_size;

  std::size_t Disassemble(const u8* begin, const u8* end, std::ostream& stream) override;
};

HostDisassemblerLLVM::HostDisassemblerLLVM(const char* host_disasm, const char* cpu,
                                           std::size_t inst_size)
    : m_instruction_size(inst_size)
{
  LLVMInitializeAllTargetInfos();
  LLVMInitializeAllTargetMCs();
  LLVMInitializeAllDisassemblers();

  m_llvm_context = LLVMCreateDisasmCPU(host_disasm, cpu, nullptr, 0, nullptr, nullptr);

  // Couldn't create llvm context
  if (!m_llvm_context)
    return;

  LLVMSetDisasmOptions(m_llvm_context, LLVMDisassembler_Option_AsmPrinterVariant |
                                           LLVMDisassembler_Option_PrintLatency);
}

HostDisassemblerLLVM::~HostDisassemblerLLVM()
{
  if (m_llvm_context)
    LLVMDisasmDispose(m_llvm_context);
}

std::size_t HostDisassemblerLLVM::Disassemble(const u8* begin, const u8* end, std::ostream& stream)
{
  std::size_t instruction_count = 0;
  if (!m_llvm_context)
    return instruction_count;

  while (begin < end)
  {
    char inst_disasm[256];

    const auto inst_size =
        LLVMDisasmInstruction(m_llvm_context, const_cast<u8*>(begin), static_cast<u64>(end - begin),
                              reinterpret_cast<u64>(begin), inst_disasm, sizeof(inst_disasm));
    if (inst_size == 0)
    {
      if (m_instruction_size != 0)
      {
        // If we are on an architecture that has a fixed instruction
        // size, we can continue onward past this bad instruction.
        fmt::println(stream, "{}\tInvalid inst: {:02x}", fmt::ptr(begin),
                     fmt::join(std::span{begin, m_instruction_size}, ""));
        begin += m_instruction_size;
      }
      else
      {
        // We can't continue if we are on an architecture that has flexible
        // instruction sizes. Dump the rest of the block instead.
        fmt::println(stream, "{}\tInvalid inst: {:02x}", fmt::ptr(begin),
                     fmt::join(std::span{begin, end}, ""));
        break;
      }
    }
    else
    {
      fmt::println(stream, "{}{}", fmt::ptr(begin), inst_disasm);
      begin += inst_size;
    }
    ++instruction_count;
  }
  return instruction_count;
}
#elif defined(_M_X86_64)
class HostDisassemblerBochs final : public HostDisassembler
{
public:
  explicit HostDisassemblerBochs();
  ~HostDisassemblerBochs() = default;

private:
  disassembler m_disasm;

  std::size_t Disassemble(const u8* begin, const u8* end, std::ostream& stream) override;
};

HostDisassemblerBochs::HostDisassemblerBochs()
{
  m_disasm.set_syntax_intel();
}

std::size_t HostDisassemblerBochs::Disassemble(const u8* begin, const u8* end, std::ostream& stream)
{
  std::size_t instruction_count = 0;
  while (begin < end)
  {
    char inst_disasm[256];

    const auto inst_size =
        m_disasm.disasm64(0, reinterpret_cast<bx_address>(begin), begin, inst_disasm);
    fmt::println(stream, "{}\t{}", fmt::ptr(begin), inst_disasm);
    begin += inst_size;
    ++instruction_count;
  }
  return instruction_count;
}
#endif

std::unique_ptr<HostDisassembler> HostDisassembler::Factory(Platform arch)
{
  switch (arch)
  {
#if defined(HAVE_LLVM)
  case Platform::x86_64:
    return std::make_unique<HostDisassemblerLLVM>("x86_64-none-unknown");
  case Platform::aarch64:
    return std::make_unique<HostDisassemblerLLVM>("aarch64-none-unknown", "cortex-a57", 4);
#elif defined(_M_X86_64)
  case Platform::x86_64:
    return std::make_unique<HostDisassemblerBochs>();
#else
  case Platform{}:  // warning C4065: "switch statement contains 'default' but no 'case' labels"
#endif
  default:
    return std::make_unique<HostDisassembler>();
  }
}

std::size_t HostDisassembler::Disassemble(const u8* begin, const u8* end, std::ostream& stream)
{
  fmt::println(stream, "{}\t{:02x}", fmt::ptr(begin), fmt::join(std::span{begin, end}, ""));
  return 0;
}