#include "backends/asio_driver_discovery.h"

#include <utility>

#ifndef APTT_ENABLE_ASIO
#define APTT_ENABLE_ASIO 0
#endif

#if APTT_ENABLE_ASIO && defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif

namespace aptt {
namespace {

constexpr std::int64_t kAsioDisabledCode = 0xA51000;
constexpr std::int64_t kAsioNotImplementedCode = 0xA51001;
constexpr std::int64_t kAsioRegistryOpenFailedCode = 0xA51002;

#if APTT_ENABLE_ASIO && defined(_WIN32)

std::string WideToUtf8(const wchar_t* value) {
  if (value == nullptr || value[0] == L'\0') {
    return {};
  }
  const int required =
      WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
  if (required <= 1) {
    return {};
  }
  std::string converted(static_cast<std::size_t>(required - 1), '\0');
  WideCharToMultiByte(
      CP_UTF8, 0, value, -1, converted.data(), required, nullptr, nullptr);
  return converted;
}

std::wstring Utf8ToWide(const std::string& value) {
  if (value.empty()) {
    return {};
  }
  const int required =
      MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
  if (required <= 1) {
    return {};
  }
  std::wstring converted(static_cast<std::size_t>(required - 1), L'\0');
  MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, converted.data(), required);
  return converted;
}

REGSAM ProcessRegistryViewFlag() {
  return CurrentProcessBitness() == 64 ? KEY_WOW64_64KEY : KEY_WOW64_32KEY;
}

std::string QueryStringValue(HKEY key, const wchar_t* value_name) {
  DWORD type = 0;
  DWORD bytes = 0;
  LONG result = RegQueryValueExW(
      key,
      value_name,
      nullptr,
      &type,
      nullptr,
      &bytes);
  if (result != ERROR_SUCCESS ||
      (type != REG_SZ && type != REG_EXPAND_SZ) ||
      bytes < sizeof(wchar_t)) {
    return {};
  }

  std::wstring value(bytes / sizeof(wchar_t), L'\0');
  result = RegQueryValueExW(
      key,
      value_name,
      nullptr,
      &type,
      reinterpret_cast<LPBYTE>(value.data()),
      &bytes);
  if (result != ERROR_SUCCESS) {
    return {};
  }
  if (!value.empty() && value.back() == L'\0') {
    value.pop_back();
  }
  return WideToUtf8(value.c_str());
}

std::string QueryDefaultStringValue(HKEY root, const std::wstring& subkey) {
  HKEY key = nullptr;
  const LONG open_result = RegOpenKeyExW(
      root,
      subkey.c_str(),
      0,
      KEY_READ | ProcessRegistryViewFlag(),
      &key);
  if (open_result != ERROR_SUCCESS || key == nullptr) {
    return {};
  }
  const std::string value = QueryStringValue(key, nullptr);
  RegCloseKey(key);
  return value;
}

std::string FindDriverPathFromClsid(const std::string& clsid) {
  if (clsid.empty()) {
    return {};
  }
  const std::wstring clsid_wide = Utf8ToWide(clsid);
  if (clsid_wide.empty()) {
    return {};
  }
  return QueryDefaultStringValue(
      HKEY_CLASSES_ROOT,
      L"CLSID\\" + clsid_wide + L"\\InprocServer32");
}

#endif

CapabilityProbeRecord MakeAsioDriverInfoProbe(bool supported) {
  CapabilityProbeRecord probe;
  probe.kind = CapabilityProbeKind::kDriverInfo;
  probe.support = supported ? CapabilitySupport::kSupported
                            : CapabilitySupport::kFailed;
  probe.detail = supported
      ? "ASIO driver registry entry discovered without driver instantiation"
      : "ASIO integration is disabled at build time";
  probe.backend_fields["opens_driver"] = "false";
  probe.backend_fields["creates_buffers"] = "false";
  probe.backend_fields["starts_stream"] = "false";
  probe.backend_fields["opens_control_panel"] = "false";
  probe.backend_fields["control_panel"] = "manual-only";
  return probe;
}

}  // namespace

bool IsAsioSdkEnabled() {
#if APTT_ENABLE_ASIO
  return true;
#else
  return false;
#endif
}

std::uint32_t CurrentProcessBitness() {
  return static_cast<std::uint32_t>(sizeof(void*) * 8);
}

std::string AsioRegistryViewForProcess() {
  return CurrentProcessBitness() == 64 ? "64-bit" : "32-bit";
}

std::string AsioStableIdFromClsid(const std::string& clsid) {
  return clsid.empty() ? "asio:unknown" : "asio:" + clsid;
}

