#include "MicrophoneRecorder.h"

#include <SPI.h>
#include <Wire.h>
#include <driver/i2s.h>
#include <limits.h>
#include <stdarg.h>
#include <time.h>

#include "driver/gpio.h"
#include "tbeam_supreme_adapter.h"

#ifndef MIC_SAMPLE_RATE
#define MIC_SAMPLE_RATE 16000
#endif

#ifndef MIC_BITS_PER_SAMPLE
#define MIC_BITS_PER_SAMPLE 16
#endif

#ifndef MIC_DATA_PIN
#define MIC_DATA_PIN 38
#endif

#ifndef MIC_CLK_PIN
#define MIC_CLK_PIN 39
#endif

#ifndef MIC_SELECT_PIN
#define MIC_SELECT_PIN 48
#endif

#ifndef MIC_SELECT_LEFT
#define MIC_SELECT_LEFT 1
#endif

#ifndef FW_BUILD_EPOCH
#define FW_BUILD_EPOCH 0
#endif

namespace {

constexpr i2s_port_t kI2sPort = I2S_NUM_0;
constexpr uint32_t kChunkBytes = 2048;
constexpr uint8_t kWavChannels = 1;
constexpr uint32_t kValidEpochFloor = 1700000000UL;

}  // namespace

MicrophoneRecorder::MicrophoneRecorder(Print& out) : out_(out) {}

MicrophoneRecorder::~MicrophoneRecorder() {
  close();
  deactivateMicrophone();
}

bool MicrophoneRecorder::begin() {
  micCfg_ = {
      .dataPin = MIC_DATA_PIN,
      .clkPin = MIC_CLK_PIN,
      .selectPin = MIC_SELECT_PIN,
      .selectLeft = (MIC_SELECT_LEFT != 0),
  };

  forceSpiDeselected();
  pmuReady_ = initPmu();
  if (!pmuReady_) {
    return false;
  }

  cycleSdRail();
  sdReady_ = mountSdCard();
  if (!sdReady_) {
    logf("SD mount failed");
    return false;
  }

  if (!ensureDirectory("/recordings")) {
    logf("failed to create /recordings");
    return false;
  }

  logf("ready: sample_rate=%u bits=%u data=%d clk=%d sel=%d",
       (unsigned)MIC_SAMPLE_RATE,
       (unsigned)MIC_BITS_PER_SAMPLE,
       micCfg_.dataPin,
       micCfg_.clkPin,
       micCfg_.selectPin);
  return true;
}

void MicrophoneRecorder::setRuntimeConfig(const MicRuntimeConfig& cfg) {
  deactivateMicrophone();
  micCfg_ = cfg;
  logf("config updated: data=%d clk=%d select=%d left=%d",
       micCfg_.dataPin,
       micCfg_.clkPin,
       micCfg_.selectPin,
       micCfg_.selectLeft ? 1 : 0);
}

bool MicrophoneRecorder::activateMicrophone() {
  if (micActive_) {
    return true;
  }

  pinMode(micCfg_.selectPin, OUTPUT);
  digitalWrite(micCfg_.selectPin, micCfg_.selectLeft ? LOW : HIGH);

  const i2s_config_t config = {
      .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
      .sample_rate = MIC_SAMPLE_RATE,
      .bits_per_sample = (i2s_bits_per_sample_t)MIC_BITS_PER_SAMPLE,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_STAND_I2S,
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
      .dma_buf_count = 8,
      .dma_buf_len = 256,
      .use_apll = false,
      .tx_desc_auto_clear = false,
      .fixed_mclk = 0,
#if ESP_IDF_VERSION_MAJOR >= 4
      .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
      .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
#endif
  };

  const i2s_pin_config_t pins = {
      .bck_io_num = I2S_PIN_NO_CHANGE,
      .ws_io_num = micCfg_.clkPin,
      .data_out_num = I2S_PIN_NO_CHANGE,
      .data_in_num = micCfg_.dataPin,
  };

  i2s_driver_uninstall(kI2sPort);
  esp_err_t err = i2s_driver_install(kI2sPort, &config, 0, nullptr);
  if (err != ESP_OK) {
    logf("i2s_driver_install failed err=%d", (int)err);
    return false;
  }

  err = i2s_set_pin(kI2sPort, &pins);
  if (err != ESP_OK) {
    logf("i2s_set_pin failed err=%d", (int)err);
    i2s_driver_uninstall(kI2sPort);
    return false;
  }

  err = i2s_set_pdm_rx_down_sample(kI2sPort, I2S_PDM_DSR_8S);
  if (err != ESP_OK) {
    logf("i2s_set_pdm_rx_down_sample failed err=%d", (int)err);
    i2s_driver_uninstall(kI2sPort);
    return false;
  }

  i2s_zero_dma_buffer(kI2sPort);
  micActive_ = true;
  logf("microphone active clk(ws)=%d data=%d select=%d left=%d",
       micCfg_.clkPin,
       micCfg_.dataPin,
       micCfg_.selectPin,
       micCfg_.selectLeft ? 1 : 0);
  return true;
}

