#pragma once

#ifndef USE_DESKTOP
#error "desktop.h is only for USE_DESKTOP builds"
#endif

#ifndef _WIN32
  #include <unistd.h>
#endif

#if defined(_WIN32) && defined(__MINGW32__)
extern "C" {
  typedef void (*__desktop_quick_exit_handler_t)(void);
  int at_quick_exit(__desktop_quick_exit_handler_t);
  void quick_exit(int);
  struct timespec;
  int timespec_get(struct timespec*, int);
}
#endif

#ifndef SDL_MAIN_HANDLED
#define SDL_MAIN_HANDLED 1
#endif
#if defined(_WIN32)
  #ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN
  #endif
  #ifndef NOMINMAX
    #define NOMINMAX
  #endif
  #include <windows.h>
  #include <wininet.h>
#endif
#include <SDL2/SDL.h>
#if defined(_WIN32)
  #include <SDL2/SDL_syswm.h>
#endif
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <functional>
#include <cerrno>
#include <cctype>
#include <sys/stat.h>
#if defined(_WIN32)
  #include <direct.h>
#endif
#include <string>
#include <thread>
#include <vector>

#define DESKTOP_SCREEN_W 800
#define DESKTOP_SCREEN_H 480

inline bool desktopTraceEnabled() {
  const char* v = std::getenv("MMDVM_DESKTOP_TRACE");
  return !v || std::strcmp(v, "0") != 0;
}

inline FILE*& desktopLogFile() {
  static FILE* f = nullptr;
  return f;
}

inline void desktopInitLog() {
  FILE*& f = desktopLogFile();
  if (!f) {
    f = std::fopen("desktop.log", "ab");
    if (f) {
      std::fprintf(f, "\n--- MMDVM desktop run ---\n");
      std::fflush(f);
    }
  }
}

inline void desktopLogRaw(const char* data, size_t len) {
  if (!data || len == 0) return;
  desktopInitLog();
  if (desktopLogFile()) {
    std::fwrite(data, 1, len, desktopLogFile());
    std::fflush(desktopLogFile());
  }
}

inline void desktopTrace(const char* fmt, ...) {
  if (!desktopTraceEnabled() || !fmt) return;
  char buf[2048];
  va_list ap;
  va_start(ap, fmt);
  int n = std::vsnprintf(buf, sizeof(buf), fmt, ap);
  va_end(ap);
  if (n <= 0) return;
  size_t len = (n < (int)sizeof(buf)) ? (size_t)n : std::strlen(buf);
  std::fwrite(buf, 1, len, stdout);
  std::fflush(stdout);
  desktopLogRaw(buf, len);
}

#ifndef HIGH
#define HIGH 1
#define LOW 0
#define OUTPUT 1
#define INPUT 0
#define INPUT_PULLUP 2
#endif

#ifndef HEX
#define HEX 16
#define DEC 10
#endif

#ifndef FILE_READ
#define FILE_READ "rb"
#define FILE_WRITE "wb"
#define FILE_APPEND "ab"
#endif

#ifndef HTTP_GET
#define HTTP_GET 0
#define HTTP_POST 1
#endif

#ifndef WL_CONNECTED
#define WL_CONNECTED 3
#define WL_DISCONNECTED 6
typedef int wl_status_t;
#endif

#ifndef WIFI_STA
#define WIFI_STA 1
#define WIFI_AP 2
#define WIFI_AP_STA 3
#endif

#ifndef UPDATE_SIZE_UNKNOWN
#define UPDATE_SIZE_UNKNOWN 0xffffffffu
#endif

#define UPDATE_ERROR_OK 0
#define UPDATE_ERROR_WRITE 1
#define UPDATE_ERROR_ERASE 2
#define UPDATE_ERROR_READ 3
#define UPDATE_ERROR_SPACE 4
#define UPDATE_ERROR_SIZE 5
#define UPDATE_ERROR_STREAM 6
#define UPDATE_ERROR_MD5 7
#define UPDATE_ERROR_MAGIC_BYTE 8
#define UPDATE_ERROR_ACTIVATE 9
#define UPDATE_ERROR_NO_PARTITION 10
#define UPDATE_ERROR_BAD_ARGUMENT 11
#define UPDATE_ERROR_ABORT 12

#ifndef PNG_SUCCESS
#define PNG_SUCCESS 0
#define PNG_RGB565_BIG_ENDIAN 1
#endif

#define FSPI 0
#define HSPI 1
#define VSPI 2

#define pdTRUE true
#define portMAX_DELAY 0xffffffffu
#define pdMS_TO_TICKS(ms) (ms)
typedef void* SemaphoreHandle_t;
inline SemaphoreHandle_t xSemaphoreCreateRecursiveMutex() { return reinterpret_cast<SemaphoreHandle_t>(1); }
typedef void* TaskHandle_t;
typedef int BaseType_t;
#ifndef pdPASS
#define pdPASS 1
#endif
inline void vTaskDelete(TaskHandle_t) {}
inline BaseType_t xTaskCreatePinnedToCore(void (*)(void*), const char*, uint32_t, void*, uint32_t, TaskHandle_t* handle, int) { if (handle) *handle = nullptr; return 0; }
inline bool xSemaphoreTakeRecursive(SemaphoreHandle_t, uint32_t) { return true; }
inline void xSemaphoreGiveRecursive(SemaphoreHandle_t) {}

