#include "backends/mme_device_discovery.h"

#include <array>
#include <charconv>
#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 <mmreg.h>
#ifndef WAVE_FORMAT_IEEE_FLOAT
#define WAVE_FORMAT_IEEE_FLOAT 0x0003
#endif
#endif

namespace aptt {
namespace {

constexpr std::int32_t kHresultNotImplemented =
    static_cast<std::int32_t>(0x80004001u);

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

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

CapabilityProbeRecord QueryMmeFormat(std::uint32_t device_id,
                                      const FormatDescriptor& candidate) {
  WAVEFORMATEXTENSIBLE wave = WaveFormatFromDescriptor(candidate);
  const MMRESULT result = waveOutOpen(
      nullptr,
      static_cast<UINT>(device_id),
      &wave.Format,
      0,
      0,
      WAVE_FORMAT_QUERY);
  return MakeMmeFormatProbeRecord(candidate, static_cast<std::uint32_t>(result));
}

DeviceCapabilitySnapshot ReadMmeDevice(std::uint32_t device_id) {
  WAVEOUTCAPSW caps{};
  const MMRESULT result = waveOutGetDevCapsW(
      static_cast<UINT_PTR>(device_id),
      &caps,
      sizeof(caps));
  if (result != MMSYSERR_NOERROR) {
    DeviceInfo device;
    device.backend = BackendApi::kMme;
    device.display_name = "MME waveOut device unavailable";
    device.stable_id = MmeDeviceStableId(device_id);
    device.driver_name = "MME/waveOut";
    auto snapshot = MakeDiscoveredDeviceSnapshot(
        std::move(device),
        DeviceAvailability::kUnavailable);
    snapshot.discovery_error = MakeApiError(
        result,
        MmeResultSymbol(result),
        "waveOutGetDevCaps failed");
    return snapshot;
  }

  auto snapshot = MakeMmeWaveOutDeviceSnapshot(
      device_id,
      WideToUtf8(caps.szPname),
      caps.dwFormats,
      caps.wChannels,
      false);
  snapshot.backend_fields["manufacturer_id"] = std::to_string(caps.wMid);
  snapshot.backend_fields["product_id"] = std::to_string(caps.wPid);
  snapshot.backend_fields["driver_version"] = std::to_string(caps.vDriverVersion);
  snapshot.backend_fields["support_flags"] = HexCode(caps.dwSupport);
  return snapshot;
}

#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();
}

}  // namespace

FormatDescriptor MmePcmCandidate(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;
  descriptor.sample_type = sample_type;
  descriptor.channels.channel_count = channel_count;
  descriptor.channels.channel_mask = channel_count == 2 ? 3 : 0;
  switch (sample_type) {
    case SampleType::kFloat32:
      descriptor.valid_bits_per_sample = 32;
      descriptor.container_bits_per_sample = 32;
      break;
    case SampleType::kInt24Packed:
      descriptor.valid_bits_per_sample = 24;
      descriptor.container_bits_per_sample = 24;
      break;
    case SampleType::kInt24In32:
      descriptor.valid_bits_per_sample = 24;
      descriptor.container_bits_per_sample = 32;
      break;
    case SampleType::kInt32:
      descriptor.valid_bits_per_sample = 32;
      descriptor.container_bits_per_sample = 32;
      break;
    case SampleType::kInt16:
    default:
      descriptor.sample_type = SampleType::kInt16;
      descriptor.valid_bits_per_sample = 16;
      descriptor.container_bits_per_sample = 16;
      break;
  }
  return descriptor;
}

std::vector<FormatDescriptor> MmeDefaultPcmCandidates() {
  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(MmePcmCandidate(sample_rate_hz, sample_type, 2));
    }
  }
  return candidates;
}

std::string MmeDeviceStableId(std::uint32_t device_id) {
  if (device_id == kMmeWaveMapperId) {
    return "mme:wave-mapper";
  }
  return "mme:waveout:" + std::to_string(device_id);
}

