#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>

// ── SD card (VSPI) ───────────────────────────────────────────────────
#define SD_CS    5
#define SD_MOSI  23
#define SD_MISO  19
#define SD_SCK   18

// ── I2S amp ──────────────────────────────────────────────────────────
#define I2S_BCLK 13
#define I2S_LRC  14
#define I2S_DIN  12

// ── Buttons ──────────────────────────────────────────────────────────
#define BTN_MP3 32
#define BTN_BT  25
#define BTN_FM  26
#define BT_PAIR_BUTTON 33

// ── OLED SSD1309 ─────────────────────────────────────────────────────
#define OLED_CS  17
#define OLED_DC  16
#define OLED_RES 4

// ── Volume pot ───────────────────────────────────────────────────────
#define POT_PIN 34

// ── Modes ────────────────────────────────────────────────────────────
#define MODE_MP3 0
#define MODE_BT  1
#define MODE_FM  2

// ── Audio plumbing ───────────────────────────────────────────────────
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;

// ── OLED ─────────────────────────────────────────────────────────────
U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI display(
  U8G2_R0, /*cs=*/OLED_CS, /*dc=*/OLED_DC, /*reset=*/OLED_RES
);

// ── State ────────────────────────────────────────────────────────────
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 lastPotRead = 0;
unsigned long lastDisplayRefresh = 0;
int currentMode = MODE_MP3;

int potSmoothed = 0;
int lastPotApplied = -9999;

// ── Track metadata (shared between MP3 and BT modes) ─────────────────
String   currentTitle      = "";
String   currentArtist     = "";
uint32_t currentBitrate    = 128000;
uint32_t currentDurationMs = 0;
uint32_t currentAudioStart = 0;
uint32_t currentFileSize   = 0;

// ── BT playback position tracking ────────────────────────────────────
// The phone reports position over AVRCP periodically (often once per
// second). Between reports we interpolate using millis() so the bar
// moves smoothly. If no reports ever arrive we just show 0 / duration.
volatile uint32_t btReportedPosMs = 0;
volatile uint32_t btReportTimeMs  = 0;
volatile bool     btPosKnown      = false;
volatile bool     btIsPlaying     = false;   // updated by play-status callback

#define DEBOUNCE_MS 500
#define BEEP_INTERVAL 1500
#define POT_READ_INTERVAL_MS 50
#define DISPLAY_REFRESH_MS 250
#define POT_DEADBAND 40

// ─────────────────────────────────────────────────────────────────────
// A2DP callbacks
// ─────────────────────────────────────────────────────────────────────

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;
}

void onAvrcMetadata(uint8_t id, const uint8_t* data) {
  String s = (const char*)data;
  switch (id) {
    case ESP_AVRC_MD_ATTR_TITLE:
      currentTitle = s;
      Serial.print("BT title:  "); Serial.println(s);
      break;
    case ESP_AVRC_MD_ATTR_ARTIST:
      currentArtist = s;
      Serial.print("BT artist: "); Serial.println(s);
      break;
    case ESP_AVRC_MD_ATTR_PLAYING_TIME:
      currentDurationMs = (uint32_t)s.toInt();
      Serial.print("BT duration ms: "); Serial.println(currentDurationMs);
      break;
    default:
      break;
  }
}

// Position update from phone. Library invokes this at the interval we
// register (we ask for 1 second). The argument is position in
// milliseconds from the start of the current track.
void onAvrcPlayPos(uint32_t playPos) {
  btReportedPosMs = playPos;
  btReportTimeMs  = millis();
  btPosKnown = true;
}

// Play / pause / stop state from the phone. We use it so the progress
// bar stops advancing when playback is paused.
void onAvrcPlayStatus(esp_avrc_playback_stat_t status) {
  btIsPlaying = (status == ESP_AVRC_PLAYBACK_PLAYING);
  // When the phone reports a new play state it usually re-reports
  // position too, but request a fresh sample anyway.
  if (btPosKnown) btReportTimeMs = millis();
}