template <typename A, typename B> static inline auto min(A a, B b) -> typename std::common_type<A, B>::type { using R = typename std::common_type<A, B>::type; return ((R)a < (R)b) ? (R)a : (R)b; }
template <typename A, typename B> static inline auto max(A a, B b) -> typename std::common_type<A, B>::type { using R = typename std::common_type<A, B>::type; return ((R)a > (R)b) ? (R)a : (R)b; }
template <typename T, typename A, typename B> static inline auto constrain(T x, A a, B b) -> typename std::common_type<T, A, B>::type { using R = typename std::common_type<T, A, B>::type; R rx=(R)x, ra=(R)a, rb=(R)b; return rx < ra ? ra : (rx > rb ? rb : rx); }
#ifndef PI
#define PI 3.14159265358979323846
#endif
static inline bool isDigit(char c) { return std::isdigit((unsigned char)c) != 0; }
static inline bool isAlpha(char c) { return std::isalpha((unsigned char)c) != 0; }
static inline bool isSpace(char c) { return std::isspace((unsigned char)c) != 0; }
static inline long map(long x, long in_min, long in_max, long out_min, long out_max) {
  if (in_max == in_min) return out_min;
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

class String {
  std::string s_;
public:
  String() = default;
  String(const char* s) : s_(s ? s : "") {}
  String(char* s) : s_(s ? s : "") {}
  String(const std::string& s) : s_(s) {}
  String(char c) : s_(1, c) {}
  String(unsigned char v, int base = DEC) { *this = fromUnsigned(v, base); }
  String(int v, int base = DEC) { *this = fromSigned(v, base); }
  String(unsigned int v, int base = DEC) { *this = fromUnsigned(v, base); }
  String(long v, int base = DEC) { *this = fromSigned(v, base); }
  String(unsigned long v, int base = DEC) { *this = fromUnsigned(v, base); }
  String(long long v, int base = DEC) { *this = fromSigned(v, base); }
  String(unsigned long long v, int base = DEC) { *this = fromUnsigned(v, base); }
  String(float v, int decimals = 2) { char buf[64]; if (decimals < 0) decimals = 2; if (decimals > 9) decimals = 9; std::snprintf(buf, sizeof(buf), "%.*f", decimals, (double)v); s_ = buf; }
  String(double v, int decimals = 2) { char buf[64]; if (decimals < 0) decimals = 2; if (decimals > 9) decimals = 9; std::snprintf(buf, sizeof(buf), "%.*f", decimals, v); s_ = buf; }

  static String fromUnsigned(unsigned long long value, int base) {
    if (base == HEX) {
      char buf[32]; std::snprintf(buf, sizeof(buf), "%llX", value); return String(buf);
    }
    return String(std::to_string(value));
  }
  static String fromSigned(long long value, int base) {
    if (base == HEX) return fromUnsigned(static_cast<unsigned long long>(value), base);
    return String(std::to_string(value));
  }

  const char* c_str() const { return s_.c_str(); }
  operator const char*() const { return s_.c_str(); }
  operator std::string() const { return s_; }
  size_t length() const { return s_.size(); }
  bool isEmpty() const { return s_.empty(); }
  void reserve(size_t n) { s_.reserve(n); }
  char charAt(size_t i) const { return i < s_.size() ? s_[i] : '\0'; }
  void setCharAt(size_t i, char c) { if (i < s_.size()) s_[i] = c; }
  char operator[](size_t i) const { return charAt(i); }
  char operator[](int i) const { return i < 0 ? '\0' : charAt((size_t)i); }
  char operator[](long i) const { return i < 0 ? '\0' : charAt((size_t)i); }

  String substring(int start) const { return substring(start, (int)s_.size()); }
  String substring(int start, int end) const {
    if (start < 0) start = 0;
    if (end < start) end = start;
    if ((size_t)start > s_.size()) return String("");
    if ((size_t)end > s_.size()) end = (int)s_.size();
    return String(s_.substr(start, end - start));
  }
  int indexOf(char c, int fromIndex = 0) const {
    if (fromIndex < 0) fromIndex = 0;
    auto p = s_.find(c, (size_t)fromIndex);
    return p == std::string::npos ? -1 : (int)p;
  }
  int indexOf(const char* needle, int fromIndex = 0) const {
    if (!needle) return -1;
    if (fromIndex < 0) fromIndex = 0;
    auto p = s_.find(needle, (size_t)fromIndex);
    return p == std::string::npos ? -1 : (int)p;
  }
  int indexOf(const String& needle, int fromIndex = 0) const { return indexOf(needle.c_str(), fromIndex); }
  int lastIndexOf(char c) const { auto p=s_.rfind(c); return p==std::string::npos?-1:(int)p; }
  int lastIndexOf(char c, int fromIndex) const { size_t pos = fromIndex < 0 ? std::string::npos : (size_t)fromIndex; auto p=s_.rfind(c, pos); return p==std::string::npos?-1:(int)p; }
  int lastIndexOf(const char* needle) const { auto p=s_.rfind(needle?needle:""); return p==std::string::npos?-1:(int)p; }
  int lastIndexOf(const char* needle, int fromIndex) const { size_t pos = fromIndex < 0 ? std::string::npos : (size_t)fromIndex; auto p=s_.rfind(needle?needle:"", pos); return p==std::string::npos?-1:(int)p; }
  bool startsWith(const char* prefix) const { if (!prefix) return false; size_t n=std::strlen(prefix); return s_.size()>=n && s_.compare(0,n,prefix)==0; }
  bool startsWith(const String& prefix) const { return startsWith(prefix.c_str()); }
  bool endsWith(const char* suffix) const { if (!suffix) return false; size_t n=std::strlen(suffix); return s_.size()>=n && s_.compare(s_.size()-n,n,suffix)==0; }
  bool endsWith(const String& suffix) const { return endsWith(suffix.c_str()); }
  void replace(const String& from, const String& to) {
    if (!from.length()) return;
    size_t pos = 0;
    while ((pos = s_.find(from.s_, pos)) != std::string::npos) { s_.replace(pos, from.length(), to.s_); pos += to.length(); }
  }
  void replace(const char* from, const char* to) { replace(String(from), String(to)); }
  void replace(const char* from, const String& to) { replace(String(from), to); }
  void replace(const String& from, const char* to) { replace(from, String(to)); }
  void remove(int index) { if (index >= 0 && (size_t)index < s_.size()) s_.erase(index); }
  void remove(int index, int count) { if (index >= 0 && count > 0 && (size_t)index < s_.size()) s_.erase(index, count); }
  void trim() {
    size_t first = s_.find_first_not_of(" \t\r\n");
    if (first == std::string::npos) { s_.clear(); return; }
    size_t last = s_.find_last_not_of(" \t\r\n");
    s_ = s_.substr(first, last - first + 1);
  }
  void toLowerCase() { std::transform(s_.begin(), s_.end(), s_.begin(), [](unsigned char c){ return (char)std::tolower(c); }); }
  void toUpperCase() { std::transform(s_.begin(), s_.end(), s_.begin(), [](unsigned char c){ return (char)std::toupper(c); }); }
  int compareTo(const String& other) const { return s_.compare(other.s_); }
  bool equalsIgnoreCase(const String& other) const {
    if (s_.size() != other.s_.size()) return false;
    for (size_t i=0;i<s_.size();++i) if (std::tolower((unsigned char)s_[i]) != std::tolower((unsigned char)other.s_[i])) return false;
    return true;
  }
  long toInt() const { try { return std::stol(s_); } catch (...) { return 0; } }
  float toFloat() const { try { return std::stof(s_); } catch (...) { return 0.0f; } }
  void toCharArray(char* buffer, unsigned int len) const { if (!buffer || !len) return; std::snprintf(buffer, len, "%s", c_str()); }

  String& operator=(const char* s) { s_ = s ? s : ""; return *this; }
  String& operator=(const std::string& s) { s_ = s; return *this; }
  String& operator+=(const String& rhs) { s_ += rhs.s_; return *this; }
  String& operator+=(const char* rhs) { s_ += rhs ? rhs : ""; return *this; }
  String& operator+=(char rhs) { s_ += rhs; return *this; }
  template <typename T> String& operator+=(T rhs) { s_ += String(rhs).s_; return *this; }

  friend String operator+(const String& a, const String& b) { return String(a.s_ + b.s_); }
  friend String operator+(const char* a, const String& b) { return String(a) + b; }
  friend String operator+(const String& a, const char* b) { return a + String(b); }
  friend String operator+(const String& a, char b) { return a + String(b); }
  friend String operator+(char a, const String& b) { return String(a) + b; }
  friend bool operator==(const String& a, const String& b) { return a.s_ == b.s_; }
  friend bool operator!=(const String& a, const String& b) { return !(a == b); }
  friend bool operator==(const String& a, const char* b) { return a.s_ == (b ? b : ""); }
  friend bool operator==(const char* a, const String& b) { return b == a; }
  friend bool operator!=(const String& a, const char* b) { return !(a == b); }
  friend bool operator!=(const char* a, const String& b) { return !(b == a); }
};

#include "desktop_json.h"

inline String JsonVariantConst::operator|(const String& fallback) const { return fallback; }

#if defined(_WIN32)
static inline struct tm* desktop_localtime_r(const time_t* timer, struct tm* result) {
  if (!timer || !result) return nullptr;
  struct tm* tmp = std::localtime(timer);
  if (!tmp) return nullptr;
  *result = *tmp;
  return result;
}
static inline struct tm* desktop_gmtime_r(const time_t* timer, struct tm* result) {
  if (!timer || !result) return nullptr;
  struct tm* tmp = std::gmtime(timer);
  if (!tmp) return nullptr;
  *result = *tmp;
  return result;
}
#define localtime_r desktop_localtime_r
#define gmtime_r desktop_gmtime_r

static inline size_t strlcpy(char* dst, const char* src, size_t dstsize) {
  const char* s = src ? src : "";
  size_t len = std::strlen(s);
  if (dst && dstsize) {
    size_t n = (len >= dstsize) ? (dstsize - 1) : len;
    std::memcpy(dst, s, n);
    dst[n] = '\0';
  }
  return len;
}
#endif

class Print {
public:
  virtual ~Print() = default;
  virtual size_t write(uint8_t b) { (void)b; return 1; }
  virtual size_t write(const uint8_t* buffer, size_t size) { size_t n=0; for (size_t i=0;i<size;i++) n += write(buffer[i]); return n; }
  size_t write(const char* s) { return s ? write(reinterpret_cast<const uint8_t*>(s), std::strlen(s)) : 0; }
  size_t print(const String& s) { return write(s.c_str()); }
  size_t print(const char* s) { return write(s); }
  size_t print(char c) { return write((uint8_t)c); }
  template <typename T> size_t print(T value) { String s(value); return print(s); }
  size_t println() { return write("\n"); }
  template <typename T> size_t println(T value) { size_t n=print(value); n += println(); return n; }
  int printf(const char* fmt, ...) {
    char buf[1024]; va_list ap; va_start(ap, fmt); int n=std::vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (n > 0) write(buf); return n;
  }
};

class Stream : public Print {
public:
  virtual int available() { return 0; }
  virtual int read() { return -1; }
  virtual size_t readBytes(uint8_t* buffer, size_t length) {
    size_t n=0; while (n<length && available()>0) { int c=read(); if (c<0) break; buffer[n++]=(uint8_t)c; } return n;
  }
};

class SerialClass : public Print {
public:
  void begin(unsigned long) { desktopInitLog(); }
  size_t write(uint8_t b) override {
    char c = (char)b;
    std::fputc(c, stdout);
    std::fflush(stdout);
    desktopLogRaw(&c, 1);
    return 1;
  }
  size_t write(const uint8_t* buffer, size_t size) override {
    if (size) {
      std::fwrite(buffer, 1, size, stdout);
      std::fflush(stdout);
      desktopLogRaw(reinterpret_cast<const char*>(buffer), size);
    }
    return size;
  }
};
inline SerialClass Serial;

#if defined(_WIN32)
inline unsigned long millis() {
  return (unsigned long)::GetTickCount();
}
#else
inline auto __desktopStartTime = std::chrono::steady_clock::now();
inline unsigned long millis() {
  auto diff = std::chrono::steady_clock::now() - __desktopStartTime;
  return (unsigned long)std::chrono::duration_cast<std::chrono::milliseconds>(diff).count();
}
#endif
inline void desktopPollEventsOnly();
inline void desktopSleepMs(unsigned long ms) {
#if defined(_WIN32)
  ::Sleep((DWORD)ms);
#else
  std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}
inline void delay(unsigned long ms) {
#if defined(_WIN32)
  desktopSleepMs(ms);
#else
  unsigned long start = millis();
  while (millis() - start < ms) { desktopPollEventsOnly(); desktopSleepMs(1); }
#endif
}
inline long random(long maxValue) { return maxValue <= 0 ? 0 : std::rand() % maxValue; }
inline long random(long minValue, long maxValue) { return minValue + random(maxValue - minValue); }
inline void randomSeed(unsigned long seed) { std::srand(seed); }
inline void pinMode(int, int) {}
inline void digitalWrite(int, int) {}
inline void analogWrite(int, int) {}
inline int digitalRead(int) { return HIGH; }
inline void ledcAttach(int, int, int) {}
inline void ledcSetup(int, int, int) {}
inline void ledcAttachPin(int, int) {}
inline void ledcWrite(int, int) {}
inline bool getLocalTime(struct tm* info, uint32_t = 0) { if (!info) return false; std::time_t t = std::time(nullptr); struct tm* lt = std::localtime(&t); if (!lt) return false; *info = *lt; return true; }
inline void configTzTime(const char*, const char*, const char*, const char*) {}

class IPAddress {
  String value_;
public:
  IPAddress(const char* v = "127.0.0.1") : value_(v) {}
  String toString() const { return value_; }
};

class WiFiClass {
  int status_ = WL_CONNECTED;
public:
  void persistent(bool) {}
  void mode(int) {}
  void disconnect(bool = false, bool = false) { status_ = WL_DISCONNECTED; }
  void setAutoReconnect(bool) {}
  void begin(const char*, const char*) { status_ = WL_CONNECTED; }
  int status() const { return status_; }
  long RSSI() const { return -42; }
  IPAddress localIP() const { return IPAddress("127.0.0.1"); }
  IPAddress softAPIP() const { return IPAddress("127.0.0.1"); }
  bool softAP(const char*, const char*) { return true; }
  void softAPdisconnect(bool = true) {}
  int scanNetworks(bool = false, bool = false) { return 1; }
  String SSID(int index) const { return index == 0 ? String("DESKTOP") : String(""); }
};
inline WiFiClass WiFi;

class WiFiClient : public Stream { public: void stop() {} };
class WiFiClientSecure : public WiFiClient { public: void setInsecure() {} };

class MemoryStream : public Stream {
  std::string data_;
  size_t pos_ = 0;
public:
  void setData(const std::string& s) { data_ = s; pos_ = 0; }
  int available() override { return (int)(data_.size() - pos_); }
  int read() override { return pos_ < data_.size() ? (unsigned char)data_[pos_++] : -1; }
  size_t readBytes(uint8_t* buffer, size_t length) override {
    size_t n = std::min(length, data_.size() - pos_);
    if (n) { std::memcpy(buffer, data_.data() + pos_, n); pos_ += n; }
    return n;
  }
};

static inline std::string desktopNormalizePath(std::string p) {
  for (char& c : p) if (c == '\\') c = '/';
  return p;
}

static inline bool desktopPathExists(const std::string& path) {
  struct stat st;
  return ::stat(path.c_str(), &st) == 0;
}

static inline bool desktopMkdirOne(const std::string& path) {
  if (path.empty() || desktopPathExists(path)) return true;
#if defined(_WIN32)
  return ::_mkdir(path.c_str()) == 0 || errno == EEXIST;
#else
  return ::mkdir(path.c_str(), 0755) == 0 || errno == EEXIST;
#endif
}

static inline bool desktopCreateDirs(const std::string& rawPath) {
  std::string path = desktopNormalizePath(rawPath);
  if (path.empty()) return true;
  std::string current;
  size_t i = 0;
  if (path.size() >= 2 && path[1] == ':') { current = path.substr(0, 2); i = 2; }
  for (; i < path.size(); ++i) {
    char c = path[i];
    current.push_back(c);
    if (c == '/') desktopMkdirOne(current);
  }
  return desktopMkdirOne(current);
}

static inline bool desktopCreateParentDirs(const std::string& rawPath) {
  std::string path = desktopNormalizePath(rawPath);
  size_t pos = path.find_last_of('/');
  if (pos == std::string::npos) return true;
  return desktopCreateDirs(path.substr(0, pos));
}

static inline std::string desktopReadWholeFile(const std::string& path, bool* ok = nullptr) {
  if (ok) *ok = false;
  FILE* f = std::fopen(path.c_str(), "rb");
  if (!f) return std::string();
  std::fseek(f, 0, SEEK_END);
  long n = std::ftell(f);
  if (n < 0) n = 0;
  std::fseek(f, 0, SEEK_SET);
  std::string out;
  out.resize((size_t)n);
  if (n > 0) {
    size_t got = std::fread(&out[0], 1, (size_t)n, f);
    out.resize(got);
  }
  std::fclose(f);
  if (ok) *ok = true;
  return out;
}


static inline std::string desktopShellQuote(const std::string& in) {
#if defined(_WIN32)
  std::string out = "\"";
  for (char c : in) {
    if (c == '"') out += "\\\"";
    else out += c;
  }
  out += "\"";
  return out;
#else
  std::string out = "'";
  for (char c : in) {
    if (c == '\'') out += "'\\''";
    else out += c;
  }
  out += "'";
  return out;
#endif
}

static inline std::string desktopHttpStatusMarker() { return "\n__MMDVM_HTTP_CODE__:"; }

#if !defined(_WIN32)
static inline bool desktopHttpGetWithCurlCommand(const std::string& url,
                                                 const std::string& headers,
                                                 const std::string& userAgent,
                                                 int timeoutMs,
                                                 std::string& bodyOut,
                                                 int& codeOut,
                                                 std::string& errorOut) {
  bodyOut.clear();
  codeOut = 0;
  errorOut.clear();
  int timeoutSec = timeoutMs > 0 ? ((timeoutMs + 999) / 1000) : 15;
  if (timeoutSec < 1) timeoutSec = 1;
  std::string cmd = "curl -L -k -sS --connect-timeout 5 --max-time " + std::to_string(timeoutSec) +
                    " -A " + desktopShellQuote(userAgent.empty() ? "MMDVM-Display-Desktop" : userAgent);
  if (!headers.empty()) {
    size_t pos = 0;
    while (pos <= headers.size()) {
      size_t nl = headers.find('\n', pos);
      std::string line = headers.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos);
      while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back();
      if (!line.empty()) cmd += " -H " + desktopShellQuote(line);
      if (nl == std::string::npos) break;
      pos = nl + 1;
    }
  }
  cmd += " -w " + desktopShellQuote(desktopHttpStatusMarker() + "%{http_code}\\n") + " " + desktopShellQuote(url);
  FILE* pipe = popen(cmd.c_str(), "r");
  if (!pipe) { errorOut = "popen(curl) failed"; return false; }
  char buf[4096];
  while (true) {
    size_t n = std::fread(buf, 1, sizeof(buf), pipe);
    if (n) bodyOut.append(buf, n);
    if (n < sizeof(buf)) {
      if (std::feof(pipe)) break;
      if (std::ferror(pipe)) { errorOut = "curl pipe read failed"; break; }
    }
  }
  int rc = pclose(pipe);
  (void)rc;
  std::string marker = desktopHttpStatusMarker();
  size_t p = bodyOut.rfind(marker);
  if (p != std::string::npos) {
    std::string codeText = bodyOut.substr(p + marker.size());
    codeOut = std::atoi(codeText.c_str());
    bodyOut.erase(p);
    while (!bodyOut.empty() && (bodyOut.back() == '\r' || bodyOut.back() == '\n')) bodyOut.pop_back();
  }
  return codeOut >= 100 && codeOut < 600;
}
#endif

