#include "backends/directsound_device_discovery.h"

#include <algorithm>
#include <cstdint>
#include <iterator>
#include <sstream>
#include <string_view>
#include <utility>

#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <mmsystem.h>
#include <dsound.h>
#include <mmreg.h>
#ifndef WAVE_FORMAT_IEEE_FLOAT
#define WAVE_FORMAT_IEEE_FLOAT 0x0003
#endif
#endif

namespace aptt {
namespace {

constexpr std::int32_t kHresultInvalidArg =
    static_cast<std::int32_t>(0x80070057u);
constexpr std::int32_t kHresultNotImplemented =
    static_cast<std::int32_t>(0x80004001u);
constexpr std::uint32_t kProbeBufferMs = 100;

#if defined(_WIN32)

std::int64_t HResultCode(HRESULT result) {
  return static_cast<std::int32_t>(result);
}

template <typename T>
class ComLikePtr {
 public:
  ComLikePtr() = default;
  ComLikePtr(const ComLikePtr&) = delete;
  ComLikePtr& operator=(const ComLikePtr&) = delete;

  ~ComLikePtr() {
    if (ptr_ != nullptr) {
      ptr_->Release();
    }
  }

  T** put() { return &ptr_; }
  T* get() const { return ptr_; }
  T* operator->() const { return ptr_; }
  explicit operator bool() const { return ptr_ != nullptr; }