void MicrophoneRecorder::deactivateMicrophone() {
  if (!micActive_) {
    return;
  }

  i2s_driver_uninstall(kI2sPort);
  micActive_ = false;
  logf("microphone inactive");
}

bool MicrophoneRecorder::open(const char* path) {
  if (!sdReady_) {
    logf("open refused: SD not ready");
    return false;
  }

  close();

  strncpy(currentPath_, path, sizeof(currentPath_) - 1);
  currentPath_[sizeof(currentPath_) - 1] = '\0';

  file_ = SD.open(currentPath_, FILE_WRITE);
  if (!file_) {
    logf("failed to open %s", currentPath_);
    currentPath_[0] = '\0';
    return false;
  }

  wavDataBytes_ = 0;
  fileOpen_ = writeWavHeader();
  if (!fileOpen_) {
    file_.close();
    currentPath_[0] = '\0';
    return false;
  }

  logf("opened %s", currentPath_);
  return true;
}

bool MicrophoneRecorder::openTimestamped() {
  const uint32_t stamp = selectTimestamp();
  char path[96];
  snprintf(path, sizeof(path), "/recordings/mic_%lu.wav", (unsigned long)stamp);
  return open(path);
}

bool MicrophoneRecorder::captureForMs(uint32_t durationMs) {
  if (!fileOpen_) {
    logf("capture refused: no open file");
    return false;
  }
  if (!activateMicrophone()) {
    return false;
  }

  int16_t samples[kChunkBytes / sizeof(int16_t)];
  const uint32_t startMs = millis();
  uint32_t lastProgressMs = startMs;
  int16_t secondMin = INT16_MAX;
  int16_t secondMax = INT16_MIN;
  uint32_t secondSampleCount = 0;
  uint64_t secondAbsSum = 0;

  while ((uint32_t)(millis() - startMs) < durationMs) {
    size_t bytesRead = 0;
    const esp_err_t err =
        i2s_read(kI2sPort, samples, sizeof(samples), &bytesRead, pdMS_TO_TICKS(250));
    if (err != ESP_OK) {
      logf("i2s_read failed err=%d", (int)err);
      return false;
    }

    if (bytesRead > 0) {
      const size_t sampleCount = bytesRead / sizeof(samples[0]);
      for (size_t i = 0; i < sampleCount; ++i) {
        const int16_t value = samples[i];
        if (value < secondMin) secondMin = value;
        if (value > secondMax) secondMax = value;
        secondAbsSum += (uint64_t)(value < 0 ? -(int32_t)value : value);
      }
      secondSampleCount += sampleCount;

      const size_t wrote = file_.write((const uint8_t*)samples, bytesRead);
      if (wrote != bytesRead) {
        logf("file write failed wanted=%u wrote=%u", (unsigned)bytesRead, (unsigned)wrote);
        return false;
      }
      wavDataBytes_ += wrote;
    }

    const uint32_t now = millis();
    if ((uint32_t)(now - lastProgressMs) >= 1000) {
      lastProgressMs = now;
      const unsigned long avgAbs =
          secondSampleCount > 0 ? (unsigned long)(secondAbsSum / secondSampleCount) : 0UL;
      const char* status =
          (secondSampleCount == 0) ? "nosamples"
          : (secondMin == secondMax) ? "flat"
          : (avgAbs < 8UL) ? "nearzero"
          : "active";
      logf("recording %s elapsed=%lus bytes=%lu min=%d max=%d avgabs=%lu samples=%lu status=%s",
           currentPath_,
           (unsigned long)((now - startMs) / 1000),
           (unsigned long)wavDataBytes_,
           secondSampleCount > 0 ? secondMin : 0,
           secondSampleCount > 0 ? secondMax : 0,
           avgAbs,
           (unsigned long)secondSampleCount,
           status);
      secondMin = INT16_MAX;
      secondMax = INT16_MIN;
      secondSampleCount = 0;
      secondAbsSum = 0;
    }
  }

  file_.flush();
  return true;
}

void MicrophoneRecorder::close() {
  if (!fileOpen_) {
    return;
  }

  finalizeWavHeader();
  file_.close();
  fileOpen_ = false;
  logf("closed %s bytes=%lu", currentPath_, (unsigned long)wavDataBytes_);
  currentPath_[0] = '\0';
}

void MicrophoneRecorder::logf(const char* fmt, ...) {
  char msg[200];
  va_list args;
  va_start(args, fmt);
  vsnprintf(msg, sizeof(msg), fmt, args);
  va_end(args);

  out_.printf("[%10lu][%06lu] %s\r\n",
              (unsigned long)millis(),
              (unsigned long)logSeq_++,
              msg);
}