#if defined(_WIN32)
static inline bool desktopHttpGetWithWinInet(const std::string& url,
                                             const std::string& headers,
                                             const std::string& userAgent,
                                             int connectTimeoutMs,
                                             int readTimeoutMs,
                                             std::string& bodyOut,
                                             int& codeOut,
                                             std::string& errorOut) {
  bodyOut.clear();
  codeOut = 0;
  errorOut.clear();

  HINTERNET hInternet = InternetOpenA(userAgent.empty() ? "MMDVM-Display-Desktop" : userAgent.c_str(),
                                      INTERNET_OPEN_TYPE_PRECONFIG, nullptr, nullptr, 0);
  if (!hInternet) { errorOut = "InternetOpen failed"; return false; }

  DWORD connectTimeout = connectTimeoutMs > 0 ? (DWORD)connectTimeoutMs : 5000;
  DWORD readTimeout = readTimeoutMs > 0 ? (DWORD)readTimeoutMs : 15000;
  InternetSetOptionA(hInternet, INTERNET_OPTION_CONNECT_TIMEOUT, &connectTimeout, sizeof(connectTimeout));
  InternetSetOptionA(hInternet, INTERNET_OPTION_RECEIVE_TIMEOUT, &readTimeout, sizeof(readTimeout));
  InternetSetOptionA(hInternet, INTERNET_OPTION_SEND_TIMEOUT, &readTimeout, sizeof(readTimeout));

  DWORD flags = INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE;
  std::string lower = url;
  std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c){ return (char)std::tolower(c); });
  if (lower.rfind("https://", 0) == 0) {
    flags |= INTERNET_FLAG_SECURE | INTERNET_FLAG_IGNORE_CERT_CN_INVALID | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID;
  }

  HINTERNET hUrl = InternetOpenUrlA(hInternet, url.c_str(), headers.empty() ? nullptr : headers.c_str(),
                                    headers.empty() ? 0 : (DWORD)headers.size(), flags, 0);
  if (!hUrl) {
    DWORD e = GetLastError();
    char tmp[128];
    std::snprintf(tmp, sizeof(tmp), "InternetOpenUrl failed (%lu)", (unsigned long)e);
    errorOut = tmp;
    InternetCloseHandle(hInternet);
    return false;
  }

  DWORD status = 0;
  DWORD statusLen = sizeof(status);
  if (HttpQueryInfoA(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status, &statusLen, nullptr)) {
    codeOut = (int)status;
  }

  char buf[4096];
  DWORD got = 0;
  while (InternetReadFile(hUrl, buf, sizeof(buf), &got) && got > 0) {
    bodyOut.append(buf, got);
  }
  if (codeOut == 0 && !bodyOut.empty()) codeOut = 200;

  InternetCloseHandle(hUrl);
  InternetCloseHandle(hInternet);
  return codeOut >= 100 && codeOut < 600;
}
#endif