std::optional<std::uint32_t> MmeDeviceIdFromStableId(const std::string& stable_id) {
  if (stable_id == "mme:wave-mapper") {
    return kMmeWaveMapperId;
  }
  constexpr std::string_view kPrefix = "mme:waveout:";
  if (stable_id.rfind(kPrefix, 0) != 0) {
    return std::nullopt;
  }
  std::uint32_t value = 0;
  const char* begin = stable_id.data() + kPrefix.size();
  const char* end = stable_id.data() + stable_id.size();
  const auto result = std::from_chars(begin, end, value);
  if (result.ec != std::errc{} || result.ptr != end) {
    return std::nullopt;
  }
  return value;
}

std::string MmeResultSymbol(std::uint32_t result) {
#if defined(_WIN32)
  switch (result) {
    case MMSYSERR_NOERROR:
      return "MMSYSERR_NOERROR";
    case MMSYSERR_ERROR:
      return "MMSYSERR_ERROR";
    case MMSYSERR_BADDEVICEID:
      return "MMSYSERR_BADDEVICEID";
    case MMSYSERR_NOTENABLED:
      return "MMSYSERR_NOTENABLED";
    case MMSYSERR_ALLOCATED:
      return "MMSYSERR_ALLOCATED";
    case MMSYSERR_INVALHANDLE:
      return "MMSYSERR_INVALHANDLE";
    case MMSYSERR_NODRIVER:
      return "MMSYSERR_NODRIVER";
    case MMSYSERR_NOMEM:
      return "MMSYSERR_NOMEM";
    case MMSYSERR_NOTSUPPORTED:
      return "MMSYSERR_NOTSUPPORTED";
    case MMSYSERR_INVALPARAM:
      return "MMSYSERR_INVALPARAM";
    case WAVERR_BADFORMAT:
      return "WAVERR_BADFORMAT";
    case WAVERR_SYNC:
      return "WAVERR_SYNC";
    default:
      break;
  }
#endif
  return "MMRESULT_" + std::to_string(result);
}

CapabilitySupport MmeSupportFromWaveOutOpenResult(std::uint32_t result) {
#if defined(_WIN32)
  if (result == MMSYSERR_NOERROR) {
    return CapabilitySupport::kSupported;
  }
  if (result == WAVERR_BADFORMAT) {
    return CapabilitySupport::kUnsupported;
  }
#else
  if (result == 0) {
    return CapabilitySupport::kSupported;
  }
#endif
  return CapabilitySupport::kFailed;
}

DeviceCapabilitySnapshot MakeMmeWaveOutDeviceSnapshot(
    std::uint32_t device_id,
    std::string product_name,
    std::uint32_t formats_mask,
    std::uint16_t channel_count,
    bool is_mapper) {
  DeviceInfo device;
  device.backend = BackendApi::kMme;
  device.display_name =
      product_name.empty() ? "MME waveOut device" : std::move(product_name);
  device.stable_id = MmeDeviceStableId(device_id);
  device.driver_name = "MME/waveOut";
  device.is_default = is_mapper;
  device.is_known_problem_device = false;

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kActive);
  snapshot.backend_fields["api"] = "waveOut";
  snapshot.backend_fields["device_id"] =
      is_mapper ? "WAVE_MAPPER" : std::to_string(device_id);
  snapshot.backend_fields["id_kind"] = is_mapper ? "mme-wave-mapper"
                                                 : "mme-waveout-device-id";
  snapshot.backend_fields["is_mapper"] = is_mapper ? "true" : "false";
  snapshot.backend_fields["formats_mask"] = HexCode(formats_mask);
  snapshot.backend_fields["reported_channels"] = std::to_string(channel_count);

  CapabilityProbeRecord probe;
  probe.kind = CapabilityProbeKind::kDiscoveryOnly;
  probe.support = CapabilitySupport::kSupported;
  probe.detail = is_mapper
      ? "MME WAVE_MAPPER represented without playback"
      : "MME waveOut device enumerated without playback";
  probe.backend_fields["opens_audio_device"] = "false";
  probe.backend_fields["initializes_stream"] = "false";
  probe.backend_fields["starts_stream"] = "false";
  snapshot.probes.push_back(std::move(probe));
  return snapshot;
}