 private:
  T* ptr_ = nullptr;
};

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;
}

std::string GuidToString(const GUID& guid) {
  wchar_t buffer[64]{};
  const int written = StringFromGUID2(guid, buffer, 64);
  if (written <= 1) {
    return {};
  }
  return WideToUtf8(buffer);
}

constexpr GUID kWaveSubtypePcm = {
    0x00000001,
    0x0000,
    0x0010,
    {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71},
};

constexpr GUID kWaveSubtypeIeeeFloat = {
    0x00000003,
    0x0000,
    0x0010,
    {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71},
};

bool RequiresExtensibleWaveFormat(const FormatDescriptor& descriptor) {
  return descriptor.sample_type == SampleType::kInt24In32 ||
         descriptor.channels.channel_mask != 0;
}

WAVEFORMATEXTENSIBLE WaveFormatFromDescriptor(
    const FormatDescriptor& descriptor) {
  WAVEFORMATEXTENSIBLE wave{};
  wave.Format.wFormatTag = RequiresExtensibleWaveFormat(descriptor)
                               ? WAVE_FORMAT_EXTENSIBLE
                               : descriptor.sample_type == SampleType::kFloat32
                                     ? WAVE_FORMAT_IEEE_FLOAT
                                     : WAVE_FORMAT_PCM;
  wave.Format.nChannels = static_cast<WORD>(descriptor.channels.channel_count);
  wave.Format.nSamplesPerSec = descriptor.sample_rate_hz;
  wave.Format.wBitsPerSample =
      static_cast<WORD>(descriptor.container_bits_per_sample);
  wave.Format.nBlockAlign =
      static_cast<WORD>((wave.Format.nChannels * wave.Format.wBitsPerSample) / 8);
  wave.Format.nAvgBytesPerSec =
      wave.Format.nSamplesPerSec * wave.Format.nBlockAlign;
  if (wave.Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
    wave.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
    wave.Samples.wValidBitsPerSample =
        static_cast<WORD>(descriptor.valid_bits_per_sample);
    wave.dwChannelMask = descriptor.channels.channel_mask;
    wave.SubFormat = descriptor.sample_type == SampleType::kFloat32
                         ? kWaveSubtypeIeeeFloat
                         : kWaveSubtypePcm;
  } else {
    wave.Format.cbSize = 0;
  }
  return wave;
}

struct DirectSoundDeviceKey {
  bool valid = false;
  bool is_default = false;
  GUID guid{};
};

DirectSoundDeviceKey ParseDirectSoundStableId(const std::string& stable_id) {
  if (stable_id == DirectSoundDefaultStableId() || stable_id == "dsound:default") {
    return DirectSoundDeviceKey{.valid = true, .is_default = true};
  }

  constexpr std::string_view kPrefix = "directsound:";
  constexpr std::string_view kLegacyPrefix = "dsound:";
  std::string guid_text;
  if (stable_id.rfind(kPrefix, 0) == 0) {
    guid_text = stable_id.substr(kPrefix.size());
  } else if (stable_id.rfind(kLegacyPrefix, 0) == 0) {
    guid_text = stable_id.substr(kLegacyPrefix.size());
  } else {
    return {};
  }

  const std::wstring wide_guid = Utf8ToWide(guid_text);
  GUID guid{};
  if (FAILED(CLSIDFromString(wide_guid.c_str(), &guid))) {
    return {};
  }
  return DirectSoundDeviceKey{
      .valid = true,
      .is_default = false,
      .guid = guid,
  };
}

CapabilityProbeRecord QueryDirectSoundFormat(
    IDirectSound* direct_sound,
    const FormatDescriptor& candidate) {
  WAVEFORMATEXTENSIBLE wave = WaveFormatFromDescriptor(candidate);
  DSBUFFERDESC buffer_desc{};
  buffer_desc.dwSize = sizeof(buffer_desc);
  buffer_desc.dwFlags = 0;
  buffer_desc.dwBufferBytes = DirectSoundProbeBufferBytes(candidate);
  buffer_desc.lpwfxFormat = &wave.Format;

  ComLikePtr<IDirectSoundBuffer> buffer;
  const HRESULT result =
      direct_sound->CreateSoundBuffer(&buffer_desc, buffer.put(), nullptr);
  return MakeDirectSoundFormatProbeRecord(candidate, HResultCode(result));
}

struct DirectSoundEnumerationContext {
  std::vector<DeviceCapabilitySnapshot> devices;
};

BOOL CALLBACK DirectSoundEnumerationCallback(LPGUID guid,
                                             LPCWSTR description,
                                             LPCWSTR module,
                                             LPVOID context) {
  auto* typed_context =
      static_cast<DirectSoundEnumerationContext*>(context);
  const bool is_default = guid == nullptr;
  const std::string stable_id = is_default
      ? DirectSoundDefaultStableId()
      : DirectSoundGuidStableIdFromText(GuidToString(*guid));
  typed_context->devices.push_back(MakeDirectSoundDeviceSnapshot(
      stable_id,
      WideToUtf8(description),
      WideToUtf8(module),
      is_default));
  return TRUE;
}

#endif

std::string JoinCandidates(const std::vector<FormatDescriptor>& formats) {
  std::ostringstream joined;
  for (std::size_t index = 0; index < formats.size(); ++index) {
    if (index != 0) {
      joined << ',';
    }
    joined << formats[index].sample_rate_hz << "Hz/"
           << formats[index].channels.channel_count << "ch/"
           << ToString(formats[index].sample_type);
  }
  return joined.str();
}

std::string DirectSoundGuidTextFromStableId(const std::string& stable_id) {
  const std::size_t separator = stable_id.find(':');
  if (separator == std::string::npos || separator + 1 >= stable_id.size()) {
    return stable_id;
  }
  return stable_id.substr(separator + 1);
}

DeviceCapabilitySnapshot MakeDirectSoundProbeFailure(std::string stable_id,
                                                     ApiErrorInfo error) {
  DeviceInfo device;
  device.backend = BackendApi::kDirectSound;
  device.display_name = "Selected DirectSound device";
  device.stable_id = std::move(stable_id);
  device.driver_name = "DirectSound";

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kUnavailable);
  snapshot.discovery_error = std::move(error);
  snapshot.backend_fields["api"] = "DirectSound";
  snapshot.backend_fields["starts_stream"] = "false";
  return snapshot;
}

}  // namespace

FormatDescriptor DirectSoundPcmCandidate(std::uint32_t sample_rate_hz,
                                         SampleType sample_type,
                                         std::uint32_t channel_count) {
  FormatDescriptor descriptor;
  descriptor.signal_mode = SignalMode::kPcm;
  descriptor.sample_rate_hz = sample_rate_hz;
  if (sample_type == SampleType::kFloat32 ||
      sample_type == SampleType::kInt24Packed ||
      sample_type == SampleType::kInt24In32 ||
      sample_type == SampleType::kInt32) {
    descriptor.sample_type = sample_type;
  } else {
    descriptor.sample_type = SampleType::kInt16;
  }
  descriptor.channels.channel_count = channel_count;
  descriptor.channels.channel_mask = channel_count == 2 ? 3 : 0;
  if (descriptor.sample_type == SampleType::kFloat32) {
    descriptor.valid_bits_per_sample = 32;
    descriptor.container_bits_per_sample = 32;
  } else if (descriptor.sample_type == SampleType::kInt24Packed) {
    descriptor.valid_bits_per_sample = 24;
    descriptor.container_bits_per_sample = 24;
  } else if (descriptor.sample_type == SampleType::kInt24In32) {
    descriptor.valid_bits_per_sample = 24;
    descriptor.container_bits_per_sample = 32;
  } else if (descriptor.sample_type == SampleType::kInt32) {
    descriptor.valid_bits_per_sample = 32;
    descriptor.container_bits_per_sample = 32;
  } else {
    descriptor.valid_bits_per_sample = 16;
    descriptor.container_bits_per_sample = 16;
  }
  return descriptor;
}