// Current BT position with interpolation between reports.
uint32_t btCurrentPositionMs() {
  if (!btPosKnown) return 0;
  if (!btIsPlaying) return btReportedPosMs;
  uint32_t elapsed = millis() - btReportTimeMs;
  uint32_t pos = btReportedPosMs + elapsed;
  if (currentDurationMs > 0 && pos > currentDurationMs) pos = currentDurationMs;
  return pos;
}

// ─────────────────────────────────────────────────────────────────────
// ID3v2 + MP3-frame parsing (MP3 mode)
// ─────────────────────────────────────────────────────────────────────

static String cleanText(const uint8_t* data, uint32_t len) {
  String s = "";
  for (uint32_t i = 0; i < len; i++) {
    if (data[i] >= 32 && data[i] < 127) s += (char)data[i];
  }
  while (s.length() > 0 && s.charAt(s.length() - 1) == ' ') {
    s.remove(s.length() - 1);
  }
  return s;
}

static String decodeTextFrame(const uint8_t* data, uint32_t len) {
  if (len < 2) return "";
  uint8_t enc = data[0];
  if (enc == 0 || enc == 3) {
    return cleanText(data + 1, len - 1);
  } else {
    uint32_t start = 1;
    if (enc == 1 && len >= 3) start = 3;
    String s = "";
    for (uint32_t i = start; i + 1 < len; i += 2) {
      uint8_t b = data[i];
      if (b >= 32 && b < 127) s += (char)b;
    }
    while (s.length() > 0 && s.charAt(s.length() - 1) == ' ') {
      s.remove(s.length() - 1);
    }
    return s;
  }
}

static uint32_t readFirstMP3Bitrate(File &f, uint32_t searchFrom) {
  static const int br_mpeg1_l3[16] = {
    0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0
  };
  static const int br_mpeg2_l3[16] = {
    0,  8, 16, 24, 32, 40, 48, 56,  64,  80,  96, 112, 128, 144, 160, 0
  };

  if (!f.seek(searchFrom)) return 0;

  int scanRemaining = 4096;
  uint8_t prev = 0, cur = 0;
  bool havePrev = false;
  while (scanRemaining-- > 0 && f.available() > 0) {
    int b = f.read();
    if (b < 0) break;
    cur = (uint8_t)b;
    if (havePrev && prev == 0xFF && (cur & 0xE0) == 0xE0) {
      uint8_t b2 = 0, b3 = 0;
      if (f.read(&b2, 1) != 1) return 0;
      if (f.read(&b3, 1) != 1) return 0;

      uint8_t versionBits = (cur >> 3) & 0x03;
      uint8_t layerBits   = (cur >> 1) & 0x03;
      uint8_t bitrateIdx  = (b2 >> 4) & 0x0F;

      if (layerBits == 0x01 && bitrateIdx > 0 && bitrateIdx < 15) {
        int kbps = (versionBits == 0x03)
                   ? br_mpeg1_l3[bitrateIdx]
                   : br_mpeg2_l3[bitrateIdx];
        if (kbps > 0) return (uint32_t)kbps * 1000;
      }
    }
    prev = cur;
    havePrev = true;
  }
  return 0;
}

