#pragma once

#include <Arduino.h>

#if defined(ARDUINO_ARCH_ESP32)
#include <driver/spi_master.h>
#include <esp_err.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#ifndef SPI_DMA_DISABLED
#define SPI_DMA_DISABLED 0
#endif
#endif

#ifndef WF100DPZ_BACKEND
#if defined(ARDUINO_ARCH_ESP32)
#define WF100DPZ_BACKEND 2  // 1=bit-bang 3-wire, 2=ESP-IDF 3-wire
#else
#define WF100DPZ_BACKEND 1
#endif
#endif

class WF100DPZCoreBitBang {
 public:
  enum class Mode : uint8_t {
    TempOnly = 0b000,
    PressureOnly = 0b001,
    Combined = 0b010,
    SleepPeriodic = 0b011,
  };

  struct Status {
    uint8_t raw = 0;
    bool drdy = false;
    uint8_t error_code = 0;
  };

  struct Snapshot {
    uint8_t spi_ctrl = 0;
    uint8_t part_id = 0;
    uint8_t status = 0;
    uint8_t cmd = 0;
  };

  WF100DPZCoreBitBang(int pinCsb, int pinSda, int pinClk);

  bool begin();

  void setSleepTimeCode(uint8_t code);
  uint8_t getSleepTimeCode() const;

  uint8_t readPartId();
  Status readStatus();
  Snapshot readSnapshot();
  uint8_t readSpiCtrl();

  void trigger(Mode mode);
  bool waitReady(uint32_t& elapsed_us, bool& drdy_hit);
  bool waitForDrdyState(bool high, uint32_t timeout_ms, uint32_t& detect_us);