class HTTPClient {
  String url_;
  String body_;
  int code_ = 0;
  int connectTimeoutMs_ = 5000;
  int readTimeoutMs_ = 15000;
  String userAgent_ = "MMDVM-Display-Desktop";
  std::string headers_;
  MemoryStream stream_;
public:
  void setConnectTimeout(int ms) { connectTimeoutMs_ = ms; }
  void setTimeout(int ms) { readTimeoutMs_ = ms; }
  void useHTTP10(bool) {}
  void setReuse(bool) {}
  void setUserAgent(const String& ua) { if (ua.length()) userAgent_ = ua; }
  void addHeader(const String& key, const String& value) {
    if (!key.length()) return;
    headers_ += (std::string)key;
    headers_ += ": ";
    headers_ += (std::string)value;
    headers_ += "\r\n";
  }
  bool begin(WiFiClient&, const String& url) { url_ = url; return url_.length() > 0; }
  bool begin(WiFiClientSecure&, const String& url) { url_ = url; return url_.length() > 0; }
  int GET() {
    body_ = "";
    code_ = 0;
    std::string u = (std::string)url_;
    if (u.rfind("file://", 0) == 0) {
      bool ok = false;
      body_ = desktopReadWholeFile(u.substr(7), &ok);
      code_ = ok ? 200 : 404;
      stream_.setData((std::string)body_);
      return code_;
    }

    std::string body;
    std::string err;
    bool got = false;
#if defined(_WIN32)
    got = desktopHttpGetWithWinInet(u, headers_, (std::string)userAgent_, connectTimeoutMs_, readTimeoutMs_, body, code_, err);
#else
    got = desktopHttpGetWithCurlCommand(u, headers_, (std::string)userAgent_, readTimeoutMs_, body, code_, err);
#endif
    if (!got) {
      if (code_ == 0) code_ = -2;
      desktopTrace("[DESKTOP][HTTP] GET failed: %s -> code %d %s\n", u.c_str(), code_, err.c_str());
    } else {
      desktopTrace("[DESKTOP][HTTP] GET %s -> HTTP %d, %u bytes\n", u.c_str(), code_, (unsigned)body.size());
    }
    body_ = String(body);
    stream_.setData(body);
    return code_;
  }
  String getString() { return body_; }
  Stream* getStreamPtr() { stream_.setData((std::string)body_); return &stream_; }
  int getSize() const { return (int)body_.length(); }
  bool connected() { return stream_.available() > 0; }
  void end() {}
};

