// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0

#include "common/dynamic_library.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/small_string.h"
#include "common/string_util.h"

#include "fmt/format.h"
#include <cstring>

#ifdef _WIN32
#include "common/windows_headers.h"
#else
#include <dlfcn.h>
#ifdef __APPLE__
#include "common/cocoa_tools.h"
#endif
#endif

LOG_CHANNEL(DynamicLibrary);

DynamicLibrary::DynamicLibrary() = default;

DynamicLibrary::DynamicLibrary(const char* filename)
{
  Error error;
  if (!Open(filename, &error))
    ERROR_LOG(error.GetDescription());
}

DynamicLibrary::DynamicLibrary(DynamicLibrary&& move) : m_handle(move.m_handle)
{
  move.m_handle = nullptr;
}

DynamicLibrary::~DynamicLibrary()
{
  Close();
}

std::string DynamicLibrary::GetUnprefixedFilename(const char* filename)
{
#if defined(_WIN32)
  return std::string(filename) + ".dll";
#elif defined(__APPLE__)
  return std::string(filename) + ".dylib";
#else
  return std::string(filename) + ".so";
#endif
}

std::string DynamicLibrary::GetVersionedFilename(const char* libname, int major, int minor, int patch)
{
#if defined(_WIN32)
  if (major >= 0 && minor >= 0 && patch >= 0)
    return fmt::format("{}-{}-{}-{}.dll", libname, major, minor, patch);
  else if (major >= 0 && minor >= 0)
    return fmt::format("{}-{}-{}.dll", libname, major, minor);
  else if (major >= 0)
    return fmt::format("{}-{}.dll", libname, major);
  else
    return fmt::format("{}.dll", libname);
#elif defined(__APPLE__)
  const char* prefix = std::strncmp(libname, "lib", 3) ? "lib" : "";
  if (major >= 0 && minor >= 0 && patch >= 0)
    return fmt::format("{}{}.{}.{}.{}.dylib", prefix, libname, major, minor, patch);
  else if (major >= 0 && minor >= 0)
    return fmt::format("{}{}.{}.{}.dylib", prefix, libname, major, minor);
  else if (major >= 0)
    return fmt::format("{}{}.{}.dylib", prefix, libname, major);
  else
    return fmt::format("{}{}.dylib", prefix, libname);
#else
  const char* prefix = std::strncmp(libname, "lib", 3) ? "lib" : "";
  if (major >= 0 && minor >= 0 && patch >= 0)
    return fmt::format("{}{}.so.{}.{}.{}", prefix, libname, major, minor, patch);
  else if (major >= 0 && minor >= 0)
    return fmt::format("{}{}.so.{}.{}", prefix, libname, major, minor);
  else if (major >= 0)
    return fmt::format("{}{}.so.{}", prefix, libname, major);
  else
    return fmt::format("{}{}.so", prefix, libname);
#endif
}

std::string DynamicLibrary::GetBundledLibraryPath(const char* libname, int major /*= -1*/, int minor /*= -1*/,
                                                  int patch /*= -1*/)
{
  Error error;
  std::string program_path = FileSystem::GetProgramPath(&error);
  if (program_path.empty()) [[unlikely]]
  {
    ERROR_LOG("Failed to get program path: {}", error.GetDescription());
    return {};
  }

#ifdef __APPLE__
  program_path = Path::Combine(Path::GetDirectory(program_path), ".." FS_OSPATH_SEPARATOR_STR "Frameworks");
  Path::Canonicalize(&program_path);
  return Path::Combine(program_path, GetVersionedFilename(libname, major, minor, patch));
#else
  return Path::Combine(Path::GetDirectory(program_path), GetVersionedFilename(libname, major, minor, patch));
#endif
}