DeviceCapabilitySnapshot MakeAsioDriverSnapshot(
    std::string registry_name,
    std::string clsid,
    std::string driver_path,
    std::string description) {
  DeviceInfo device;
  device.backend = BackendApi::kAsio;
  device.display_name = description.empty() ? registry_name : description;
  device.stable_id = AsioStableIdFromClsid(clsid);
  device.driver_name = registry_name.empty() ? "ASIO driver" : registry_name;
  device.is_known_problem_device = true;

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kActive);
  snapshot.backend_fields["registry_name"] = std::move(registry_name);
  snapshot.backend_fields["clsid"] = clsid;
  snapshot.backend_fields["driver_path"] = std::move(driver_path);
  snapshot.backend_fields["process_bitness"] =
      std::to_string(CurrentProcessBitness());
  snapshot.backend_fields["registry_view"] = AsioRegistryViewForProcess();
  snapshot.backend_fields["asio_enabled"] = IsAsioSdkEnabled() ? "true" : "false";
  snapshot.backend_fields["opens_driver"] = "false";
  snapshot.backend_fields["creates_buffers"] = "false";
  snapshot.backend_fields["starts_stream"] = "false";
  snapshot.backend_fields["opens_control_panel"] = "false";
  snapshot.probes.push_back(MakeAsioDriverInfoProbe(true));
  return snapshot;
}

DeviceCapabilitySnapshot MakeAsioDisabledSnapshot() {
  DeviceInfo device;
  device.backend = BackendApi::kAsio;
  device.display_name = "ASIO integration disabled";
  device.stable_id = "asio:disabled";
  device.driver_name = "ASIO";
  device.is_known_problem_device = true;

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kUnavailable);
  snapshot.discovery_error = MakeApiError(
      kAsioDisabledCode,
      "APTT_ENABLE_ASIO_OFF",
      "ASIO integration is disabled at build time");
  snapshot.backend_fields["process_bitness"] =
      std::to_string(CurrentProcessBitness());
  snapshot.backend_fields["registry_view"] = AsioRegistryViewForProcess();
  snapshot.backend_fields["asio_enabled"] = "false";
  snapshot.backend_fields["sdk_compiled"] = "false";
  snapshot.backend_fields["opens_driver"] = "false";
  snapshot.backend_fields["creates_buffers"] = "false";
  snapshot.backend_fields["starts_stream"] = "false";
  snapshot.backend_fields["opens_control_panel"] = "false";
  snapshot.probes.push_back(MakeAsioDriverInfoProbe(false));
  return snapshot;
}

DiscoverySnapshot EnumerateAsioDrivers() {
  DiscoverySnapshot snapshot;
  snapshot.backend = BackendApi::kAsio;
  snapshot.opens_audio_device = false;
  snapshot.initializes_stream = false;
  snapshot.starts_stream = false;

  if (!IsAsioSdkEnabled()) {
    snapshot.enumeration_error = MakeApiError(
        kAsioDisabledCode,
        "APTT_ENABLE_ASIO_OFF",
        "ASIO integration is disabled at build time");
    snapshot.devices.push_back(MakeAsioDisabledSnapshot());
    return snapshot;
  }

#if !(APTT_ENABLE_ASIO && defined(_WIN32))
  snapshot.enumeration_error = MakeApiError(
      kAsioNotImplementedCode,
      "E_NOTIMPL",
      "ASIO driver discovery is available only on Windows with ENABLE_ASIO");
  return snapshot;
#else
  HKEY asio_root = nullptr;
  const LONG open_result = RegOpenKeyExW(
      HKEY_LOCAL_MACHINE,
      L"SOFTWARE\\ASIO",
      0,
      KEY_READ | ProcessRegistryViewFlag(),
      &asio_root);
  if (open_result != ERROR_SUCCESS || asio_root == nullptr) {
    snapshot.enumeration_error = MakeApiError(
        kAsioRegistryOpenFailedCode,
        "ASIO_REGISTRY_OPEN_FAILED",
        "failed to open HKLM\\SOFTWARE\\ASIO for the current process bitness");
    return snapshot;
  }

  DWORD index = 0;
  wchar_t subkey_name[256] = {};
  DWORD subkey_chars = 256;
  while (RegEnumKeyExW(
             asio_root,
             index++,
             subkey_name,
             &subkey_chars,
             nullptr,
             nullptr,
             nullptr,
             nullptr) == ERROR_SUCCESS) {
    HKEY driver_key = nullptr;
    const LONG driver_open_result = RegOpenKeyExW(
        asio_root,
        subkey_name,
        0,
        KEY_READ | ProcessRegistryViewFlag(),
        &driver_key);
    if (driver_open_result == ERROR_SUCCESS && driver_key != nullptr) {
      std::string registry_name = WideToUtf8(subkey_name);
      std::string clsid = QueryStringValue(driver_key, L"CLSID");
      std::string description = QueryStringValue(driver_key, L"description");
      std::string driver_path = FindDriverPathFromClsid(clsid);
      snapshot.devices.push_back(MakeAsioDriverSnapshot(
          std::move(registry_name),
          std::move(clsid),
          std::move(driver_path),
          std::move(description)));
      RegCloseKey(driver_key);
    }
    subkey_chars = 256;
  }

  RegCloseKey(asio_root);
  return snapshot;
#endif
}

}  // namespace aptt