namespace fs {
class File : public Stream {
  FILE* f_ = nullptr;
  std::string path_;
  bool writeMode_ = false;
public:
  File() = default;
  File(const std::string& path, const char* mode) { open(path, mode); }
  ~File() { close(); }
  File(const File&) = delete;
  File& operator=(const File&) = delete;
  File(File&& other) noexcept { *this = std::move(other); }
  File& operator=(File&& other) noexcept {
    if (this != &other) {
      close();
      f_ = other.f_;
      path_ = other.path_;
      writeMode_ = other.writeMode_;
      other.f_ = nullptr;
      other.writeMode_ = false;
    }
    return *this;
  }
  bool open(const std::string& path, const char* mode) {
    close();
    path_ = path;
    std::string m = mode ? mode : FILE_READ;
    writeMode_ = (m.find('w') != std::string::npos) || (m.find('a') != std::string::npos) || (m.find('+') != std::string::npos);
    if (writeMode_) desktopCreateParentDirs(path);
    f_ = std::fopen(path.c_str(), m.c_str());
    return f_ != nullptr;
  }
  explicit operator bool() const { return f_ != nullptr; }
  size_t write(uint8_t b) override { return f_ ? std::fwrite(&b, 1, 1, f_) : 0; }
  size_t write(const uint8_t* buffer, size_t size) override { return (f_ && buffer && size) ? std::fwrite(buffer, 1, size, f_) : 0; }
  int available() override {
    if (!f_) return 0;
    long cur = std::ftell(f_);
    if (cur < 0) return 0;
    std::fseek(f_, 0, SEEK_END);
    long end = std::ftell(f_);
    std::fseek(f_, cur, SEEK_SET);
    return (end >= cur) ? (int)(end - cur) : 0;
  }
  int read() override { if (!f_) return -1; int c = std::fgetc(f_); return c == EOF ? -1 : c; }
  size_t read(uint8_t* buffer, size_t len) { return (f_ && buffer && len) ? std::fread(buffer, 1, len, f_) : 0; }
  size_t readBytes(uint8_t* buffer, size_t len) override { return read(buffer, len); }
  size_t readBytes(char* buffer, size_t len) { return read(reinterpret_cast<uint8_t*>(buffer), len); }
  String readStringUntil(char delimiter) {
    std::string out;
    int c;
    while ((c = read()) >= 0) {
      if ((char)c == delimiter) break;
      out.push_back((char)c);
    }
    return String(out);
  }
  bool seek(uint32_t pos) { return f_ && std::fseek(f_, (long)pos, SEEK_SET) == 0; }
  uint32_t position() { if (!f_) return 0; long p = std::ftell(f_); return p < 0 ? 0u : (uint32_t)p; }
  size_t size() {
    if (!f_) return 0;
    long cur = std::ftell(f_);
    std::fseek(f_, 0, SEEK_END);
    long end = std::ftell(f_);
    std::fseek(f_, cur < 0 ? 0 : cur, SEEK_SET);
    return end < 0 ? 0u : (size_t)end;
  }
  void close() { if (f_) { std::fclose(f_); f_ = nullptr; } }
};

class FS {
  std::string root_;
  std::string mapPath(const char* path) const {
    std::string p = desktopNormalizePath(path ? path : "");
    while (!p.empty() && p[0] == '/') p.erase(p.begin());
    if (p.empty()) return root_;
    return root_ + "/" + p;
  }
public:
  FS(const char* root = "sdcard") : root_(desktopNormalizePath(root ? root : "sdcard")) {}
  bool begin(const char* = nullptr, bool = false, bool = false) { return desktopCreateDirs(root_); }
  bool begin(int, class SPIClass&, int) { return desktopCreateDirs(root_); }
  bool setPins(int, int, int) { return true; }
  File open(const char* path, const char* mode = FILE_READ) { return File(mapPath(path), mode); }
  bool exists(const char* path) { return desktopPathExists(mapPath(path)); }
  bool remove(const char* path) { std::string p = mapPath(path); return std::remove(p.c_str()) == 0 || !desktopPathExists(p); }
  bool mkdir(const char* path) { return desktopCreateDirs(mapPath(path)); }
  bool rename(const char* from, const char* to) {
    std::string src = mapPath(from);
    std::string dst = mapPath(to);
    desktopCreateParentDirs(dst);
    return std::rename(src.c_str(), dst.c_str()) == 0;
  }
};
}

inline fs::FS SD("sdcard");
inline fs::FS SD_MMC("sdcard");

class SPIClass {
public:
  explicit SPIClass(int = 0) {}
  void begin(int = -1, int = -1, int = -1, int = -1) {}
};

struct HTTPUpload {
  int status = 0;
  String filename;
  uint8_t* buf = nullptr;
  size_t currentSize = 0;
};
#define UPLOAD_FILE_START 0
#define UPLOAD_FILE_WRITE 1
#define UPLOAD_FILE_END 2
#define UPLOAD_FILE_ABORTED 3

class WebServer {
  HTTPUpload upload_;
public:
  explicit WebServer(int) {}
  template <typename Handler> void on(const String&, int, Handler) {}
  template <typename Handler, typename UploadHandler> void on(const String&, int, Handler, UploadHandler) {}
  void begin() { Serial.println("Desktop WebServer stub active; no HTTP listener is started."); }
  void handleClient() {}
  bool hasArg(const String&) const { return false; }
  String arg(const String&) const { return ""; }
  HTTPUpload& upload() { return upload_; }
  void sendHeader(const String&, const String&) {}
  void send(int code, const String& type, const String& body) { Serial.printf("[WebServer stub] send %d %s (%u bytes)\n", code, type.c_str(), (unsigned)body.length()); }
  void streamFile(fs::File& f, const String&) { uint8_t buf[256]; while (f.available()) f.read(buf, sizeof(buf)); }
};

class UpdateClass {
  int err_ = UPDATE_ERROR_OK;
public:
  bool begin(size_t) { err_ = UPDATE_ERROR_OK; return true; }
  size_t write(uint8_t*, size_t size) { return size; }
  bool end(bool = false) { return true; }
  void abort() { err_ = UPDATE_ERROR_ABORT; }
  int getError() const { return err_; }
};
inline UpdateClass Update;