  bool measureTemp(float& temp_c, uint32_t& wait_us, bool& drdy_hit);
  bool measurePressure(float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool measureCombined(float& temp_c, float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool readLatestCombined(float& temp_c, float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool dataReady();
  bool readLatestTempNow(float& temp_c);
  bool readLatestPressureNow(float& pressure_kpa);
  bool readLatestCombinedNow(float& temp_c, float& pressure_kpa);
  bool measureTemp(float& temp_c) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measureTemp(temp_c, wait_us, drdy_hit);
  }
  bool measurePressure(float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measurePressure(pressure_kpa, wait_us, drdy_hit);
  }
  bool measureCombined(float& temp_c, float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measureCombined(temp_c, pressure_kpa, wait_us, drdy_hit);
  }
  bool readLatestCombined(float& temp_c, float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return readLatestCombined(temp_c, pressure_kpa, wait_us, drdy_hit);
  }

 private:
  static constexpr uint32_t kHalfclkUs = 0;
  static constexpr uint32_t kDrdyTimeoutMs = 12;
  static constexpr uint32_t kDrdyPollUs = 50;

  static constexpr uint8_t kRegSpiCtrl = 0x00;
  static constexpr uint8_t kRegPartId = 0x01;
  static constexpr uint8_t kRegStatus = 0x02;
  static constexpr uint8_t kRegDataMsb = 0x06;
  static constexpr uint8_t kRegDataCsb = 0x07;
  static constexpr uint8_t kRegDataLsb = 0x08;
  static constexpr uint8_t kRegTempMsb = 0x09;
  static constexpr uint8_t kRegTempLsb = 0x0A;
  static constexpr uint8_t kRegCmd = 0x30;

  static constexpr uint8_t kCmdRead = 0x80;
  static constexpr uint8_t kCmdWrite = 0x00;

  static constexpr uint8_t kStatusDrdyMask = 0x01;
  static constexpr uint8_t kStatusErrorCodeMask = 0xF0;
  static constexpr uint8_t kStatusErrorCodeShift = 4;

  int pin_csb_;
  int pin_sda_;
  int pin_clk_;
  uint8_t sleep_time_code_ = 0x00;

  void SDAOut();
  void SDAIn();
  void csLow();
  void csHigh();
  void clkLow();
  void clkHigh();
  void SDAWrite(bool v);
  bool SDARead();
  void halfclkDelay();
  void spiWriteByte(uint8_t b);
  uint8_t spiReadByte();

  void writeReg(uint8_t addr, uint8_t value);
  uint8_t readReg(uint8_t addr);
  void readTempRegs(uint8_t& t_msb, uint8_t& t_lsb);
  void readPressureRegs(uint8_t& p_msb, uint8_t& p_csb, uint8_t& p_lsb);
};

#if defined(ARDUINO_ARCH_ESP32)
class WF100DPZCoreEspIdf3Wire {
 public:
  enum class Mode : uint8_t {
    TempOnly = 0b000,
    PressureOnly = 0b001,
    Combined = 0b010,
    SleepPeriodic = 0b011,
  };

  struct Status {
    uint8_t raw = 0;
    bool drdy = false;
    uint8_t error_code = 0;
  };

  struct Snapshot {
    uint8_t spi_ctrl = 0;
    uint8_t part_id = 0;
    uint8_t status = 0;
    uint8_t cmd = 0;
  };

  WF100DPZCoreEspIdf3Wire(int pinCsb, int pinSda, int pinClk, spi_host_device_t host, uint32_t spi_hz);

  bool begin();
  void configure(spi_host_device_t host, uint32_t spi_hz);
  bool setClockHz(uint32_t spi_hz);
  uint32_t getClockHz() const;

  void setSleepTimeCode(uint8_t code);
  uint8_t getSleepTimeCode() const;

  uint8_t readPartId();
  Status readStatus();
  Snapshot readSnapshot();
  uint8_t readSpiCtrl();

  void trigger(Mode mode);
  bool waitReady(uint32_t& elapsed_us, bool& drdy_hit);
  bool waitForDrdyState(bool high, uint32_t timeout_ms, uint32_t& detect_us);

  bool measureTemp(float& temp_c, uint32_t& wait_us, bool& drdy_hit);
  bool measurePressure(float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool measureCombined(float& temp_c, float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool readLatestCombined(float& temp_c, float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool dataReady();
  bool readLatestTempNow(float& temp_c);
  bool readLatestPressureNow(float& pressure_kpa);
  bool readLatestCombinedNow(float& temp_c, float& pressure_kpa);
  bool measureTemp(float& temp_c) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measureTemp(temp_c, wait_us, drdy_hit);
  }
  bool measurePressure(float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measurePressure(pressure_kpa, wait_us, drdy_hit);
  }
  bool measureCombined(float& temp_c, float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measureCombined(temp_c, pressure_kpa, wait_us, drdy_hit);
  }
  bool readLatestCombined(float& temp_c, float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return readLatestCombined(temp_c, pressure_kpa, wait_us, drdy_hit);
  }

 private:
  static constexpr uint32_t kDrdyTimeoutMs = 12;
  static constexpr uint32_t kDrdyPollUs = 50;

  static constexpr uint8_t kRegSpiCtrl = 0x00;
  static constexpr uint8_t kRegPartId = 0x01;
  static constexpr uint8_t kRegStatus = 0x02;
  static constexpr uint8_t kRegDataMsb = 0x06;
  static constexpr uint8_t kRegDataCsb = 0x07;
  static constexpr uint8_t kRegDataLsb = 0x08;
  static constexpr uint8_t kRegTempMsb = 0x09;
  static constexpr uint8_t kRegTempLsb = 0x0A;
  static constexpr uint8_t kRegCmd = 0x30;

  static constexpr uint8_t kCmdRead = 0x80;
  static constexpr uint8_t kCmdWrite = 0x00;

  static constexpr uint8_t kStatusDrdyMask = 0x01;
  static constexpr uint8_t kStatusErrorCodeMask = 0xF0;
  static constexpr uint8_t kStatusErrorCodeShift = 4;

  int pin_csb_;
  int pin_sda_;
  int pin_clk_;
  spi_host_device_t host_;
  uint32_t spi_hz_;
  spi_device_handle_t dev_ = nullptr;
  uint8_t sleep_time_code_ = 0x00;
  bool last_cmd_write_ok_ = true;

  bool addDevice(uint32_t spi_hz);
  void csLow();
  void csHigh();
  bool writeReg(uint8_t addr, uint8_t value);
  uint8_t readReg(uint8_t addr);
  void readTempRegs(uint8_t& t_msb, uint8_t& t_lsb);
  void readPressureRegs(uint8_t& p_msb, uint8_t& p_csb, uint8_t& p_lsb);
};
#endif

#if WF100DPZ_BACKEND == 2 && defined(ARDUINO_ARCH_ESP32)
using WF100DPZCoreDefaultBackend = WF100DPZCoreEspIdf3Wire;
#else
using WF100DPZCoreDefaultBackend = WF100DPZCoreBitBang;
#endif

enum class WF100DPZCoreBackendKind : uint8_t {
  BitBang = 1,
  EspIdf3Wire = 2,
};

template <WF100DPZCoreBackendKind Kind>
class WF100DPZCoreManual;

template <>
class WF100DPZCoreManual<WF100DPZCoreBackendKind::BitBang> : public WF100DPZCoreBitBang {
 public:
  WF100DPZCoreManual(int pinCsb, int pinSda, int pinClk) : WF100DPZCoreBitBang(pinCsb, pinSda, pinClk) {}
};

#if defined(ARDUINO_ARCH_ESP32)
template <>
class WF100DPZCoreManual<WF100DPZCoreBackendKind::EspIdf3Wire> : public WF100DPZCoreEspIdf3Wire {
 public:
  WF100DPZCoreManual(int pinCsb, int pinSda, int pinClk, uint32_t spi_hz = 20000000,
                 spi_host_device_t host = SPI2_HOST)
      : WF100DPZCoreEspIdf3Wire(pinCsb, pinSda, pinClk, host, spi_hz) {}
};
#endif

class WF100DPZCore {
 public:
  enum class Backend : uint8_t {
    Auto = 0,
    BitBang = 1,
    EspIdf3Wire = 2,
  };

  enum class Error : uint8_t {
    None = 0,
    UnsupportedBackendOnPlatform = 1,
    BackendInitFailed = 2,
    BackendNotInitialized = 3,
  };
  enum class NonBlockingState : uint8_t {
    Idle = 0,
    Pending = 1,
  };

  struct NonBlockingResult {
    WF100DPZCoreBitBang::Mode mode = WF100DPZCoreBitBang::Mode::Combined;
    bool has_temp = false;
    bool has_pressure = false;
    float temp_c = 0.0f;
    float pressure_kpa = 0.0f;
    uint8_t status_raw = 0;
    uint8_t error_code = 0;
  };

  using Mode = WF100DPZCoreBitBang::Mode;
  using Status = WF100DPZCoreBitBang::Status;
  using Snapshot = WF100DPZCoreBitBang::Snapshot;

#if defined(ARDUINO_ARCH_ESP32)
  WF100DPZCore(int pinCsb, int pinSda, int pinClk, Backend backend = Backend::Auto,
           uint32_t spi_hz = 20000000, spi_host_device_t host = SPI2_HOST);
#else
  WF100DPZCore(int pinCsb, int pinSda, int pinClk, Backend backend = Backend::Auto,
           uint32_t spi_hz = 20000000);
#endif

  bool begin();

  Backend requestedBackend() const { return requested_backend_; }
  Backend getActiveBackend() const { return active_backend_; }
  Error lastError() const { return last_error_; }
  const char* lastErrorString() const;

  void setSleepTimeCode(uint8_t code);
  uint8_t getSleepTimeCode() const;

  uint8_t readPartId();
  Status readStatus();
  Snapshot readSnapshot();
  uint8_t readSpiCtrl();

  void trigger(Mode mode);
  bool waitReady(uint32_t& elapsed_us, bool& drdy_hit);
  bool waitForDrdyState(bool high, uint32_t timeout_ms, uint32_t& detect_us);

  bool measureTemp(float& temp_c, uint32_t& wait_us, bool& drdy_hit);
  bool measurePressure(float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool measureCombined(float& temp_c, float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool readLatestCombined(float& temp_c, float& pressure_kpa, uint32_t& wait_us, bool& drdy_hit);
  bool measureTemp(float& temp_c) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measureTemp(temp_c, wait_us, drdy_hit);
  }
  bool measurePressure(float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measurePressure(pressure_kpa, wait_us, drdy_hit);
  }
  bool measureCombined(float& temp_c, float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return measureCombined(temp_c, pressure_kpa, wait_us, drdy_hit);
  }
  bool readLatestCombined(float& temp_c, float& pressure_kpa) {
    uint32_t wait_us = 0;
    bool drdy_hit = false;
    return readLatestCombined(temp_c, pressure_kpa, wait_us, drdy_hit);
  }

  // Non-blocking one-shot workflow:
  // 1) beginNonBlocking(mode)
  // 2) poll nonBlockingReady()
  // 3) readNonBlocking(result) when ready
  bool beginNonBlocking(Mode mode);
  bool nonBlockingReady();
  bool readNonBlocking(NonBlockingResult& out);
  void cancelNonBlocking();
  NonBlockingState nonBlockingState() const { return nb_state_; }

#if defined(ARDUINO_ARCH_ESP32)
  bool setClockHz(uint32_t spi_hz);
  uint32_t getClockHz() const;
  void configureRequested(Backend backend, uint32_t spi_hz, spi_host_device_t host);
#else
  void configureRequested(Backend backend, uint32_t spi_hz);
#endif

 private:
  Backend requested_backend_ = Backend::Auto;
  Backend active_backend_ = Backend::Auto;
  Error last_error_ = Error::None;
  NonBlockingState nb_state_ = NonBlockingState::Idle;
  Mode nb_mode_ = Mode::Combined;

  WF100DPZCoreBitBang bitbang_;
#if defined(ARDUINO_ARCH_ESP32)
  WF100DPZCoreEspIdf3Wire espidf_;
  mutable SemaphoreHandle_t api_lock_ = nullptr;
#endif

  bool ensureInitialized();
  void clearNonBlocking();
#if defined(ARDUINO_ARCH_ESP32)
  bool ensureLock();
  bool lockApi() const;
  void unlockApi() const;
#endif
  static WF100DPZCoreBitBang::Mode toBitBangMode(Mode mode) { return mode; }
#if defined(ARDUINO_ARCH_ESP32)
  static WF100DPZCoreEspIdf3Wire::Mode toEspMode(Mode mode) {
    return static_cast<WF100DPZCoreEspIdf3Wire::Mode>(static_cast<uint8_t>(mode));
  }
#endif
};