void parseTrackMetadata(const String& path) {
  currentTitle = "";
  currentArtist = "";
  currentBitrate = 128000;
  currentDurationMs = 0;
  currentAudioStart = 0;
  currentFileSize = 0;

  File f = SD.open(path);
  if (!f) return;
  currentFileSize = f.size();

  uint8_t hdr[10];
  if (f.read(hdr, 10) == 10 &&
      hdr[0] == 'I' && hdr[1] == 'D' && hdr[2] == '3') {
    uint8_t majorVer = hdr[3];
    uint32_t tagSize = ((uint32_t)hdr[6] << 21) |
                      ((uint32_t)hdr[7] << 14) |
                      ((uint32_t)hdr[8] <<  7) |
                       (uint32_t)hdr[9];

    uint32_t pos = 10;
    uint32_t endPos = 10 + tagSize;

    while (pos + 10 < endPos) {
      f.seek(pos);
      uint8_t fh[10];
      if (f.read(fh, 10) != 10) break;
      if (fh[0] == 0) break;

      char id[5] = { (char)fh[0], (char)fh[1], (char)fh[2], (char)fh[3], 0 };
      uint32_t frameSize;
      if (majorVer >= 4) {
        frameSize = ((uint32_t)fh[4] << 21) |
                    ((uint32_t)fh[5] << 14) |
                    ((uint32_t)fh[6] <<  7) |
                     (uint32_t)fh[7];
      } else {
        frameSize = ((uint32_t)fh[4] << 24) |
                    ((uint32_t)fh[5] << 16) |
                    ((uint32_t)fh[6] <<  8) |
                     (uint32_t)fh[7];
      }
      if (frameSize == 0 || frameSize > 4096) break;

      bool wantThis = (strcmp(id, "TIT2") == 0 ||
                       strcmp(id, "TPE1") == 0 ||
                       strcmp(id, "TLEN") == 0);
      if (wantThis) {
        uint8_t buf[256];
        uint32_t toRead = frameSize > sizeof(buf) ? sizeof(buf) : frameSize;
        if (f.read(buf, toRead) == (int)toRead) {
          String text = decodeTextFrame(buf, toRead);
          if      (strcmp(id, "TIT2") == 0) currentTitle = text;
          else if (strcmp(id, "TPE1") == 0) currentArtist = text;
          else if (strcmp(id, "TLEN") == 0) currentDurationMs = (uint32_t)text.toInt();
        }
      }
      pos += 10 + frameSize;
    }
    currentAudioStart = 10 + tagSize;
  }

  uint32_t br = readFirstMP3Bitrate(f, currentAudioStart);
  if (br > 0) currentBitrate = br;

  if (currentDurationMs == 0 && currentBitrate > 0 && currentFileSize > currentAudioStart) {
    uint64_t audioBytes = currentFileSize - currentAudioStart;
    currentDurationMs = (uint32_t)((audioBytes * 8000ULL) / currentBitrate);
  }

  f.close();
}

uint32_t mp3CurrentPositionMs() {
  if (!audioFile || currentBitrate == 0) return 0;
  uint32_t filePos = audioFile.position();
  if (filePos <= currentAudioStart) return 0;
  uint32_t audioBytes = filePos - currentAudioStart;
  return (uint32_t)(((uint64_t)audioBytes * 8000ULL) / currentBitrate);
}

// ─────────────────────────────────────────────────────────────────────
// SD / playback
// ─────────────────────────────────────────────────────────────────────

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;

  parseTrackMetadata(tracks[idx]);

  audioFile = SD.open(tracks[idx]);
  if (audioFile && currentAudioStart > 0) {
    audioFile.seek(currentAudioStart);
  }

  Serial.print("Now playing: "); Serial.println(tracks[idx]);
  Serial.print("  Title:    "); Serial.println(currentTitle);
  Serial.print("  Artist:   "); Serial.println(currentArtist);
  Serial.print("  Bitrate:  "); Serial.println(currentBitrate);
  Serial.print("  Duration: "); Serial.println(currentDurationMs);
}

// ─────────────────────────────────────────────────────────────────────
// 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 switching
// ─────────────────────────────────────────────────────────────────────

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();
}

// ─────────────────────────────────────────────────────────────────────
// Volume
// ─────────────────────────────────────────────────────────────────────

void applyVolume(float v) {
  if (v < 0.0f) v = 0.0f;
  if (v > 1.0f) v = 1.0f;
  volume = v;
  volStream.setVolume(volume);
  if (currentMode == MODE_BT) {
    a2dp_sink.set_volume((uint8_t)(volume * 127.0f));
  }
}

void readPot() {
  int raw = analogRead(POT_PIN);
  potSmoothed = (potSmoothed * 7 + raw) / 8;
  if (abs(potSmoothed - lastPotApplied) > POT_DEADBAND) {
    lastPotApplied = potSmoothed;
    applyVolume(potSmoothed / 4095.0f);
  }
}

// ─────────────────────────────────────────────────────────────────────
// OLED
// ─────────────────────────────────────────────────────────────────────