std::vector<FormatDescriptor> DirectSoundDefaultPcmCandidates() {
  constexpr std::uint32_t kSampleRatesHz[] = {
      44100, 48000, 88200, 96000, 176400, 192000, 352800,
      384000, 705600, 768000, 786000, 1411200, 1536000};
  constexpr SampleType kSampleTypes[] = {
      SampleType::kInt16,
      SampleType::kInt24Packed,
      SampleType::kInt24In32,
      SampleType::kFloat32,
      SampleType::kInt32,
  };

  std::vector<FormatDescriptor> candidates;
  candidates.reserve(std::size(kSampleRatesHz) * std::size(kSampleTypes));
  for (const auto sample_rate_hz : kSampleRatesHz) {
    for (const auto sample_type : kSampleTypes) {
      candidates.push_back(
          DirectSoundPcmCandidate(sample_rate_hz, sample_type, 2));
    }
  }
  return candidates;
}

std::string DirectSoundDefaultStableId() {
  return "directsound:default";
}

std::string DirectSoundGuidStableIdFromText(std::string guid_text) {
  return "directsound:" + std::move(guid_text);
}

std::uint32_t DirectSoundProbeBufferBytes(const FormatDescriptor& descriptor) {
  const std::uint32_t bytes_per_sample =
      std::max<std::uint32_t>(1, descriptor.container_bits_per_sample / 8);
  const std::uint32_t block_align =
      std::max<std::uint32_t>(1, descriptor.channels.channel_count) *
      bytes_per_sample;
  std::uint32_t bytes =
      (descriptor.sample_rate_hz * block_align * kProbeBufferMs) / 1000;
  bytes = std::max<std::uint32_t>(bytes, block_align);
  const std::uint32_t remainder = bytes % block_align;
  if (remainder != 0) {
    bytes += block_align - remainder;
  }
  return bytes;
}

std::string DirectSoundResultSymbol(std::int64_t result) {
#if defined(_WIN32)
  switch (static_cast<HRESULT>(result)) {
    case DS_OK:
      return "DS_OK";
    case DSERR_ALLOCATED:
      return "DSERR_ALLOCATED";
    case DSERR_BADFORMAT:
      return "DSERR_BADFORMAT";
    case DSERR_CONTROLUNAVAIL:
      return "DSERR_CONTROLUNAVAIL";
    case DSERR_INVALIDCALL:
      return "DSERR_INVALIDCALL";
    case DSERR_INVALIDPARAM:
      return "DSERR_INVALIDPARAM";
    case DSERR_NODRIVER:
      return "DSERR_NODRIVER";
    case DSERR_OUTOFMEMORY:
      return "DSERR_OUTOFMEMORY";
    case DSERR_PRIOLEVELNEEDED:
      return "DSERR_PRIOLEVELNEEDED";
    case DSERR_UNINITIALIZED:
      return "DSERR_UNINITIALIZED";
    case DSERR_UNSUPPORTED:
      return "DSERR_UNSUPPORTED";
    default:
      break;
  }
#endif
  return "HRESULT_" + HexCode(result);
}

CapabilitySupport DirectSoundSupportFromHResult(std::int64_t result) {
  if (result == 0) {
    return CapabilitySupport::kSupported;
  }
#if defined(_WIN32)
  if (static_cast<HRESULT>(result) == DSERR_BADFORMAT ||
      static_cast<HRESULT>(result) == DSERR_CONTROLUNAVAIL ||
      static_cast<HRESULT>(result) == DSERR_UNSUPPORTED) {
    return CapabilitySupport::kUnsupported;
  }
#endif
  return CapabilitySupport::kFailed;
}