class ESPClass {
public:
  void restart() { std::exit(0); }
  uint64_t getEfuseMac() const { return 0xD35C0DEu; }
  uint32_t getFreeHeap() const { return 128u * 1024u * 1024u; }
};
inline ESPClass ESP;
inline int rtc_get_reset_reason(int) { return 0; }

struct PNGFILE { void* fHandle = nullptr; int32_t iSize = 0; int32_t iPos = 0; };
struct PNGDRAW { int y = 0; int iWidth = 0; uint8_t* pPixels = nullptr; };
class PNG {
public:
  int32_t open(const char*, void* (*)(const char*, int32_t*), void (*)(void*), int32_t (*)(PNGFILE*, uint8_t*, int32_t), int32_t (*)(PNGFILE*, int32_t), int (*)(PNGDRAW*)) { return -1; }
  int getPixelType() const { return 0; }
  bool hasAlpha() const { return false; }
  int32_t getWidth() const { return 0; }
  int32_t getHeight() const { return 0; }
  int32_t decode(void*, int) { return -1; }
  void close() {}
  void getLineAsRGB565(PNGDRAW*, uint16_t*, int, uint32_t) {}
  bool getAlphaMask(PNGDRAW*, uint8_t*, int) { return false; }
};

static inline uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); }
#ifndef TFT_BL
#define TFT_BL 0
#endif
#ifndef TFT_BLACK
#define TFT_BLACK     color565(0, 0, 0)
#define TFT_WHITE     color565(255, 255, 255)
#define TFT_RED       color565(255, 0, 0)
#define TFT_GREEN     color565(0, 255, 0)
#define TFT_BLUE      color565(0, 0, 255)
#define TFT_YELLOW    color565(255, 255, 0)
#define TFT_CYAN      color565(0, 255, 255)
#define TFT_DARKGREY  color565(64, 64, 64)
#define TFT_LIGHTGREY color565(192, 192, 192)
#endif

#ifndef TL_DATUM
#define TL_DATUM 0
#define TC_DATUM 1
#define TR_DATUM 2
#define ML_DATUM 3
#define MC_DATUM 4
#define MR_DATUM 5
#endif

struct TS_Point {
  int16_t x;
  int16_t y;
  int16_t z;
  TS_Point(int16_t px = 0, int16_t py = 0, int16_t pz = 0) : x(px), y(py), z(pz) {}
};

inline bool gDesktopRunning = true;
inline SDL_Window* gWindow = nullptr;
inline SDL_Renderer* gRenderer = nullptr;
#if defined(_WIN32)
inline HWND gDesktopHwnd = nullptr;
#endif
inline int gMouseX = 0;
inline int gMouseY = 0;
inline bool gMouseDown = false;

static inline void sdlSetColor565(uint16_t c) {
  uint8_t r = (uint8_t)(((c >> 11) & 0x1F) * 255 / 31);
  uint8_t g = (uint8_t)(((c >> 5) & 0x3F) * 255 / 63);
  uint8_t b = (uint8_t)((c & 0x1F) * 255 / 31);
  SDL_SetRenderDrawColor(gRenderer, r, g, b, 255);
}

inline bool desktopSdlEventPollingEnabled() {
#if defined(_WIN32)
  const char* v = std::getenv("MMDVM_DESKTOP_SDL_EVENTS");
  return v && std::strcmp(v, "1") == 0;
#else
  const char* v = std::getenv("MMDVM_DESKTOP_SDL_EVENTS");
  return !v || std::strcmp(v, "0") != 0;
#endif
}

inline void desktopKeyboardQuitOnly() {
#if defined(_WIN32)
#endif
}


#if defined(_WIN32)
inline void desktopWin32CaptureWindowHandle() {
  if (gDesktopHwnd || !gWindow) return;
  SDL_SysWMinfo info;
  SDL_VERSION(&info.version);
  if (SDL_GetWindowWMInfo(gWindow, &info)) {
    gDesktopHwnd = info.info.win.window;
    desktopTrace("[DESKTOP] Win32 HWND captured for non-SDL message pump.\n");
  } else {
    desktopTrace("[DESKTOP][WARN] SDL_GetWindowWMInfo failed: %s\n", SDL_GetError());
  }
}

inline int desktopWin32SignedLowWord(LPARAM v) { return (int)(short)(v & 0xFFFF); }
inline int desktopWin32SignedHighWord(LPARAM v) { return (int)(short)((v >> 16) & 0xFFFF); }

inline bool desktopWin32PumpMessagesNoSDL() {
  desktopWin32CaptureWindowHandle();
  MSG msg;
  bool keepRunning = true;
  while (::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
    switch (msg.message) {
      case WM_QUIT:
      case WM_CLOSE:
      case WM_DESTROY:
        keepRunning = false;
        break;
      case WM_MOUSEMOVE:
        if (!gDesktopHwnd || msg.hwnd == gDesktopHwnd) {
          gMouseX = constrain(desktopWin32SignedLowWord(msg.lParam), 0, DESKTOP_SCREEN_W - 1);
          gMouseY = constrain(desktopWin32SignedHighWord(msg.lParam), 0, DESKTOP_SCREEN_H - 1);
        }
        break;
      case WM_LBUTTONDOWN:
        if (!gDesktopHwnd || msg.hwnd == gDesktopHwnd) {
          gMouseDown = true;
          gMouseX = constrain(desktopWin32SignedLowWord(msg.lParam), 0, DESKTOP_SCREEN_W - 1);
          gMouseY = constrain(desktopWin32SignedHighWord(msg.lParam), 0, DESKTOP_SCREEN_H - 1);
          if (gDesktopHwnd) ::SetCapture(gDesktopHwnd);
        }
        break;
      case WM_LBUTTONUP:
        if (!gDesktopHwnd || msg.hwnd == gDesktopHwnd || ::GetCapture() == gDesktopHwnd) {
          gMouseDown = false;
          gMouseX = constrain(desktopWin32SignedLowWord(msg.lParam), 0, DESKTOP_SCREEN_W - 1);
          gMouseY = constrain(desktopWin32SignedHighWord(msg.lParam), 0, DESKTOP_SCREEN_H - 1);
          if (::GetCapture() == gDesktopHwnd) ::ReleaseCapture();
        }
        break;
    }
    ::TranslateMessage(&msg);
    ::DispatchMessage(&msg);
  }

  if (gDesktopHwnd) {
    POINT pt;
    if (::GetCursorPos(&pt) && ::ScreenToClient(gDesktopHwnd, &pt)) {
      const bool inside = (pt.x >= 0 && pt.y >= 0 && pt.x < DESKTOP_SCREEN_W && pt.y < DESKTOP_SCREEN_H);
      const bool leftDown = ((::GetKeyState(VK_LBUTTON) & 0x8000) != 0);
      if (inside) {
        gMouseX = constrain((int)pt.x, 0, DESKTOP_SCREEN_W - 1);
        gMouseY = constrain((int)pt.y, 0, DESKTOP_SCREEN_H - 1);
        gMouseDown = leftDown;
      } else if (!leftDown) {
        gMouseDown = false;
      }
    }
  }

  if (!keepRunning) gDesktopRunning = false;
  return keepRunning;
}
#else
inline bool desktopWin32PumpMessagesNoSDL() { return true; }
#endif