String prettyTrackName(const String& path) {
  String s = path;
  if (s.startsWith("/")) s = s.substring(1);
  if (s.endsWith(".mp3") || s.endsWith(".MP3")) s = s.substring(0, s.length() - 4);
  return s;
}

String fitToWidth(const String& s, int maxWidth) {
  if (display.getStrWidth(s.c_str()) <= maxWidth) return s;
  String t = s;
  while (t.length() > 0 &&
         display.getStrWidth((t + "...").c_str()) > maxWidth) {
    t.remove(t.length() - 1);
  }
  return t + "...";
}

void formatTime(uint32_t ms, char* out, size_t outSize) {
  uint32_t totalSec = ms / 1000;
  uint32_t mins = totalSec / 60;
  uint32_t secs = totalSec % 60;
  if (mins > 99) mins = 99;
  snprintf(out, outSize, "%lu:%02lu",
           (unsigned long)mins, (unsigned long)secs);
}

void drawTitleArtist(int titleBaseline, int artistBaseline,
                     const String& title, const String& artist) {
  display.setFont(u8g2_font_helvB10_tr);
  display.drawStr(0, titleBaseline, fitToWidth(title, 128).c_str());
  if (artist.length() > 0) {
    display.setFont(u8g2_font_helvR08_tr);
    display.drawStr(0, artistBaseline, fitToWidth(artist, 128).c_str());
  }
}

// Time text on either side, progress bar in the middle.
// `posMs` may be 0 if we don't know it yet; `durMs` similarly.
// If durMs is 0 we hide the right-hand time and the bar.
void drawProgressRow(uint32_t posMs, uint32_t durMs) {
  display.setFont(u8g2_font_helvR08_tr);
  char tposBuf[8], tdurBuf[8];
  formatTime(posMs, tposBuf, sizeof(tposBuf));
  int wPos = display.getStrWidth(tposBuf);
  display.drawStr(0, 47, tposBuf);

  if (durMs == 0) return;  // no duration → no bar, no end time

  formatTime(durMs, tdurBuf, sizeof(tdurBuf));
  int wDur = display.getStrWidth(tdurBuf);
  display.drawStr(128 - wDur, 47, tdurBuf);

  int barX = wPos + 4;
  int barW = 128 - wPos - wDur - 8;
  int barY = 42;
  if (barW > 10) {
    display.drawFrame(barX, barY, barW, 5);
    int fill = (int)((float)posMs / (float)durMs * (barW - 2));
    if (fill < 0) fill = 0;
    if (fill > barW - 2) fill = barW - 2;
    if (fill > 0) display.drawBox(barX + 1, barY + 1, fill, 3);
  }
}