DeviceCapabilitySnapshot MakeDirectSoundDeviceSnapshot(
    std::string stable_id,
    std::string friendly_name,
    std::string driver_module,
    bool is_default) {
  DeviceInfo device;
  device.backend = BackendApi::kDirectSound;
  device.display_name = friendly_name.empty()
      ? (is_default ? "DirectSound default output" : "DirectSound output device")
      : std::move(friendly_name);
  device.stable_id = std::move(stable_id);
  device.driver_name = "DirectSound";
  device.is_default = is_default;
  device.is_known_problem_device = false;

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kActive);
  if (is_default) {
    snapshot.default_roles.push_back("default");
  }
  snapshot.backend_fields["api"] = "DirectSoundEnumerateW";
  snapshot.backend_fields["api_device_id"] =
      is_default ? "NULL-default-device" : DirectSoundGuidTextFromStableId(
                                            snapshot.device.stable_id);
  snapshot.backend_fields["device_guid"] =
      is_default ? "NULL" : DirectSoundGuidTextFromStableId(
                              snapshot.device.stable_id);
  snapshot.backend_fields["driver_module"] = std::move(driver_module);
  snapshot.backend_fields["id_kind"] =
      is_default ? "directsound-default-device" : "directsound-guid";
  snapshot.backend_fields["cooperative_level"] = "deferred";

  CapabilityProbeRecord probe;
  probe.kind = CapabilityProbeKind::kDiscoveryOnly;
  probe.support = CapabilitySupport::kSupported;
  probe.detail = is_default
      ? "DirectSound default device enumerated without playback"
      : "DirectSound device enumerated without playback";
  probe.backend_fields["opens_audio_device"] = "false";
  probe.backend_fields["creates_secondary_buffer"] = "false";
  probe.backend_fields["initializes_stream"] = "false";
  probe.backend_fields["starts_stream"] = "false";
  probe.backend_fields["plays_secondary_buffer"] = "false";
  snapshot.probes.push_back(std::move(probe));
  return snapshot;
}

CapabilityProbeRecord MakeDirectSoundFormatProbeRecord(
    const FormatDescriptor& requested,
    std::int64_t result) {
  CapabilityProbeRecord probe;
  probe.kind = CapabilityProbeKind::kFormatSupport;
  probe.requested_format = requested;
  probe.backend_fields["api"] = "IDirectSound::CreateSoundBuffer";
  probe.backend_fields["cooperative_level"] = "DSSCL_NORMAL";
  probe.backend_fields["secondary_buffer_ms"] =
      std::to_string(kProbeBufferMs);
  probe.backend_fields["secondary_buffer_bytes"] =
      std::to_string(DirectSoundProbeBufferBytes(requested));
  probe.backend_fields["opens_audio_device"] = "true";
  probe.backend_fields["creates_secondary_buffer"] = "true";
  probe.backend_fields["initializes_stream"] = "false";
  probe.backend_fields["starts_stream"] = "false";
  probe.backend_fields["plays_secondary_buffer"] = "false";
  probe.backend_fields["lock_unlock"] = "not-used";
  probe.backend_fields["restore"] = "not-used";
  probe.support = DirectSoundSupportFromHResult(result);

  if (probe.support == CapabilitySupport::kSupported) {
    probe.accepted_format = requested;
    probe.detail = "DirectSound secondary buffer format accepted without playback";
    return probe;
  }

  probe.detail = probe.support == CapabilitySupport::kUnsupported
      ? "DirectSound secondary buffer format unsupported"
      : "DirectSound secondary buffer format probe failed";
  probe.error = MakeApiError(
      result,
      DirectSoundResultSymbol(result),
      probe.detail);
  return probe;
}

