#include "AudioTools.h"
#include "AudioTools/AudioCodecs/CodecMP3Helix.h"
#include "BluetoothA2DPSink.h"
#include <SPI.h>
#include <SD.h>
#include <math.h>
#include <Preferences.h>
#include <U8g2lib.h>

// ---- Pin map ----
#define SD_CS    5
#define SD_MOSI  23
#define SD_MISO  19
#define SD_SCK   18

#define I2S_BCLK 13
#define I2S_LRC  14
#define I2S_DIN  12

#define BTN_MP3 32
#define BTN_BT  25
#define BTN_FM  26
#define BT_PAIR_BUTTON 33

// OLED (software SPI, any pins)
#define OLED_SCK  21
#define OLED_MOSI 22
#define OLED_CS   16
#define OLED_DC   17
#define OLED_RES  27

// Pot: must be ADC1 (ADC2 is unusable when BT/Wi-Fi is on).
// GPIO 34 is input-only and on ADC1 — perfect.
#define POT_PIN 34

#define MODE_MP3 0
#define MODE_BT  1
#define MODE_FM  2

// ---- Globals ----
I2SStream i2s;
VolumeStream volStream(i2s);
EncodedAudioStream decoder(&volStream, new MP3DecoderHelix());
File audioFile;
StreamCopy copier(decoder, audioFile, 4096);

BluetoothA2DPSink a2dp_sink(i2s);
Preferences prefs;

I2SConfig i2sCfg;

// SSD1309 128x64, software SPI. NONAME0 is the common controller variant.
// If your display shows garbled output, swap NONAME0 -> NONAME2.
U8G2_SSD1309_128X64_NONAME0_F_4W_SW_SPI u8g2(
  U8G2_R0, OLED_SCK, OLED_MOSI, OLED_CS, OLED_DC, OLED_RES);

String tracks[20];
int trackCount = 0;
int currentTrack = 0;
float volume = 0.5;
bool paused = false;
bool discoverable = false;
volatile bool connectEvent = false;
volatile bool disconnectEvent = false;
unsigned long lastBtnPress = 0;
unsigned long lastBeep = 0;
unsigned long lastModeCheck = 0;
unsigned long lastDisplay = 0;
unsigned long lastPotUpdate = 0;
int currentMode = MODE_MP3;
#define DEBOUNCE_MS 500
#define BEEP_INTERVAL 1500
#define DISPLAY_INTERVAL 200
#define POT_INTERVAL 50

// Pot smoothing
#define POT_SAMPLES 8
int potReadings[POT_SAMPLES];
int potIdx = 0;
int smoothedPot = 0;
int lastAppliedPot = -9999;

// ---- A2DP callback ----
void onConnectionStateChanged(esp_a2d_connection_state_t state, void* obj) {
  if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
    connectEvent = true;
  } else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
    disconnectEvent = true;
  }
}

// ---- SD / tracks ----
void scanTracks() {
  File root = SD.open("/");
  trackCount = 0;
  while (true) {
    File entry = root.openNextFile();
    if (!entry) break;
    String name = entry.name();
    if (name.endsWith(".mp3") && !name.startsWith(".") && trackCount < 20) {
      tracks[trackCount++] = "/" + name;
    }
    entry.close();
  }
  root.close();
  Serial.print("Found tracks: ");
  Serial.println(trackCount);
}

void playTrack(int idx) {
  if (audioFile) audioFile.close();
  if (idx < 0) idx = trackCount - 1;
  if (idx >= trackCount) idx = 0;
  currentTrack = idx;
  audioFile = SD.open(tracks[idx]);
  Serial.print("Now playing: ");
  Serial.println(tracks[idx]);
}

// ---- Tones ----
void playBeep() {
  const int sr = 44100;
  int samples = sr * 200 / 1000;
  for (int i = 0; i < samples; i++) {
    float t = (float)i / sr;
    int16_t sample = sin(2 * PI * 880 * t) * 10000;
    int16_t stereo[2] = {sample, sample};
    i2s.write((uint8_t*)stereo, 4);
  }
}

void playChime(bool ascending) {
  const int sr = 44100;
  int notes[] = {523, 784, 1047};
  int durations[] = {180, 180, 500};

  for (int n = 0; n < 3; n++) {
    int idx = ascending ? n : (2 - n);
    int samples = sr * durations[n] / 1000;
    int fadeIn = samples / 12;
    int fadeOut = samples / 2;

    for (int i = 0; i < samples; i++) {
      float t = (float)i / sr;
      float envelope = 1.0;
      if (i < fadeIn) envelope = (float)i / fadeIn;
      else if (i > samples - fadeOut) {
        float fp = (float)(samples - i) / fadeOut;
        envelope = fp * fp;
      }
      int16_t sample = sin(2 * PI * notes[idx] * t) * 8000 * envelope;
      int16_t stereo[2] = {sample, sample};
      i2s.write((uint8_t*)stereo, 4);
    }
  }
}