void drawDisplay() {
  display.clearBuffer();

  // Header row
  display.setFont(u8g2_font_helvR08_tr);
  const char* modeName =
    (currentMode == MODE_MP3) ? "MP3" :
    (currentMode == MODE_BT)  ? "BLUETOOTH" : "FM";
  display.drawStr(0, 8, modeName);

  const char* topRight = nullptr;
  if (currentMode == MODE_MP3 && paused) topRight = "PAUSED";
  else if (currentMode == MODE_BT && discoverable && !a2dp_sink.is_connected()) topRight = "PAIRING";
  if (topRight) {
    int w = display.getStrWidth(topRight);
    display.drawStr(128 - w, 8, topRight);
  }

  display.drawHLine(0, 11, 128);

  if (currentMode == MODE_MP3) {
    if (trackCount == 0) {
      display.setFont(u8g2_font_helvR08_tr);
      display.drawStr(0, 30, "No tracks on SD");
    } else {
      String t = currentTitle.length() > 0
                   ? currentTitle
                   : prettyTrackName(tracks[currentTrack]);
      drawTitleArtist(24, 35, t, currentArtist);

      uint32_t posMs = mp3CurrentPositionMs();
      if (currentDurationMs > 0 && posMs > currentDurationMs) {
        posMs = currentDurationMs;
      }
      drawProgressRow(posMs, currentDurationMs);
    }
  } else if (currentMode == MODE_BT) {
    bool connected = a2dp_sink.is_connected();

    if (connected && currentTitle.length() > 0) {
      drawTitleArtist(24, 35, currentTitle, currentArtist);
      // Progress row — if the phone never reports position the bar stays
      // at 0:00; if it never reports duration we hide the bar entirely.
      drawProgressRow(btCurrentPositionMs(), currentDurationMs);
    } else if (connected) {
      display.setFont(u8g2_font_helvB10_tr);
      display.drawStr(0, 28, "Connected");
      display.setFont(u8g2_font_helvR08_tr);
      display.drawStr(0, 42, "Press play on your phone");
    } else if (discoverable) {
      display.setFont(u8g2_font_helvB10_tr);
      display.drawStr(0, 28, "Pairing…");
      display.setFont(u8g2_font_helvR08_tr);
      display.drawStr(0, 42, "Look for: Retro Radio");
    } else {
      display.setFont(u8g2_font_helvB10_tr);
      display.drawStr(0, 28, "Ready");
      display.setFont(u8g2_font_helvR08_tr);
      display.drawStr(0, 42, "Press pair to discover");
    }
  } else {
    display.setFont(u8g2_font_helvR08_tr);
    display.drawStr(0, 28, "Coming soon");
  }

  // Volume bar
  display.setFont(u8g2_font_helvR08_tr);
  display.drawStr(0, 63, "Vol");
  int volX = 22, volY = 56, volW = 106, volH = 7;
  display.drawFrame(volX, volY, volW, volH);
  int volFill = (int)(volume * (volW - 2));
  if (volFill > 0) display.drawBox(volX + 1, volY + 1, volFill, volH - 2);

  display.sendBuffer();
}

// ─────────────────────────────────────────────────────────────────────
// Mode setup / loops
// ─────────────────────────────────────────────────────────────────────

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_avrc_metadata_callback(onAvrcMetadata);
  // Position notifications every 1 second. Some phones honor this
  // closely; others send less often. Either way we interpolate.
  a2dp_sink.set_avrc_rn_play_pos_callback(onAvrcPlayPos, 1);
  // Play / pause / stop state changes — used to freeze the bar on pause.
  a2dp_sink.set_avrc_rn_playstatus_callback(onAvrcPlayStatus);
  a2dp_sink.set_discoverability(ESP_BT_NON_DISCOVERABLE);
  a2dp_sink.start("Retro Radio");
  a2dp_sink.set_volume((uint8_t)(volume * 127.0f));
}

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

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;
    else if (cmd == '+') applyVolume(volume + 0.1f);
    else if (cmd == '-') applyVolume(volume - 0.1f);
  }
}

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);
    currentTitle = "";
    currentArtist = "";
    currentDurationMs = 0;
    btPosKnown = false;
    btReportedPosMs = 0;
    btIsPlaying = false;
    delay(200);
    playChime(true);
  }

  if (disconnectEvent) {
    disconnectEvent = false;
    Serial.println("BT DISCONNECTED");
    currentTitle = "";
    currentArtist = "";
    currentDurationMs = 0;
    btPosKnown = false;
    btReportedPosMs = 0;
    btIsPlaying = false;
    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;
    i2s.begin(i2sCfg);
    playBeep();
  }
}

void loopFM() {
  // Placeholder
}

// ─────────────────────────────────────────────────────────────────────
// Arduino entry points
// ─────────────────────────────────────────────────────────────────────

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);

  analogReadResolution(12);

  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();
  }

  display.begin();
  display.setContrast(255);
  display.clearBuffer();
  display.setFont(u8g2_font_helvB10_tr);
  display.drawStr(4, 30, "Retro Radio");
  display.drawStr(4, 48, "Booting…");
  display.sendBuffer();

  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);

  potSmoothed = analogRead(POT_PIN);
  applyVolume(potSmoothed / 4095.0f);
  lastPotApplied = potSmoothed;

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

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

  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);
  }

  if (now - lastPotRead > POT_READ_INTERVAL_MS) {
    lastPotRead = now;
    readPot();
  }

  if (now - lastDisplayRefresh > DISPLAY_REFRESH_MS) {
    lastDisplayRefresh = now;
    drawDisplay();
  }

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