CapabilityProbeRecord MakeMmeFormatProbeRecord(
    const FormatDescriptor& requested,
    std::uint32_t result) {
  CapabilityProbeRecord probe;
  probe.kind = CapabilityProbeKind::kFormatSupport;
  probe.requested_format = requested;
  probe.backend_fields["api"] = "waveOutOpen";
  probe.backend_fields["flags"] = "WAVE_FORMAT_QUERY";
  probe.backend_fields["opens_audio_device"] = "false";
  probe.backend_fields["initializes_stream"] = "false";
  probe.backend_fields["starts_stream"] = "false";
  probe.backend_fields["callback"] = "none";
  probe.support = MmeSupportFromWaveOutOpenResult(result);

  if (probe.support == CapabilitySupport::kSupported) {
    probe.accepted_format = requested;
    probe.detail = "MME PCM format query supported";
    return probe;
  }

  probe.detail = probe.support == CapabilitySupport::kUnsupported
      ? "MME PCM format query unsupported"
      : "MME PCM format query failed";
  probe.error = MakeApiError(
      result,
      MmeResultSymbol(result),
      probe.detail);
  return probe;
}

DeviceCapabilitySnapshot ProbeMmeWaveOutFormats(
    const MmeFormatProbeRequest& request) {
  DeviceInfo device;
  device.backend = BackendApi::kMme;
  device.display_name = request.device_id == kMmeWaveMapperId
      ? "MME WAVE_MAPPER"
      : "MME waveOut device";
  device.stable_id = MmeDeviceStableId(request.device_id);
  device.driver_name = "MME/waveOut";
  device.is_default = request.device_id == kMmeWaveMapperId;

  auto snapshot = MakeDiscoveredDeviceSnapshot(
      std::move(device),
      DeviceAvailability::kActive);
  snapshot.backend_fields["api"] = "waveOut";
  snapshot.backend_fields["device_id"] =
      request.device_id == kMmeWaveMapperId
          ? "WAVE_MAPPER"
          : std::to_string(request.device_id);
  snapshot.backend_fields["candidate_formats"] =
      JoinCandidates(request.candidate_formats);

#if !defined(_WIN32)
  snapshot.discovery_error = MakeApiError(
      kHresultNotImplemented,
      "E_NOTIMPL",
      "MME waveOut format probing is available only on Windows");
  return snapshot;
#else
  std::vector<FormatDescriptor> candidates = request.candidate_formats;
  if (candidates.empty()) {
    candidates = MmeDefaultPcmCandidates();
  }
  for (const auto& candidate : candidates) {
    snapshot.probes.push_back(QueryMmeFormat(request.device_id, candidate));
  }
  return snapshot;
#endif
}

DiscoverySnapshot EnumerateMmeWaveOutDevices() {
  DiscoverySnapshot snapshot;
  snapshot.backend = BackendApi::kMme;
  snapshot.opens_audio_device = false;
  snapshot.initializes_stream = false;
  snapshot.starts_stream = false;

#if !defined(_WIN32)
  snapshot.enumeration_error = MakeApiError(
      kHresultNotImplemented,
      "E_NOTIMPL",
      "MME waveOut enumeration is available only on Windows");
  return snapshot;
#else
  auto mapper = MakeMmeWaveOutDeviceSnapshot(
      kMmeWaveMapperId,
      "MME WAVE_MAPPER",
      0,
      0,
      true);
  for (const auto& candidate : MmeDefaultPcmCandidates()) {
    mapper.probes.push_back(QueryMmeFormat(kMmeWaveMapperId, candidate));
  }
  snapshot.devices.push_back(std::move(mapper));

  const UINT count = waveOutGetNumDevs();
  snapshot.devices.reserve(static_cast<std::size_t>(count) + 1);
  for (UINT id = 0; id < count; ++id) {
    auto device = ReadMmeDevice(id);
    if (device.availability == DeviceAvailability::kActive) {
      for (const auto& candidate : MmeDefaultPcmCandidates()) {
        device.probes.push_back(QueryMmeFormat(id, candidate));
      }
    }
    snapshot.devices.push_back(std::move(device));
  }
  return snapshot;
#endif
}

}  // namespace aptt
