#include "core/device_discovery.h"

#include <charconv>
#include <iomanip>
#include <sstream>
#include <utility>

namespace aptt {
namespace {

std::string EscapeJson(std::string_view value) {
  std::string escaped;
  escaped.reserve(value.size() + 8);
  for (const char ch : value) {
    switch (ch) {
      case '\\':
        escaped += "\\\\";
        break;
      case '"':
        escaped += "\\\"";
        break;
      case '\n':
        escaped += "\\n";
        break;
      case '\r':
        escaped += "\\r";
        break;
      case '\t':
        escaped += "\\t";
        break;
      default:
        escaped += ch;
        break;
    }
  }
  return escaped;
}

void AppendComma(std::ostringstream& out, bool& first) {
  if (!first) {
    out << ',';
  }
  first = false;
}

void AppendString(std::ostringstream& out,
                  std::string_view key,
                  std::string_view value,
                  bool& first) {
  AppendComma(out, first);
  out << '"' << key << "\":\"" << EscapeJson(value) << '"';
}

void AppendBool(std::ostringstream& out,
                std::string_view key,
                bool value,
                bool& first) {
  AppendComma(out, first);
  out << '"' << key << "\":" << (value ? "true" : "false");
}

void AppendInt(std::ostringstream& out,
               std::string_view key,
               std::int64_t value,
               bool& first) {
  AppendComma(out, first);
  out << '"' << key << "\":" << value;
}

std::string SerializeStringArray(const std::vector<std::string>& values) {
  std::ostringstream out;
  out << '[';
  for (std::size_t i = 0; i < values.size(); ++i) {
    if (i != 0) {
      out << ',';
    }
    out << '"' << EscapeJson(values[i]) << '"';
  }
  out << ']';
  return out.str();
}

std::string SerializeStringMap(const std::map<std::string, std::string>& fields) {
  std::ostringstream out;
  out << '{';
  bool first = true;
  for (const auto& [key, value] : fields) {
    if (!first) {
      out << ',';
    }
    first = false;
    out << '"' << EscapeJson(key) << "\":\"" << EscapeJson(value) << '"';
  }
  out << '}';
  return out.str();
}

std::string SerializeApiError(const std::optional<ApiErrorInfo>& error) {
  if (!error) {
    return "null";
  }

  std::ostringstream out;
  bool first = true;
  out << '{';
  AppendInt(out, "raw_code", error->raw_code, first);
  AppendString(out, "raw_code_hex", error->raw_code_hex, first);
  AppendString(out, "symbol", error->symbol, first);
  AppendString(out, "message", error->message, first);
  out << '}';
  return out.str();
}

void AppendFormat(std::ostringstream& out,
                  std::string_view prefix,
                  const std::optional<FormatDescriptor>& format,
                  bool& first) {
  if (!format) {
    return;
  }

  AppendInt(out, std::string(prefix) + "_sample_rate_hz",
            format->sample_rate_hz, first);
  AppendString(out, std::string(prefix) + "_signal_mode",
               ToString(format->signal_mode), first);
  AppendString(out, std::string(prefix) + "_sample_type",
               ToString(format->sample_type), first);
  AppendInt(out, std::string(prefix) + "_valid_bits",
            format->valid_bits_per_sample, first);
  AppendInt(out, std::string(prefix) + "_container_bits",
            format->container_bits_per_sample, first);
  AppendInt(out, std::string(prefix) + "_channel_count",
            format->channels.channel_count, first);
  AppendInt(out, std::string(prefix) + "_channel_mask",
            format->channels.channel_mask, first);
}

std::string SerializeProbe(const CapabilityProbeRecord& probe) {
  std::ostringstream out;
  bool first = true;
  out << '{';
  AppendString(out, "kind", ToString(probe.kind), first);
  AppendString(out, "support", ToString(probe.support), first);
  AppendString(out, "detail", probe.detail, first);
  AppendFormat(out, "requested", probe.requested_format, first);
  AppendFormat(out, "accepted", probe.accepted_format, first);
  AppendComma(out, first);
  out << "\"error\":" << SerializeApiError(probe.error);
  AppendComma(out, first);
  out << "\"backend_fields\":" << SerializeStringMap(probe.backend_fields);
  out << '}';
  return out.str();
}

std::string SerializeDevice(const DeviceCapabilitySnapshot& snapshot) {
  std::ostringstream out;
  bool first = true;
  out << '{';
  AppendString(out, "backend", ToString(snapshot.device.backend), first);
  AppendString(out, "display_name", snapshot.device.display_name, first);
  AppendString(out, "stable_id", snapshot.device.stable_id, first);
  AppendString(out, "driver_name", snapshot.device.driver_name, first);
  AppendBool(out, "is_default", snapshot.device.is_default, first);
  AppendBool(out, "is_known_problem_device",
             snapshot.device.is_known_problem_device, first);
  AppendString(out, "availability", ToString(snapshot.availability), first);
  AppendComma(out, first);
  out << "\"default_roles\":" << SerializeStringArray(snapshot.default_roles);
  AppendComma(out, first);
  out << "\"discovery_error\":" << SerializeApiError(snapshot.discovery_error);
  AppendComma(out, first);
  out << "\"backend_fields\":" << SerializeStringMap(snapshot.backend_fields);
  AppendComma(out, first);
  out << "\"probes\":[";
  for (std::size_t i = 0; i < snapshot.probes.size(); ++i) {
    if (i != 0) {
      out << ',';
    }
    out << SerializeProbe(snapshot.probes[i]);
  }
  out << "]}";
  return out.str();
}

std::optional<std::string> ExtractString(const std::string& text,
                                         std::string_view key) {
  const std::string needle = "\"" + std::string(key) + "\":\"";
  const std::size_t start = text.find(needle);
  if (start == std::string::npos) {
    return std::nullopt;
  }

  std::string value;
  bool escaping = false;
  for (std::size_t i = start + needle.size(); i < text.size(); ++i) {
    const char ch = text[i];
    if (escaping) {
      switch (ch) {
        case 'n':
          value += '\n';
          break;
        case 'r':
          value += '\r';
          break;
        case 't':
          value += '\t';
          break;
        default:
          value += ch;
          break;
      }
      escaping = false;
      continue;
    }
    if (ch == '\\') {
      escaping = true;
      continue;
    }
    if (ch == '"') {
      return value;
    }
    value += ch;
  }
  return std::nullopt;
}

std::optional<std::int64_t> ExtractInt(const std::string& text,
                                       std::string_view key) {
  const std::string needle = "\"" + std::string(key) + "\":";
  const std::size_t pos = text.find(needle);
  if (pos == std::string::npos) {
    return std::nullopt;
  }

  const char* begin = text.data() + pos + needle.size();
  const char* end = text.data() + text.size();
  while (begin < end && *begin == ' ') {
    ++begin;
  }
  const char* number_end = begin;
  if (number_end < end && *number_end == '-') {
    ++number_end;
  }
  while (number_end < end && *number_end >= '0' && *number_end <= '9') {
    ++number_end;
  }

  std::int64_t value = 0;
  const auto result = std::from_chars(begin, number_end, value);
  if (result.ec != std::errc{}) {
    return std::nullopt;
  }
  return value;
}

bool ExtractBool(const std::string& text, std::string_view key) {
  const std::string needle = "\"" + std::string(key) + "\":";
  const std::size_t pos = text.find(needle);
  if (pos == std::string::npos) {
    return false;
  }
  const std::size_t start = pos + needle.size();
  return text.compare(start, 4, "true") == 0;
}

std::optional<std::string> ExtractObject(const std::string& text,
                                         std::string_view key) {
  const std::string needle = "\"" + std::string(key) + "\":";
  const std::size_t pos = text.find(needle);
  if (pos == std::string::npos) {
    return std::nullopt;
  }

  std::size_t start = pos + needle.size();
  while (start < text.size() && text[start] == ' ') {
    ++start;
  }
  if (start >= text.size() || text[start] == 'n') {
    return std::nullopt;
  }
  if (text[start] != '{') {
    return std::nullopt;
  }

  int depth = 0;
  bool in_string = false;
  bool escaping = false;
  for (std::size_t i = start; i < text.size(); ++i) {
    const char ch = text[i];
    if (escaping) {
      escaping = false;
      continue;
    }
    if (ch == '\\') {
      escaping = true;
      continue;
    }
    if (ch == '"') {
      in_string = !in_string;
      continue;
    }
    if (in_string) {
      continue;
    }
    if (ch == '{') {
      ++depth;
    } else if (ch == '}') {
      --depth;
      if (depth == 0) {
        return text.substr(start, i - start + 1);
      }
    }
  }
  return std::nullopt;
}

std::vector<std::string> ExtractArrayObjects(const std::string& text,
                                             std::string_view key) {
  std::vector<std::string> objects;
  const std::string needle = "\"" + std::string(key) + "\":[";
  const std::size_t pos = text.find(needle);
  if (pos == std::string::npos) {
    return objects;
  }

  const std::size_t start = pos + needle.size();
  int depth = 0;
  bool in_string = false;
  bool escaping = false;
  std::optional<std::size_t> object_start;
  for (std::size_t i = start; i < text.size(); ++i) {
    const char ch = text[i];
    if (escaping) {
      escaping = false;
      continue;
    }
    if (ch == '\\') {
      escaping = true;
      continue;
    }
    if (ch == '"') {
      in_string = !in_string;
      continue;
    }
    if (in_string) {
      continue;
    }
    if (ch == '{') {
      if (depth == 0) {
        object_start = i;
      }
      ++depth;
    } else if (ch == '}') {
      --depth;
      if (depth == 0 && object_start) {
        objects.push_back(text.substr(*object_start, i - *object_start + 1));
        object_start.reset();
      }
    } else if (ch == ']' && depth == 0) {
      break;
    }
  }
  return objects;
}

std::vector<std::string> ExtractStringArray(const std::string& text,
                                            std::string_view key) {
  std::vector<std::string> values;
  const std::string needle = "\"" + std::string(key) + "\":[";
  const std::size_t pos = text.find(needle);
  if (pos == std::string::npos) {
    return values;
  }

  std::size_t i = pos + needle.size();
  while (i < text.size() && text[i] != ']') {
    if (text[i] != '"') {
      ++i;
      continue;
    }
    const std::size_t item_start = i;
    std::string item_text = "{\"value\":";
    std::size_t item_end = i + 1;
    bool escaping = false;
    for (; item_end < text.size(); ++item_end) {
      const char ch = text[item_end];
      if (escaping) {
        escaping = false;
        continue;
      }
      if (ch == '\\') {
        escaping = true;
        continue;
      }
      if (ch == '"' && item_end != item_start) {
        break;
      }
    }
    if (item_end >= text.size()) {
      break;
    }
    item_text += text.substr(item_start, item_end - item_start + 1);
    item_text += '}';
    if (auto value = ExtractString(item_text, "value")) {
      values.push_back(*value);
    }
    i = item_end + 1;
  }
  return values;
}

std::map<std::string, std::string> ExtractStringMap(const std::string& text,
                                                    std::string_view key) {
  std::map<std::string, std::string> fields;
  const auto object = ExtractObject(text, key);
  if (!object) {
    return fields;
  }

  std::size_t pos = 1;
  while (pos < object->size() && (*object)[pos] != '}') {
    while (pos < object->size() &&
           ((*object)[pos] == ',' || (*object)[pos] == ' ')) {
      ++pos;
    }
    if (pos >= object->size() || (*object)[pos] != '"') {
      break;
    }
    const std::size_t key_start = ++pos;
    const std::size_t key_end = object->find('"', key_start);
    if (key_end == std::string::npos) {
      break;
    }
    std::string field_key = object->substr(key_start, key_end - key_start);
    pos = key_end + 1;
    if (pos >= object->size() || (*object)[pos] != ':') {
      break;
    }
    ++pos;
    if (pos >= object->size() || (*object)[pos] != '"') {
      break;
    }
    const std::size_t value_start = pos;
    std::size_t value_end = pos + 1;
    bool escaping = false;
    for (; value_end < object->size(); ++value_end) {
      const char ch = (*object)[value_end];
      if (escaping) {
        escaping = false;
        continue;
      }
      if (ch == '\\') {
        escaping = true;
        continue;
      }
      if (ch == '"' && value_end != value_start) {
        break;
      }
    }
    if (value_end >= object->size()) {
      break;
    }
    const std::string wrapped =
        "{\"value\":" + object->substr(value_start, value_end - value_start + 1) + '}';
    if (auto field_value = ExtractString(wrapped, "value")) {
      fields.emplace(std::move(field_key), std::move(*field_value));
    }
    pos = value_end + 1;
  }
  return fields;
}

BackendApi BackendApiFromString(const std::string& value) {
  if (value == "MME") {
    return BackendApi::kMme;
  }
  if (value == "DirectSound") {
    return BackendApi::kDirectSound;
  }
  if (value == "WASAPI Shared") {
    return BackendApi::kWasapiShared;
  }
  if (value == "WASAPI Exclusive") {
    return BackendApi::kWasapiExclusive;
  }
  if (value == "ASIO") {
    return BackendApi::kAsio;
  }
  if (value == "Kernel Streaming") {
    return BackendApi::kKernelStreaming;
  }
  return BackendApi::kNull;
}

SampleType SampleTypeFromString(const std::string& value) {
  if (value == "int16") {
    return SampleType::kInt16;
  }
  if (value == "int24-packed") {
    return SampleType::kInt24Packed;
  }
  if (value == "int24-in-32") {
    return SampleType::kInt24In32;
  }
  if (value == "int32") {
    return SampleType::kInt32;
  }
  if (value == "float64") {
    return SampleType::kFloat64;
  }
  return SampleType::kFloat32;
}

SignalMode SignalModeFromString(const std::string& value) {
  if (value == "DoP") {
    return SignalMode::kDop;
  }
  if (value == "ASIO Native DSD") {
    return SignalMode::kAsioNativeDsd;
  }
  return SignalMode::kPcm;
}

std::optional<ApiErrorInfo> ParseApiError(const std::string& text,
                                          std::string_view key) {
  const auto object = ExtractObject(text, key);
  if (!object) {
    return std::nullopt;
  }

  ApiErrorInfo error;
  error.raw_code = ExtractInt(*object, "raw_code").value_or(0);
  error.raw_code_hex = ExtractString(*object, "raw_code_hex").value_or("");
  error.symbol = ExtractString(*object, "symbol").value_or("");
  error.message = ExtractString(*object, "message").value_or("");
  return error;
}

std::optional<FormatDescriptor> ParseFormat(const std::string& text,
                                            std::string_view prefix) {
  const auto sample_rate = ExtractInt(text, std::string(prefix) + "_sample_rate_hz");
  if (!sample_rate) {
    return std::nullopt;
  }

  FormatDescriptor format;
  format.sample_rate_hz = static_cast<std::uint32_t>(*sample_rate);
  format.signal_mode = SignalModeFromString(
      ExtractString(text, std::string(prefix) + "_signal_mode").value_or("PCM"));
  format.sample_type = SampleTypeFromString(
      ExtractString(text, std::string(prefix) + "_sample_type").value_or("float32"));
  format.valid_bits_per_sample = static_cast<std::uint32_t>(
      ExtractInt(text, std::string(prefix) + "_valid_bits").value_or(32));
  format.container_bits_per_sample = static_cast<std::uint32_t>(
      ExtractInt(text, std::string(prefix) + "_container_bits").value_or(32));
  format.channels.channel_count = static_cast<std::uint32_t>(
      ExtractInt(text, std::string(prefix) + "_channel_count").value_or(2));
  format.channels.channel_mask = static_cast<std::uint32_t>(
      ExtractInt(text, std::string(prefix) + "_channel_mask").value_or(0));
  return format;
}

CapabilityProbeRecord ParseProbe(const std::string& text) {
  CapabilityProbeRecord probe;
  probe.kind = CapabilityProbeKindFromString(
      ExtractString(text, "kind").value_or("discovery-only"));
  probe.support = CapabilitySupportFromString(
      ExtractString(text, "support").value_or("unknown"));
  probe.detail = ExtractString(text, "detail").value_or("");
  probe.requested_format = ParseFormat(text, "requested");
  probe.accepted_format = ParseFormat(text, "accepted");
  probe.error = ParseApiError(text, "error");
  probe.backend_fields = ExtractStringMap(text, "backend_fields");
  return probe;
}

DeviceCapabilitySnapshot ParseDevice(const std::string& text) {
  DeviceInfo device;
  device.backend = BackendApiFromString(ExtractString(text, "backend").value_or(""));
  device.display_name = ExtractString(text, "display_name").value_or("");
  device.stable_id = ExtractString(text, "stable_id").value_or("");
  device.driver_name = ExtractString(text, "driver_name").value_or("");
  device.is_default = ExtractBool(text, "is_default");
  device.is_known_problem_device = ExtractBool(text, "is_known_problem_device");

  DeviceCapabilitySnapshot snapshot;
  snapshot.device = std::move(device);
  snapshot.availability = DeviceAvailabilityFromString(
      ExtractString(text, "availability").value_or("unknown"));
  snapshot.default_roles = ExtractStringArray(text, "default_roles");
  snapshot.discovery_error = ParseApiError(text, "discovery_error");
  snapshot.backend_fields = ExtractStringMap(text, "backend_fields");
  for (const auto& probe : ExtractArrayObjects(text, "probes")) {
    snapshot.probes.push_back(ParseProbe(probe));
  }
  return snapshot;
}

}  // namespace

const char* ToString(DeviceAvailability availability) {
  switch (availability) {
    case DeviceAvailability::kUnknown:
      return "unknown";
    case DeviceAvailability::kActive:
      return "active";
    case DeviceAvailability::kDisabled:
      return "disabled";
    case DeviceAvailability::kNotPresent:
      return "not-present";
    case DeviceAvailability::kUnplugged:
      return "unplugged";
    case DeviceAvailability::kUnavailable:
      return "unavailable";
  }
  return "unknown";
}

const char* ToString(CapabilitySupport support) {
  switch (support) {
    case CapabilitySupport::kUnknown:
      return "unknown";
    case CapabilitySupport::kSupported:
      return "supported";
    case CapabilitySupport::kUnsupported:
      return "unsupported";
    case CapabilitySupport::kClosestMatch:
      return "closest-match";
    case CapabilitySupport::kFailed:
      return "failed";
  }
  return "unknown";
}

const char* ToString(CapabilityProbeKind kind) {
  switch (kind) {
    case CapabilityProbeKind::kDiscoveryOnly:
      return "discovery-only";
    case CapabilityProbeKind::kFormatSupport:
      return "format-support";
    case CapabilityProbeKind::kBufferRange:
      return "buffer-range";
    case CapabilityProbeKind::kLatency:
      return "latency";
    case CapabilityProbeKind::kDriverInfo:
      return "driver-info";
  }
  return "discovery-only";
}

DeviceAvailability DeviceAvailabilityFromString(const std::string& value) {
  if (value == "active") {
    return DeviceAvailability::kActive;
  }
  if (value == "disabled") {
    return DeviceAvailability::kDisabled;
  }
  if (value == "not-present") {
    return DeviceAvailability::kNotPresent;
  }
  if (value == "unplugged") {
    return DeviceAvailability::kUnplugged;
  }
  if (value == "unavailable") {
    return DeviceAvailability::kUnavailable;
  }
  return DeviceAvailability::kUnknown;
}

CapabilitySupport CapabilitySupportFromString(const std::string& value) {
  if (value == "supported") {
    return CapabilitySupport::kSupported;
  }
  if (value == "unsupported") {
    return CapabilitySupport::kUnsupported;
  }
  if (value == "closest-match") {
    return CapabilitySupport::kClosestMatch;
  }
  if (value == "failed") {
    return CapabilitySupport::kFailed;
  }
  return CapabilitySupport::kUnknown;
}

CapabilityProbeKind CapabilityProbeKindFromString(const std::string& value) {
  if (value == "format-support") {
    return CapabilityProbeKind::kFormatSupport;
  }
  if (value == "buffer-range") {
    return CapabilityProbeKind::kBufferRange;
  }
  if (value == "latency") {
    return CapabilityProbeKind::kLatency;
  }
  if (value == "driver-info") {
    return CapabilityProbeKind::kDriverInfo;
  }
  return CapabilityProbeKind::kDiscoveryOnly;
}

std::string HexCode(std::int64_t raw_code) {
  std::ostringstream stream;
  const auto unsigned_value = static_cast<std::uint64_t>(raw_code);
  const int width =
      (raw_code >= 0 && raw_code <= 0xFFFFFFFFLL) ? 8 : 16;
  stream << "0x" << std::uppercase << std::hex << std::setw(width)
         << std::setfill('0') << unsigned_value;
  return stream.str();
}

std::string HexCode32ForDomain(std::int64_t raw_code,
                               std::string_view code_domain) {
  if (code_domain == "win32" || code_domain == "hresult" ||
      code_domain == "ntstatus") {
    std::ostringstream stream;
    stream << "0x" << std::uppercase << std::hex << std::setw(8)
           << std::setfill('0') << static_cast<std::uint32_t>(raw_code);
    return stream.str();
  }
  return HexCode(raw_code);
}

ApiErrorInfo MakeApiError(std::int64_t raw_code,
                          std::string symbol,
                          std::string message) {
  return ApiErrorInfo{
      .raw_code = raw_code,
      .raw_code_hex = HexCode(raw_code),
      .symbol = std::move(symbol),
      .message = std::move(message),
  };
}

ApiErrorInfo MakeApiErrorForDomain(std::int64_t raw_code,
                                   std::string symbol,
                                   std::string message,
                                   std::string_view code_domain) {
  return ApiErrorInfo{
      .raw_code = raw_code,
      .raw_code_hex = HexCode32ForDomain(raw_code, code_domain),
      .symbol = std::move(symbol),
      .message = std::move(message),
  };
}

DeviceCapabilitySnapshot MakeDiscoveredDeviceSnapshot(
    DeviceInfo device,
    DeviceAvailability availability) {
  DeviceCapabilitySnapshot snapshot;
  snapshot.device = std::move(device);
  snapshot.availability = availability;
  return snapshot;
}

bool DiscoverySnapshotIsSafeDryRun(const DiscoverySnapshot& snapshot) {
  return !snapshot.opens_audio_device &&
         !snapshot.initializes_stream &&
         !snapshot.starts_stream;
}

std::optional<DeviceCapabilitySnapshot> FindDeviceByStableId(
    const DiscoverySnapshot& snapshot,
    const std::string& stable_id) {
  for (const auto& device : snapshot.devices) {
    if (device.device.stable_id == stable_id) {
      return device;
    }
  }
  return std::nullopt;
}

std::string SerializeDiscoverySnapshotJson(const DiscoverySnapshot& snapshot) {
  std::ostringstream out;
  bool first = true;
  out << '{';
  AppendInt(out, "schema_version", snapshot.schema_version, first);
  AppendString(out, "capture_utc", snapshot.capture_utc, first);
  AppendString(out, "backend", ToString(snapshot.backend), first);
  AppendBool(out, "opens_audio_device", snapshot.opens_audio_device, first);
  AppendBool(out, "initializes_stream", snapshot.initializes_stream, first);
  AppendBool(out, "starts_stream", snapshot.starts_stream, first);
  AppendComma(out, first);
  out << "\"enumeration_error\":" << SerializeApiError(snapshot.enumeration_error);
  AppendComma(out, first);
  out << "\"devices\":[";
  for (std::size_t i = 0; i < snapshot.devices.size(); ++i) {
    if (i != 0) {
      out << ',';
    }
    out << SerializeDevice(snapshot.devices[i]);
  }
  out << "]}\n";
  return out.str();
}

std::optional<DiscoverySnapshot> ParseDiscoverySnapshotJson(const std::string& json) {
  const auto schema_version = ExtractInt(json, "schema_version");
  if (!schema_version) {
    return std::nullopt;
  }

  DiscoverySnapshot snapshot;
  snapshot.schema_version = static_cast<int>(*schema_version);
  snapshot.capture_utc = ExtractString(json, "capture_utc").value_or("");
  snapshot.backend = BackendApiFromString(ExtractString(json, "backend").value_or(""));
  snapshot.opens_audio_device = ExtractBool(json, "opens_audio_device");
  snapshot.initializes_stream = ExtractBool(json, "initializes_stream");
  snapshot.starts_stream = ExtractBool(json, "starts_stream");
  snapshot.enumeration_error = ParseApiError(json, "enumeration_error");

  for (const auto& device : ExtractArrayObjects(json, "devices")) {
    snapshot.devices.push_back(ParseDevice(device));
  }

  return snapshot;
}

}  // namespace aptt