bool MicrophoneRecorder::initPmu() {
  if (!tbeam_supreme::initPmuForPeripherals(pmu_, &out_)) {
    logf("PMU init failed");
    return false;
  }
  return true;
}

void MicrophoneRecorder::forceSpiDeselected() {
  pinMode(tbeam_supreme::sdCs(), OUTPUT);
  digitalWrite(tbeam_supreme::sdCs(), HIGH);
  pinMode(tbeam_supreme::imuCs(), OUTPUT);
  digitalWrite(tbeam_supreme::imuCs(), HIGH);
}

void MicrophoneRecorder::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
  if (!pmu_) {
    return;
  }

  forceSpiDeselected();
  SD.end();
  sdSpiH_.end();
  sdSpiF_.end();

  pmu_->disablePowerOutput(XPOWERS_BLDO1);
  delay(offMs);
  pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
  pmu_->enablePowerOutput(XPOWERS_BLDO1);
  delay(onSettleMs);
}

bool MicrophoneRecorder::mountSdCard() {
  const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};

  for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
    if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i])) {
      return true;
    }
  }
  for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
    if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i])) {
      return true;
    }
  }

  return false;
}

bool MicrophoneRecorder::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz) {
  SD.end();
  bus.end();
  delay(5);
  forceSpiDeselected();

  bus.begin(tbeam_supreme::sdSck(),
            tbeam_supreme::sdMiso(),
            tbeam_supreme::sdMosi(),
            tbeam_supreme::sdCs());
  digitalWrite(tbeam_supreme::sdCs(), HIGH);
  delay(1);
  for (uint8_t i = 0; i < 10; ++i) {
    bus.transfer(0xFF);
  }

  if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
    return false;
  }
  if (SD.cardType() == CARD_NONE) {
    SD.end();
    return false;
  }

  activeSdSpi_ = &bus;
  logf("SD mounted via %s @ %lu Hz", busName, (unsigned long)hz);
  return true;
}

bool MicrophoneRecorder::ensureDirectory(const char* path) {
  if (SD.exists(path)) {
    return true;
  }
  return SD.mkdir(path);
}

bool MicrophoneRecorder::writeWavHeader() {
  const WavHeader header = {
      .riff = {'R', 'I', 'F', 'F'},
      .chunkSize = 36,
      .wave = {'W', 'A', 'V', 'E'},
      .fmt = {'f', 'm', 't', ' '},
      .subchunk1Size = 16,
      .audioFormat = 1,
      .numChannels = kWavChannels,
      .sampleRate = MIC_SAMPLE_RATE,
      .byteRate = MIC_SAMPLE_RATE * kWavChannels * (MIC_BITS_PER_SAMPLE / 8),
      .blockAlign = (uint16_t)(kWavChannels * (MIC_BITS_PER_SAMPLE / 8)),
      .bitsPerSample = MIC_BITS_PER_SAMPLE,
      .data = {'d', 'a', 't', 'a'},
      .dataSize = 0,
  };

  return file_.write((const uint8_t*)&header, sizeof(header)) == sizeof(header);
}

bool MicrophoneRecorder::finalizeWavHeader() {
  if (!file_) {
    return false;
  }

  WavHeader header = {
      .riff = {'R', 'I', 'F', 'F'},
      .chunkSize = 36 + wavDataBytes_,
      .wave = {'W', 'A', 'V', 'E'},
      .fmt = {'f', 'm', 't', ' '},
      .subchunk1Size = 16,
      .audioFormat = 1,
      .numChannels = kWavChannels,
      .sampleRate = MIC_SAMPLE_RATE,
      .byteRate = MIC_SAMPLE_RATE * kWavChannels * (MIC_BITS_PER_SAMPLE / 8),
      .blockAlign = (uint16_t)(kWavChannels * (MIC_BITS_PER_SAMPLE / 8)),
      .bitsPerSample = MIC_BITS_PER_SAMPLE,
      .data = {'d', 'a', 't', 'a'},
      .dataSize = wavDataBytes_,
  };

  file_.seek(0);
  const bool ok = file_.write((const uint8_t*)&header, sizeof(header)) == sizeof(header);
  file_.seek(file_.size());
  return ok;
}

uint32_t MicrophoneRecorder::selectTimestamp() const {
  const time_t now = time(nullptr);
  if (now >= (time_t)kValidEpochFloor) {
    return (uint32_t)now;
  }
  if (FW_BUILD_EPOCH >= kValidEpochFloor) {
    return (uint32_t)FW_BUILD_EPOCH + (millis() / 1000UL);
  }
  return millis() / 1000UL;
}