DeviceCapabilitySnapshot ProbeDirectSoundDeviceFormats(
    const DirectSoundFormatProbeRequest& request) {
  if (request.stable_id.empty()) {
    return MakeDirectSoundProbeFailure(
        {},
        MakeApiError(
            kHresultInvalidArg,
            "E_INVALIDARG",
            "DirectSound format probe requires an explicit device stable ID"));
  }

#if !defined(_WIN32)
  return MakeDirectSoundProbeFailure(
      request.stable_id,
      MakeApiError(
          kHresultNotImplemented,
          "E_NOTIMPL",
          "DirectSound format probing is available only on Windows"));
#else
  const auto device_key = ParseDirectSoundStableId(request.stable_id);
  if (!device_key.valid) {
    return MakeDirectSoundProbeFailure(
        request.stable_id,
        MakeApiError(
            kHresultInvalidArg,
            "E_INVALIDARG",
            "DirectSound stable ID must be directsound:default or directsound:{GUID}"));
  }

  DeviceInfo device;
  device.backend = BackendApi::kDirectSound;
  device.display_name = device_key.is_default
      ? "Selected DirectSound default device"
      : "Selected DirectSound device";
  device.stable_id = request.stable_id;
  device.driver_name = "DirectSound";
  device.is_default = device_key.is_default;

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kActive);
  snapshot.backend_fields["api"] = "DirectSoundCreate";
  snapshot.backend_fields["id_kind"] =
      device_key.is_default ? "directsound-default-device" : "directsound-guid";
  snapshot.backend_fields["cooperative_level"] = "DSSCL_NORMAL";
  snapshot.backend_fields["opens_audio_device"] = "true";
  snapshot.backend_fields["initializes_stream"] = "false";
  snapshot.backend_fields["starts_stream"] = "false";
  snapshot.backend_fields["device_guid"] =
      device_key.is_default ? "NULL" : DirectSoundGuidTextFromStableId(
                                      request.stable_id);
  snapshot.backend_fields["candidate_formats"] =
      JoinCandidates(request.candidate_formats);

  ComLikePtr<IDirectSound> direct_sound;
  const GUID* guid = device_key.is_default ? nullptr : &device_key.guid;
  HRESULT result = DirectSoundCreate(guid, direct_sound.put(), nullptr);
  if (FAILED(result) || !direct_sound) {
    snapshot.availability = DeviceAvailability::kUnavailable;
    snapshot.discovery_error = MakeApiError(
        HResultCode(result),
        DirectSoundResultSymbol(HResultCode(result)),
        "DirectSoundCreate failed for selected device");
    return snapshot;
  }

  result = direct_sound->SetCooperativeLevel(GetDesktopWindow(), DSSCL_NORMAL);
  if (FAILED(result)) {
    CapabilityProbeRecord probe;
    probe.kind = CapabilityProbeKind::kDriverInfo;
    probe.support = CapabilitySupport::kFailed;
    probe.detail = "DirectSound cooperative level setup failed";
    probe.error = MakeApiError(
        HResultCode(result),
        DirectSoundResultSymbol(HResultCode(result)),
        probe.detail);
    probe.backend_fields["api"] = "IDirectSound::SetCooperativeLevel";
    probe.backend_fields["cooperative_level"] = "DSSCL_NORMAL";
    probe.backend_fields["starts_stream"] = "false";
    snapshot.probes.push_back(std::move(probe));
    return snapshot;
  }

  std::vector<FormatDescriptor> candidates = request.candidate_formats;
  if (candidates.empty()) {
    candidates = DirectSoundDefaultPcmCandidates();
  }

  for (const auto& candidate : candidates) {
    snapshot.probes.push_back(
        QueryDirectSoundFormat(direct_sound.get(), candidate));
  }
  return snapshot;
#endif
}

DiscoverySnapshot EnumerateDirectSoundOutputDevices() {
  DiscoverySnapshot snapshot;
  snapshot.backend = BackendApi::kDirectSound;
  snapshot.opens_audio_device = false;
  snapshot.initializes_stream = false;
  snapshot.starts_stream = false;

#if !defined(_WIN32)
  snapshot.enumeration_error = MakeApiError(
      kHresultNotImplemented,
      "E_NOTIMPL",
      "DirectSound enumeration is available only on Windows");
  return snapshot;
#else
  DirectSoundEnumerationContext context;
  const HRESULT result =
      DirectSoundEnumerateW(DirectSoundEnumerationCallback, &context);
  if (FAILED(result)) {
    snapshot.enumeration_error = MakeApiError(
        HResultCode(result),
        DirectSoundResultSymbol(HResultCode(result)),
        "DirectSoundEnumerateW failed");
    return snapshot;
  }
  snapshot.devices = std::move(context.devices);
  return snapshot;
#endif
}

}  // namespace aptt