bool DynamicLibrary::Open(const char* filename, Error* error)
{
#ifdef _WIN32
  m_handle = reinterpret_cast<void*>(LoadLibraryW(StringUtil::UTF8StringToWideString(filename).c_str()));
  if (!m_handle)
  {
    Error::SetWin32(error, TinyString::from_format("Loading {} failed: ", filename), GetLastError());
    return false;
  }

  return true;
#else
  m_handle = dlopen(filename, RTLD_NOW);
  if (!m_handle)
  {
#ifdef __APPLE__
    // On MacOS, try searching in Frameworks.
    if (!Path::IsAbsolute(filename))
    {
      std::optional<std::string> bundle_path = CocoaTools::GetBundlePath();
      if (bundle_path.has_value())
      {
        std::string frameworks_path = fmt::format("{}/Contents/Frameworks/{}", bundle_path.value(), filename);
        if (FileSystem::FileExists(frameworks_path.c_str()))
        {
          m_handle = dlopen(frameworks_path.c_str(), RTLD_NOW);
          if (m_handle)
          {
            Error::Clear(error);
            return true;
          }
        }
      }
    }
#endif

    const char* err = dlerror();
    Error::SetStringFmt(error, "Loading {} failed: {}", filename, err ? err : "<UNKNOWN>");
    return false;
  }

  return true;
#endif
}

void DynamicLibrary::Adopt(void* handle)
{
  AssertMsg(handle, "Handle is valid");

  Close();

  m_handle = handle;
}

void DynamicLibrary::Close()
{
  if (!IsOpen())
    return;

#ifdef _WIN32
  FreeLibrary(reinterpret_cast<HMODULE>(m_handle));
#else
  dlclose(m_handle);
#endif
  m_handle = nullptr;
}

void* DynamicLibrary::GetSymbolAddress(const char* name) const
{
#ifdef _WIN32
  return reinterpret_cast<void*>(GetProcAddress(reinterpret_cast<HMODULE>(m_handle), name));
#else
  return reinterpret_cast<void*>(dlsym(m_handle, name));
#endif
}

bool DynamicLibrary::ResolveSymbols(const SymbolTable* symbols, size_t count, Error* error /* = nullptr */) const
{
  for (size_t i = 0; i < count; i++)
  {
    if (!GetSymbol(symbols[i].name, symbols[i].ptr))
    {
      Error::SetStringFmt(error, "Failed to load symbol {} from library", symbols[i].name);
      ClearSymbols(symbols, count);
      return false;
    }
  }

  return true;
}

bool DynamicLibrary::ResolveSymbols(const OptionalSymbolTable* symbols, size_t count,
                                    Error* error /* = nullptr */) const
{
  for (size_t i = 0; i < count; i++)
  {
    if (!GetSymbol(symbols[i].name, symbols[i].ptr))
    {
      if (symbols[i].required)
      {
        Error::SetStringFmt(error, "Failed to load required symbol {} from library", symbols[i].name);
        ClearSymbols(symbols, count);
        return false;
      }
      else
      {
        WARNING_LOG("Failed to load optional symbol {} from library", symbols[i].name);
      }
    }
  }

  return true;
}

bool DynamicLibrary::ResolveSymbols(const std::span<const SymbolTable> symbols, Error* error /*= nullptr*/) const
{
  return ResolveSymbols(symbols.data(), symbols.size(), error);
}

bool DynamicLibrary::ResolveSymbols(const std::span<const OptionalSymbolTable> symbols,
                                    Error* error /*= nullptr*/) const
{
  return ResolveSymbols(symbols.data(), symbols.size(), error);
}

void DynamicLibrary::ClearSymbols(const SymbolTable* symbols, size_t count)
{
  for (size_t i = 0; i < count; i++)
    *symbols[i].ptr = nullptr;
}

void DynamicLibrary::ClearSymbols(const OptionalSymbolTable* symbols, size_t count)
{
  for (size_t i = 0; i < count; i++)
    *symbols[i].ptr = nullptr;
}

void DynamicLibrary::ClearSymbols(const std::span<const SymbolTable> symbols)
{
  ClearSymbols(symbols.data(), symbols.size());
}

void DynamicLibrary::ClearSymbols(const std::span<const OptionalSymbolTable> symbols)
{
  ClearSymbols(symbols.data(), symbols.size());
}

DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& move)
{
  Close();
  m_handle = move.m_handle;
  move.m_handle = nullptr;
  return *this;
}