inline void desktopPollEventsOnly() {
  if (!gWindow) return;
#if defined(_WIN32)
  if (!desktopSdlEventPollingEnabled()) {
    desktopWin32PumpMessagesNoSDL();
    return;
  }
#endif
  SDL_Event ev;
  while (SDL_PollEvent(&ev)) {
    if (ev.type == SDL_QUIT) gDesktopRunning = false;
    else if (ev.type == SDL_MOUSEMOTION) { gMouseX = ev.motion.x; gMouseY = ev.motion.y; }
    else if (ev.type == SDL_MOUSEBUTTONDOWN) { gMouseDown = true; gMouseX = ev.button.x; gMouseY = ev.button.y; }
    else if (ev.type == SDL_MOUSEBUTTONUP) { gMouseDown = false; gMouseX = ev.button.x; gMouseY = ev.button.y; }
    else if (ev.type == SDL_KEYDOWN && (ev.key.keysym.sym == SDLK_ESCAPE || ev.key.keysym.sym == SDLK_q)) { gDesktopRunning = false; }
  }
}

inline bool desktopRunning() {
#if defined(_WIN32)
  return gDesktopRunning;
#else
  desktopPollEventsOnly();
  return gDesktopRunning;
#endif
}
inline void desktopShutdown() {
  if (gRenderer) { SDL_DestroyRenderer(gRenderer); gRenderer = nullptr; }
  if (gWindow) { SDL_DestroyWindow(gWindow); gWindow = nullptr; }
  SDL_Quit();
  if (desktopLogFile()) { std::fflush(desktopLogFile()); std::fclose(desktopLogFile()); desktopLogFile() = nullptr; }
}

class DisplayCompat {
  uint8_t datum_ = TL_DATUM;
  uint8_t font_ = 2;
  uint8_t textSize_ = 1;
  uint16_t fg_ = TFT_WHITE;
  uint16_t bg_ = TFT_BLACK;