// ---- Mode switch ----
void switchMode(int newMode) {
  Serial.print("Switching to mode ");
  Serial.println(newMode);
  prefs.begin("radio", false);
  prefs.putInt("mode", newMode);
  prefs.end();
  delay(200);
  ESP.restart();
}

// ---- Pot ----
//
// ESP32 ADC1 is noisy and slightly non-linear near the rails. Two things
// help: (a) average several samples, (b) ignore very small changes so the
// volume doesn't flicker by 1% when the knob is still.
//
// We also clip the usable range to 100..4000 instead of 0..4095 so the
// physical end stops of the knob actually correspond to 0% and 100%.
void updatePot() {
  potReadings[potIdx] = analogRead(POT_PIN);
  potIdx = (potIdx + 1) % POT_SAMPLES;

  long sum = 0;
  for (int i = 0; i < POT_SAMPLES; i++) sum += potReadings[i];
  smoothedPot = sum / POT_SAMPLES;

  // Deadband: don't update unless the knob has moved noticeably.
  if (abs(smoothedPot - lastAppliedPot) < 40) return;
  lastAppliedPot = smoothedPot;

  int clipped = constrain(smoothedPot, 100, 4000);

  if (currentMode == MODE_MP3) {
    volume = (clipped - 100) / 3900.0f;
    volStream.setVolume(volume);
  } else if (currentMode == MODE_BT) {
    uint8_t btVol = map(clipped, 100, 4000, 0, 127);
    a2dp_sink.set_volume(btVol);
  }
}

int potPercent() {
  int clipped = constrain(smoothedPot, 100, 4000);
  return map(clipped, 100, 4000, 0, 100);
}

// ---- OLED ----
void updateDisplay() {
  u8g2.clearBuffer();

  // Header — mode name
  u8g2.setFont(u8g2_font_helvB10_tr);
  const char* title = "?";
  if (currentMode == MODE_MP3) title = "MP3 PLAYER";
  else if (currentMode == MODE_BT) title = "BLUETOOTH";
  else if (currentMode == MODE_FM) title = "FM RADIO";
  u8g2.drawStr(0, 11, title);
  u8g2.drawHLine(0, 14, 128);

  // Status lines (body font)
  u8g2.setFont(u8g2_font_6x10_tr);

  if (currentMode == MODE_MP3) {
    if (trackCount > 0) {
      String name = tracks[currentTrack];
      if (name.startsWith("/")) name = name.substring(1);
      if (name.length() > 21) name = name.substring(0, 21);
      u8g2.drawStr(0, 27, name.c_str());
      char info[24];
      snprintf(info, sizeof(info), "Track %d of %d%s",
               currentTrack + 1, trackCount, paused ? " (paused)" : "");
      u8g2.drawStr(0, 39, info);
    } else {
      u8g2.drawStr(0, 27, "No tracks found");
    }
  } else if (currentMode == MODE_BT) {
    if (a2dp_sink.is_connected()) {
      u8g2.drawStr(0, 27, "Connected");
    } else if (discoverable) {
      u8g2.drawStr(0, 27, "Pairing mode ON");
      u8g2.drawStr(0, 39, "Discoverable");
    } else {
      u8g2.drawStr(0, 27, "Ready");
      u8g2.drawStr(0, 39, "Press btn to pair");
    }
  } else if (currentMode == MODE_FM) {
    u8g2.drawStr(0, 27, "Not yet wired");
  }

  // Volume bar
  int pct = potPercent();
  char vlabel[12];
  snprintf(vlabel, sizeof(vlabel), "Vol %d%%", pct);
  u8g2.drawStr(0, 60, vlabel);
  u8g2.drawFrame(48, 52, 80, 10);
  u8g2.drawBox(50, 54, (76 * pct) / 100, 6);

  u8g2.sendBuffer();
}

// ---- Mode setups ----
void setupMP3() {
  Serial.println("MP3 MODE");
  decoder.begin();
  if (trackCount > 0) playTrack(0);
}

void setupBluetooth() {
  Serial.println("BLUETOOTH MODE");
  a2dp_sink.set_on_connection_state_changed(onConnectionStateChanged);
  a2dp_sink.set_discoverability(ESP_BT_NON_DISCOVERABLE);
  a2dp_sink.start("Retro Radio");

  // Apply the knob's current position as initial BT volume.
  int clipped = constrain(smoothedPot, 100, 4000);
  a2dp_sink.set_volume(map(clipped, 100, 4000, 0, 127));
}

void setupFM() {
  Serial.println("FM MODE (not yet wired)");
}

// ---- Mode loops ----
void loopMP3() {
  if (!paused) copier.copy();
  if (audioFile && !audioFile.available() && !paused) {
    playTrack(currentTrack + 1);
  }

  if (Serial.available()) {
    char cmd = Serial.read();
    if (cmd == 'n') playTrack(currentTrack + 1);
    else if (cmd == 'p') playTrack(currentTrack - 1);
    else if (cmd == 's') paused = !paused;
  }
}