  int fontScale() const {
    int base = 1;
    if (font_ >= 4) base = 3;
    else if (font_ >= 2) base = 2;
    return std::max(1, base * (int)textSize_);
  }
  int glyphW() const { return (5 * fontScale()) + fontScale(); }
  int glyphH() const { return 7 * fontScale(); }
  static const uint8_t* glyphRows(char ch) {
    static const uint8_t SPACE[7]={0,0,0,0,0,0,0};
    static const uint8_t EXCL[7]={4,4,4,4,4,0,4};
    static const uint8_t QUOT[7]={10,10,10,0,0,0,0};
    static const uint8_t HASH[7]={10,10,31,10,31,10,10};
    static const uint8_t DOLL[7]={4,15,20,14,5,30,4};
    static const uint8_t PERC[7]={24,25,2,4,8,19,3};
    static const uint8_t AMP[7]={12,18,20,8,21,18,13};
    static const uint8_t APOS[7]={4,4,8,0,0,0,0};
    static const uint8_t LPAREN[7]={2,4,8,8,8,4,2};
    static const uint8_t RPAREN[7]={8,4,2,2,2,4,8};
    static const uint8_t STAR[7]={0,4,21,14,21,4,0};
    static const uint8_t PLUS[7]={0,4,4,31,4,4,0};
    static const uint8_t COMMA[7]={0,0,0,0,4,4,8};
    static const uint8_t MINUS[7]={0,0,0,31,0,0,0};
    static const uint8_t DOT[7]={0,0,0,0,0,12,12};
    static const uint8_t SLASH[7]={1,2,2,4,8,8,16};
    static const uint8_t COLON[7]={0,12,12,0,12,12,0};
    static const uint8_t SEMI[7]={0,12,12,0,4,4,8};
    static const uint8_t LT[7]={2,4,8,16,8,4,2};
    static const uint8_t EQ[7]={0,0,31,0,31,0,0};
    static const uint8_t GT[7]={8,4,2,1,2,4,8};
    static const uint8_t QUEST[7]={14,17,1,2,4,0,4};
    static const uint8_t AT[7]={14,17,23,21,23,16,14};
    static const uint8_t LBR[7]={14,8,8,8,8,8,14};
    static const uint8_t BS[7]={16,8,8,4,2,2,1};
    static const uint8_t RBR[7]={14,2,2,2,2,2,14};
    static const uint8_t CARET[7]={4,10,17,0,0,0,0};
    static const uint8_t UNDER[7]={0,0,0,0,0,0,31};
    static const uint8_t GRAVE[7]={8,4,2,0,0,0,0};
    static const uint8_t LBRACE[7]={2,4,4,8,4,4,2};
    static const uint8_t BAR[7]={4,4,4,4,4,4,4};
    static const uint8_t RBRACE[7]={8,4,4,2,4,4,8};
    static const uint8_t TILDE[7]={0,0,8,21,2,0,0};
    static const uint8_t D0[7]={14,17,19,21,25,17,14};
    static const uint8_t D1[7]={4,12,4,4,4,4,14};
    static const uint8_t D2[7]={14,17,1,2,4,8,31};
    static const uint8_t D3[7]={30,1,1,14,1,1,30};
    static const uint8_t D4[7]={2,6,10,18,31,2,2};
    static const uint8_t D5[7]={31,16,30,1,1,17,14};
    static const uint8_t D6[7]={6,8,16,30,17,17,14};
    static const uint8_t D7[7]={31,1,2,4,8,8,8};
    static const uint8_t D8[7]={14,17,17,14,17,17,14};
    static const uint8_t D9[7]={14,17,17,15,1,2,12};
    static const uint8_t A[7]={14,17,17,31,17,17,17};
    static const uint8_t B[7]={30,17,17,30,17,17,30};
    static const uint8_t C[7]={14,17,16,16,16,17,14};
    static const uint8_t D[7]={30,17,17,17,17,17,30};
    static const uint8_t E[7]={31,16,16,30,16,16,31};
    static const uint8_t F[7]={31,16,16,30,16,16,16};
    static const uint8_t G[7]={14,17,16,23,17,17,15};
    static const uint8_t H[7]={17,17,17,31,17,17,17};
    static const uint8_t I[7]={14,4,4,4,4,4,14};
    static const uint8_t J[7]={7,2,2,2,18,18,12};
    static const uint8_t K[7]={17,18,20,24,20,18,17};
    static const uint8_t L[7]={16,16,16,16,16,16,31};
    static const uint8_t M[7]={17,27,21,21,17,17,17};
    static const uint8_t N[7]={17,25,21,19,17,17,17};
    static const uint8_t O[7]={14,17,17,17,17,17,14};
    static const uint8_t P[7]={30,17,17,30,16,16,16};
    static const uint8_t Q[7]={14,17,17,17,21,18,13};
    static const uint8_t R[7]={30,17,17,30,20,18,17};
    static const uint8_t S[7]={15,16,16,14,1,1,30};
    static const uint8_t T[7]={31,4,4,4,4,4,4};
    static const uint8_t U[7]={17,17,17,17,17,17,14};
    static const uint8_t V[7]={17,17,17,17,17,10,4};
    static const uint8_t W[7]={17,17,17,21,21,21,10};
    static const uint8_t X[7]={17,17,10,4,10,17,17};
    static const uint8_t Y[7]={17,17,10,4,4,4,4};
    static const uint8_t Z[7]={31,1,2,4,8,16,31};
    if (ch >= 'a' && ch <= 'z') ch = (char)std::toupper((unsigned char)ch);
    switch (ch) {
      case ' ': return SPACE; case '!': return EXCL; case '"': return QUOT; case '#': return HASH;
      case '$': return DOLL; case '%': return PERC; case '&': return AMP; case '\'': return APOS;
      case '(': return LPAREN; case ')': return RPAREN; case '*': return STAR; case '+': return PLUS;
      case ',': return COMMA; case '-': return MINUS; case '.': return DOT; case '/': return SLASH;
      case ':': return COLON; case ';': return SEMI; case '<': return LT; case '=': return EQ;
      case '>': return GT; case '?': return QUEST; case '@': return AT; case '[': return LBR;
      case '\\': return BS; case ']': return RBR; case '^': return CARET; case '_': return UNDER;
      case '`': return GRAVE; case '{': return LBRACE; case '|': return BAR; case '}': return RBRACE;
      case '~': return TILDE;
      case '0': return D0; case '1': return D1; case '2': return D2; case '3': return D3; case '4': return D4;
      case '5': return D5; case '6': return D6; case '7': return D7; case '8': return D8; case '9': return D9;
      case 'A': return A; case 'B': return B; case 'C': return C; case 'D': return D; case 'E': return E;
      case 'F': return F; case 'G': return G; case 'H': return H; case 'I': return I; case 'J': return J;
      case 'K': return K; case 'L': return L; case 'M': return M; case 'N': return N; case 'O': return O;
      case 'P': return P; case 'Q': return Q; case 'R': return R; case 'S': return S; case 'T': return T;
      case 'U': return U; case 'V': return V; case 'W': return W; case 'X': return X; case 'Y': return Y; case 'Z': return Z;
      default: return QUEST;
    }
  }
  void drawGlyphBitmap(char ch, int x, int y, uint16_t color) {
    if (ch == ' ') return;
    const int scale = fontScale();
    const uint8_t* rows = glyphRows(ch);
    sdlSetColor565(color);
    for (int row = 0; row < 7; ++row) {
      for (int col = 0; col < 5; ++col) {
        if (rows[row] & (1u << (4 - col))) {
          SDL_Rect r { x + col * scale, y + row * scale, scale, scale };
          SDL_RenderFillRect(gRenderer, &r);
        }
      }
    }
  }
public:
  void init() {
    if (!gWindow) {
      desktopInitLog();
      SDL_SetMainReady();
      if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
        desktopTrace("[DESKTOP][FATAL] SDL_Init failed: %s\n", SDL_GetError());
        gDesktopRunning = false;
        return;
      }
      gWindow = SDL_CreateWindow("MMDVM-Display - Desktop", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, DESKTOP_SCREEN_W, DESKTOP_SCREEN_H, SDL_WINDOW_SHOWN);
      if (!gWindow) {
        desktopTrace("[DESKTOP][FATAL] SDL_CreateWindow failed: %s\n", SDL_GetError());
        gDesktopRunning = false;
        return;
      }
      const char* useAccel = std::getenv("MMDVM_DESKTOP_ACCEL");
      if (useAccel && std::strcmp(useAccel, "0") != 0) {
        gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED);
      }
      if (!gRenderer) gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_SOFTWARE);
      if (!gRenderer) {
        desktopTrace("[DESKTOP][FATAL] SDL renderer creation failed: %s\n", SDL_GetError());
        gDesktopRunning = false;
        return;
      } else {
        desktopTrace("[DESKTOP] SDL renderer ready (%s).\n", (useAccel && std::strcmp(useAccel, "0") != 0) ? "accelerated" : "software");
      }
    }
    fillScreen(TFT_BLACK);
    flush();
  }
  bool ready() const { return gRenderer != nullptr; }
  void setRotation(uint8_t) {}
  int16_t width() const { return DESKTOP_SCREEN_W; }
  int16_t height() const { return DESKTOP_SCREEN_H; }
  void flush() { if (ready()) { desktopPollEventsOnly(); SDL_RenderPresent(gRenderer); } }
  void setTextWrap(bool) {}
  void setTextDatum(uint8_t d) { datum_ = d; }
  void setTextFont(uint8_t f) { font_ = f ? f : 1; }
  void setTextSize(uint8_t s) { textSize_ = s ? s : 1; }
  void setTextColor(uint16_t fg, uint16_t bg = TFT_BLACK) { fg_ = fg; bg_ = bg; }
  int16_t textWidth(const String& s) { return (int16_t)(s.length() * glyphW()); }

  void fillScreen(uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_RenderClear(gRenderer); }
  void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_Rect r{x,y,w,h}; SDL_RenderFillRect(gRenderer,&r); }
  void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_Rect r{x,y,w,h}; SDL_RenderDrawRect(gRenderer,&r); }
  void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t, uint16_t c) { drawRect(x,y,w,h,c); }
  void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t, uint16_t c) { fillRect(x,y,w,h,c); }
  void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_RenderDrawLine(gRenderer, x, y, x + w - 1, y); }
  void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_RenderDrawLine(gRenderer, x, y, x, y + h - 1); }
  void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_RenderDrawLine(gRenderer, x0, y0, x1, y1); }
  void drawPixel(int16_t x, int16_t y, uint16_t c) { if (!ready()) return; sdlSetColor565(c); SDL_RenderDrawPoint(gRenderer, x, y); }
  void fillCircle(int16_t x0, int16_t y0, int16_t r, uint16_t c) { if (!ready()) return; sdlSetColor565(c); for (int y=-r;y<=r;y++) for (int x=-r;x<=r;x++) if (x*x+y*y<=r*r) SDL_RenderDrawPoint(gRenderer,x0+x,y0+y); }
  void drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t c) { if (!ready()) return; sdlSetColor565(c); for (int a=0;a<360;a+=4) { double rad=a*3.141592653589793/180.0; SDL_RenderDrawPoint(gRenderer,x0+(int)(std::cos(rad)*r),y0+(int)(std::sin(rad)*r)); } }
  void pushImage(int16_t x, int16_t y, int16_t w, int16_t h, const uint16_t* data) { if (!data) return; for (int yy=0; yy<h; ++yy) for (int xx=0; xx<w; ++xx) drawPixel(x+xx, y+yy, data[yy*w+xx]); }
  void pushLvglArea(int16_t x, int16_t y, int16_t w, int16_t h, const uint16_t* data) { pushImage(x,y,w,h,data); }
  void startWrite() {}
  void endWrite() {}
  int16_t drawString(const String& text, int16_t x, int16_t y) {
    if (!ready()) return 0;
    int w = textWidth(text);
    int h = glyphH();
    int sx = x, sy = y;
    if (datum_ == TC_DATUM || datum_ == MC_DATUM) sx -= w / 2;
    else if (datum_ == TR_DATUM || datum_ == MR_DATUM) sx -= w;
    if (datum_ == ML_DATUM || datum_ == MC_DATUM || datum_ == MR_DATUM) sy -= h / 2;
    fillRect(sx, sy, w, h, bg_);
    for (size_t i=0; i<text.length(); ++i) drawGlyphBitmap(text.charAt(i), sx + (int)i * glyphW(), sy, fg_);
    return w;
  }
};

class TouchCompat {
public:
  void begin(SPIClass&) {}
  void setRotation(uint8_t) {}
  bool touched() { desktopPollEventsOnly(); return gMouseDown; }
  TS_Point getPoint() { desktopPollEventsOnly(); return TS_Point((int16_t)gMouseX, (int16_t)gMouseY, gMouseDown ? 1 : 0); }
};