void loopBluetooth() {
  unsigned long now = millis();
  bool nowConnected = a2dp_sink.is_connected();

  if (connectEvent) {
    connectEvent = false;
    Serial.println("BT CONNECTED");
    discoverable = false;
    a2dp_sink.set_discoverability(ESP_BT_NON_DISCOVERABLE);
    delay(200);
    playChime(true);
  }

  if (disconnectEvent) {
    disconnectEvent = false;
    Serial.println("BT DISCONNECTED");
    // A2DP tore I2S down; reinstall the driver so the chime can play.
    delay(300);
    i2s.begin(i2sCfg);
    playChime(false);
  }

  if (!digitalRead(BT_PAIR_BUTTON) && now - lastBtnPress > DEBOUNCE_MS) {
    lastBtnPress = now;
    discoverable = !discoverable;
    if (discoverable) {
      a2dp_sink.set_discoverability(ESP_BT_GENERAL_DISCOVERABLE);
      Serial.println("Pairing mode ON");
    } else {
      a2dp_sink.set_discoverability(ESP_BT_NON_DISCOVERABLE);
      Serial.println("Pairing mode OFF");
    }
  }

  if (discoverable && !nowConnected && now - lastBeep > BEEP_INTERVAL) {
    lastBeep = now;
    // Same fix as the disconnect chime: if a connection attempt failed,
    // I2S may have been left at a different sample rate. Reinstall it at
    // 44.1 kHz so the beep is always the same pitch.
    i2s.begin(i2sCfg);
    playBeep();
  }
}

void loopFM() {
  // Placeholder
}

// ---- Setup / loop ----
void setup() {
  Serial.begin(115200);
  delay(2000);
  AudioLogger::instance().begin(Serial, AudioLogger::Warning);

  pinMode(BTN_MP3, INPUT_PULLUP);
  pinMode(BTN_BT, INPUT_PULLUP);
  pinMode(BTN_FM, INPUT_PULLUP);
  pinMode(BT_PAIR_BUTTON, INPUT_PULLUP);

  // ADC1 setup for the pot. analogReadResolution defaults to 12 bits on
  // ESP32, so analogRead() returns 0..4095. No setup needed beyond that.
  // The Arduino core uses 11 dB attenuation by default → 0..3.3V range.

  prefs.begin("radio", true);
  currentMode = prefs.getInt("mode", MODE_MP3);
  prefs.end();

  SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
  if (!SD.begin(SD_CS)) {
    Serial.println("SD failed");
  } else {
    scanTracks();
  }

  // I2S
  i2sCfg = i2s.defaultConfig(TX_MODE);
  i2sCfg.pin_bck  = I2S_BCLK;
  i2sCfg.pin_ws   = I2S_LRC;
  i2sCfg.pin_data = I2S_DIN;
  i2sCfg.buffer_size = 1024;
  i2sCfg.buffer_count = 8;
  i2sCfg.sample_rate = 44100;
  i2sCfg.channels = 2;
  i2sCfg.bits_per_sample = 16;
  i2s.begin(i2sCfg);

  auto vcfg = volStream.defaultConfig();
  vcfg.copyFrom(i2sCfg);
  volStream.begin(vcfg);

  // OLED
  u8g2.begin();
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_helvB10_tr);
  u8g2.drawStr(20, 35, "Retro Radio");
  u8g2.sendBuffer();

  // Prime pot averaging buffer with the current reading so the initial
  // volume matches the physical knob position from the first frame.
  int initial = analogRead(POT_PIN);
  for (int i = 0; i < POT_SAMPLES; i++) potReadings[i] = initial;
  smoothedPot = initial;
  int clipped = constrain(smoothedPot, 100, 4000);
  volume = (clipped - 100) / 3900.0f;
  volStream.setVolume(volume);

  if (currentMode == MODE_MP3) setupMP3();
  else if (currentMode == MODE_BT) setupBluetooth();
  else if (currentMode == MODE_FM) setupFM();
}

void loop() {
  unsigned long now = millis();

  // Mode buttons
  if (now - lastModeCheck > 200) {
    lastModeCheck = now;
    if (!digitalRead(BTN_MP3) && currentMode != MODE_MP3) switchMode(MODE_MP3);
    if (!digitalRead(BTN_BT)  && currentMode != MODE_BT)  switchMode(MODE_BT);
    if (!digitalRead(BTN_FM)  && currentMode != MODE_FM)  switchMode(MODE_FM);
  }

  // Pot — sample frequently for smoothing, apply with deadband
  if (now - lastPotUpdate > POT_INTERVAL) {
    lastPotUpdate = now;
    updatePot();
  }

  // OLED — refresh ~5 Hz
  if (now - lastDisplay > DISPLAY_INTERVAL) {
    lastDisplay = now;
    updateDisplay();
  }

  if (currentMode == MODE_MP3) loopMP3();
  else if (currentMode == MODE_BT) loopBluetooth();
  else if (currentMode == MODE_FM) loopFM();
}
