/*
ESP32 Frequency Modulation Broadcaster

Some code is from my old game project but now is almost dead.

Wei - 2026

You can do anything you want to this source code. BUT you MUST cite the original source of the code.
*/
#include <SPI.h>
#include <SD.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "KT0803.h"
#include <ESP_I2S.h>
#include "Audio.h"

#define VERSION "1.2"

KT0803L fm(&Wire);
Audio audio;

// ======================= Display ======================= 
#define SCREEN_W 128
#define SCREEN_H 64

#define OLED_MOSI 23 // SDA
#define OLED_CLK  18 // SCK
#define OLED_DC   27
#define OLED_CS   5
#define OLED_RST  14 // RES

U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(
  U8G2_R0,
  OLED_CS,
  OLED_DC,
  OLED_RST
);

// ======================= Joystick Pins =======================
#define JOY_X  34
#define JOY_Y  35
#define JOY_SW 32

#define FM_SDA 21
#define FM_SCL 22

// ======================= SD (microSD) Card =======================
#define SD_MOSI   23 // sd reader share spi pin with the display
#define SD_MISO   19
#define SD_SCK    18
#define SD_CS     33

// ======================= PCM5102A DAC =======================
#define I2S_BCLK  26
#define I2S_LRCK  25
#define I2S_DOUT  17

constexpr uint32_t SAMPLE_RATE = 44100;
constexpr float TONE_FREQ = 440.0f;

I2SClass I2S;

typedef struct CursorPos {
  int x;
  int y;
} CursorPos;

void printScreen(int x, int y, const char* text, bool clear_screen, bool send_buffer) {
  if (clear_screen) {
    u8g2.clearBuffer();
  }

  u8g2.setCursor(x, y);
  u8g2.print(text);

  if (send_buffer) {
    u8g2.sendBuffer();
  }
}

struct JoyStickStatus;

// Change its value if your joystick's position is not same as you expected.
bool swapJoyStickVertical = false;
bool swapJoyStickHorizontal = false;

JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw);

struct JoyStickStatus {
  bool up;
  bool down;
  bool left;
  bool right;
  bool pressed;
};

int readJoystickAxis(int pin) {
    constexpr int SAMPLE_COUNT = 4;

    int sum = 0;

    for (int i = 0; i < SAMPLE_COUNT; i++) {
        sum += analogRead(pin);
    }

    return sum / SAMPLE_COUNT;
}

JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw) {
  JoyStickStatus status = {0};

  constexpr int CENTER_LOW  = 1500;
  constexpr int CENTER_HIGH = 2600;

  int x = readJoystickAxis(pin_x);
  int y = readJoystickAxis(pin_y);
  bool pressed = (digitalRead(pin_sw) == LOW);

  status.up = x >= CENTER_LOW && x <= CENTER_HIGH && y < 500;
  status.down = x >= CENTER_LOW && x <= CENTER_HIGH && y > 3800;
  status.left = y >= CENTER_LOW && y <= CENTER_HIGH && x < 500;
  status.right = y >= CENTER_LOW && y <= CENTER_HIGH && x > 3800;

  if (swapJoyStickVertical) {
      // Wow, c++ have this cool thing
      std::swap(status.up, status.down);
  }

  if (swapJoyStickHorizontal) {
      std::swap(status.left, status.right);
  }

  status.pressed = pressed;
  return status;
}

void getJoyStickerDebugInfo(int start_x, int start_y, int pin_x, int pin_y, int pin_sw, bool print_in_serial) {
  int x = readJoystickAxis(pin_x);
  int y = readJoystickAxis(pin_y);
  bool pressed = (digitalRead(pin_sw) == LOW);

  u8g2.setCursor(start_x, start_y);
  u8g2.print("X: ");
  u8g2.print(x);

  u8g2.setCursor(start_x, start_y + 12);
  u8g2.print("Y: ");
  u8g2.print(y);

  u8g2.setCursor(start_x, start_y + 24);
  u8g2.print("SW: ");
  u8g2.print(pressed ? "PRESSED" : "RELEASED");

  JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW);

  u8g2.setCursor(start_x, start_y + 36);
  if (status.up) {
    u8g2.print("Direction: UP");
  } 
  else if (status.down) {
    u8g2.print("Direction: DOWN");
  }
  else if (status.left) {
    u8g2.print("Direction: LEFT");
  }
  else if (status.right) {
    u8g2.print("Direction: RIGHT");
  }

  if (print_in_serial) {
    char buffer[100];
    snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED");
    Serial.println(buffer);
  }
}

// Audio
int dacVolume = 12;

// Option & Menu
int selectOptionID = 1;
int currentOptionPage = 0; // 0: Home, 1: JoyStick Debug, 2: FM Broadcast, 3: PCM5102 Debug
const int optionIDList[] = {1,2,3,4,5};
const char* menuNames[] = {
  "Debug Joystick",
  "FM Brocaster",
  "Debug PCM5102",
  "Volume",
  "Play Music"
};
int buttonPressCount = 0;
bool wasPressed = false;
unsigned long buttonPressTime = 0;

float roundToOneDecimal(float num) {
    return (int)(num * 10 + 0.5) / 10.0;
}

void hanleQuitMenuEvent() {
  JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW);

  if (joyStatus.pressed && !wasPressed) {
    // Start time record
    buttonPressTime = millis();
    wasPressed = true;
  }

  if (!joyStatus.pressed && wasPressed) {
    // Quit menu if pressed time more than over 1.5 sec
    unsigned long duration = millis() - buttonPressTime;

    if (duration >= 1000) {
      currentOptionPage=0;
    }

    wasPressed = false;
  }
}

void setFM(float freq, bool mute) {
  fm.setUSA();
  fm.setFrequency(freq);
  fm.setMute(mute);
}

void fmBroadcast(void) {
  float fm_freq = 98.1;
  float old_freq = 98.1;

  if (!fm.begin(fm_freq, false)) {
    Serial.println("FM Module KT0803 not found. Exiting...");
    printScreen(0, 14, "FM Module KT0803 not", true, false);
    u8g2.setCursor(0, 26);
    u8g2.print(" found. Exiting...");
    u8g2.sendBuffer();
    delay(3500);
    currentOptionPage=0;
    return;
  }

  setFM(fm_freq, false);

  while (currentOptionPage == 2) {
    int adc = analogRead(JOY_X);
    int offset = adc - 2048;

    if (abs(offset) > 200)
    {
        float speed = offset / 2048.0f;

        fm_freq += speed * 0.1f;
        fm_freq = roundToOneDecimal(fm_freq);

        // Max and min value check
        if (fm_freq < 88.0) {
          fm_freq = 88.0;
        }
        else if (fm_freq > 108.0) {
          fm_freq = 108.0;
        }
    }

    if (old_freq != fm_freq) {
      // Update frequency
      setFM(fm_freq, false);
      old_freq = fm_freq;
      Serial.print("Frequency: ");
      Serial.println(fm_freq);
    }

    printScreen(0, 14, "Brocasting_", true, false);
    u8g2.setCursor(0, 26);
    u8g2.print("Frequency: ");
    u8g2.print(fm.getFrequency());
    u8g2.setCursor(0, 38);
    u8g2.print("Now you can leave");
    u8g2.setCursor(0, 50);
    u8g2.print("this page.");
    u8g2.sendBuffer();
    delay(300);
    printScreen(0, 14, "Brocasting ", true, false);
    u8g2.setCursor(0, 26);
    u8g2.print("Frequency: ");
    u8g2.print(fm.getFrequency());
    u8g2.setCursor(0, 38);
    u8g2.print("Now you can leave");
    u8g2.setCursor(0, 50);
    u8g2.print("this page.");
    u8g2.sendBuffer();
    delay(300);
    hanleQuitMenuEvent();
  }
}

void joyStickDebugMenu(void) {
  while (currentOptionPage == 1) {
    printScreen(0, 14, "Debugging joystick.", true, false);
    getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true);
    u8g2.sendBuffer();
    delay(300);
    printScreen(0, 14, "Debugging joystick..", true, false);
    getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true);
    u8g2.sendBuffer();
    delay(300);
    printScreen(0, 14, "Debugging joystick...", true, false);
    getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true);
    u8g2.sendBuffer();
    delay(300);
    printScreen(0, 14, "Debugging joystick....", true, false);
    getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true);
    u8g2.sendBuffer();
    delay(300);
    hanleQuitMenuEvent();
  }
}

CursorPos printOptionMenu(int lastX, int lastY, int availableHeight, int textSize) {
  CursorPos pos = {lastX, lastY};

  int optionCount = sizeof(menuNames) / sizeof(menuNames[0]);
  int maxDisplayOptionCount = availableHeight / textSize;
  int usedHeight = 0;

  // Below code is for scrolling menu support
  int startIndex = selectOptionID - maxDisplayOptionCount / 2;

  // border check
  if (startIndex < 0) {
    startIndex = 0;
  }

  if (startIndex + maxDisplayOptionCount > optionCount) {
    startIndex = optionCount - maxDisplayOptionCount;
  }

  if (startIndex < 0) {
    startIndex = 0;
  }

  int endIndex = min(startIndex + maxDisplayOptionCount, optionCount);

  for (int i = startIndex; i < endIndex; i++) {
    pos.y += 12;
    u8g2.setCursor(pos.x, pos.y);

    // Content
    u8g2.print(i+1);
    u8g2.print(": ");
    u8g2.print(menuNames[i]);

    if (selectOptionID == i + 1) {
      u8g2.print(" [X]");
    } else {
      u8g2.print(" [ ]");
    }
  }

  return pos;
}

void handleOptionSelectEvent() {
  JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW);
  size_t lengthOfOptions = sizeof(optionIDList) / sizeof(optionIDList[0]);

  if (joyStatus.down) {
    if (selectOptionID+1 <= lengthOfOptions) {
      selectOptionID+=1;
    }
  }
  else if (joyStatus.up) {
    if (selectOptionID-1 >= 1) {
      selectOptionID-=1;
    }
  }

  if (joyStatus.pressed) {
    buttonPressCount+=1;
  }

  // Do option page replace if button is pressed twice.
  if (buttonPressCount>=2) {
    buttonPressCount=0;
    currentOptionPage=selectOptionID;

    // Reset
    selectOptionID=1;
  }
}

void printLogo(int x, int y) {
  // Draw background
  u8g2.setDrawColor(1);
  u8g2.drawBox(x, y, 128 - x, 1);

  // Draw text
  u8g2.setCursor(x + 2, y + 10);

  u8g2.print("FM Brocaster v");
  u8g2.print(VERSION);
}

void homeMenu(void) {
  bool frameMoment = false;
  unsigned long lastDisplayUpdate = millis();
  unsigned long lastTickUpdate = millis();
  unsigned long lastAnimateUpdate = millis();
  unsigned long perFrameTime = 33;
  unsigned long perTickTime = 500;
  unsigned long perAnimateTime = 500;

  while (currentOptionPage == 0) {
    unsigned long time = millis();
    if (time - lastTickUpdate >= perTickTime) {
      lastTickUpdate = millis();
      handleOptionSelectEvent();
    }
    
    if (time - lastAnimateUpdate >= perAnimateTime) {
      lastAnimateUpdate = millis();
      frameMoment = !frameMoment;
    }

    if (time - lastDisplayUpdate >= perFrameTime) {
      lastDisplayUpdate = millis();
      u8g2.clearBuffer();
      u8g2.setCursor(0, 14);

      if (frameMoment) {
        u8g2.print("Running_");
      } else {
        u8g2.print("Running ");
      }

      CursorPos currentPos = printOptionMenu(0, 14, 36, 12);
      printLogo(currentPos.x, currentPos.y + 2);

      u8g2.sendBuffer();
    }

    delay(1);
  }
}

void test_pcm5102(float sineFreq, float squareFreq, float triangleFreq) {
    static float sinePhase = 0.0f;
    static float squarePhase = 0.0f;
    static float trianglePhase = 0.0f;

    constexpr size_t FRAMES = 256;

    int16_t samples[FRAMES * 2];

    const float sineStep     = TWO_PI * sineFreq / SAMPLE_RATE;
    const float squareStep   = TWO_PI * squareFreq / SAMPLE_RATE;
    const float triangleStep = TWO_PI * triangleFreq / SAMPLE_RATE;

    for (size_t i = 0; i < FRAMES; i++) {
        float sine = 0.0f;
        float square = 0.0f;
        float triangle = 0.0f;

        if (sineFreq > 0.0f) {
            sine = sinf(sinePhase) * 2500.0f;
        }

        if (squareFreq > 0.0f) {
            square = (squarePhase < PI ? 1.0f : -1.0f) * 1000.0f;
        }

        if (triangleFreq > 0.0f) {
            triangle =
                (trianglePhase < PI
                    ? (2.0f * trianglePhase / PI - 1.0f)
                    : (3.0f - 2.0f * trianglePhase / PI))
                * 1000.0f;
        }

        float mixed = sine + square + triangle;

        mixed = constrain(mixed, -32768.0f, 32767.0f);

        int16_t sample = static_cast<int16_t>(mixed);

        samples[i * 2]     = sample;
        samples[i * 2 + 1] = sample;

        sinePhase += sineStep;
        squarePhase += squareStep;
        trianglePhase += triangleStep;

        if (sinePhase >= TWO_PI)
            sinePhase -= TWO_PI;

        if (squarePhase >= TWO_PI)
            squarePhase -= TWO_PI;

        if (trianglePhase >= TWO_PI)
            trianglePhase -= TWO_PI;
    }

    I2S.write(
        reinterpret_cast<uint8_t *>(samples),
        sizeof(samples)
    );
}

void pcm5102DebugMenu(void) {
  unsigned long lastDisplayUpdate = 0;
  int frameMoment = 0;
  float frequency = TONE_FREQ;
  float squareFreq = 0.0f;
  float triangleFreq = 0.0f;

  while (currentOptionPage == 3) {
    test_pcm5102(frequency, squareFreq, triangleFreq);

    u8g2.clearBuffer();
    u8g2.setCursor(0, 14);

    int adc = readJoystickAxis(JOY_X);
    int offset = adc - 2048;

    if (abs(offset) > 200)
    {
        float speed = offset / 2048.0f;

        frequency += speed * 0.1f;
        frequency = roundToOneDecimal(frequency);
    }

    int adcY = readJoystickAxis(JOY_Y);
    offset = adcY - 2048;

    if (offset > 0) {
      // For triangle
      offset = abs(offset);

      float speed = offset / 2048.0f;
      triangleFreq += speed * 0.1f;
      triangleFreq = roundToOneDecimal(triangleFreq);
    } else {
      // For square
      offset = abs(offset);

      float speed = offset / 2048.0f;
      squareFreq += speed * 0.1f;
      squareFreq = roundToOneDecimal(squareFreq);
    }

    if (millis() - lastDisplayUpdate >= 300) {
      lastDisplayUpdate = millis();

      u8g2.clearBuffer();
      u8g2.setCursor(0, 14);

      switch (frameMoment) {
        case 0:
          u8g2.print("Debugging PCM5102.");
          break;

        case 1:
          u8g2.print("Debugging PCM5102..");
          break;

        case 2:
          u8g2.print("Debugging PCM5102...");
          break;

        case 3:
          u8g2.print("Debugging PCM5102....");
          break;
      }

      u8g2.setCursor(0, 28);
      u8g2.print("Sine Freq: ");
      u8g2.print(frequency);
      u8g2.setCursor(0, 40);
      u8g2.print("Square Freq: ");
      u8g2.print(squareFreq);
      u8g2.setCursor(0, 52);
      u8g2.print("Triangle Freq: ");
      u8g2.print(triangleFreq);

      u8g2.sendBuffer();

      frameMoment = (frameMoment + 1) % 4;

      hanleQuitMenuEvent();
    }
  }
}

void volumeMenu() {
  static unsigned long lastVolumeUpdate = 0;
  while (currentOptionPage == 4) {
    if (millis() - lastVolumeUpdate >= 30) {
        lastVolumeUpdate = millis();

        JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW);
        int offset=0;
        if (status.left) {
          offset = 1;
        } else if (status.right){
          offset = -1;
        }

        dacVolume += offset;

        dacVolume = constrain(dacVolume, 0, 255);

        int barWidth = (dacVolume * 84) / 255;
        
        u8g2.firstPage();
        do { 
          u8g2.setCursor(0, 14);
          u8g2.print("Volume");

          u8g2.drawFrame(20, 30, 88, 12); 
          u8g2.drawBox(22, 32, barWidth, 8); 
        } while (u8g2.nextPage());

        hanleQuitMenuEvent();
        delay(1);
    }
  }
}

constexpr int MAX_MUSIC_FILES = 20;
constexpr int MAX_FILENAME_LEN = 48;

char musicFiles[MAX_MUSIC_FILES][MAX_FILENAME_LEN];
int musicFileCount = 0;
int selectedMusic = 0;

void scanMusicFiles() {
    musicFileCount = 0;

    File dir = SD.open("/music");

    if (!dir || !dir.isDirectory()) {
        return;
    }

    File file = dir.openNextFile();

    while (file && musicFileCount < MAX_MUSIC_FILES) {
        if (!file.isDirectory()) {
            const char* name = file.name();

            const char* ext = strrchr(name, '.');

            if (ext && strcasecmp(ext, ".mp3") == 0) {
                strncpy(
                    musicFiles[musicFileCount],
                    name,
                    MAX_FILENAME_LEN - 1
                );

                musicFiles[musicFileCount][MAX_FILENAME_LEN - 1] = '\0';

                musicFileCount++;
            }
        }

        file.close();
        file = dir.openNextFile();
    }

    dir.close();
}

void handleMusicSelect() {
    int y = readJoystickAxis(JOY_Y);

    if (y < 500) {
        selectedMusic--;

        if (selectedMusic < 0) {
            selectedMusic = musicFileCount - 1;
        }

        delay(200);
    }

    if (y > 3800) {
        selectedMusic++;

        if (selectedMusic >= musicFileCount) {
            selectedMusic = 0;
        }

        delay(200);
    }
}

void drawMusicMenu() {
    u8g2.firstPage();

    do {
        u8g2.setCursor(0, 10);
        u8g2.print("Music");

        if (musicFileCount == 0) {
            u8g2.setCursor(0, 30);
            u8g2.print("No MP3 files");
            continue;
        }

        for (int row = -1; row <= 1; row++) {
            int index = selectedMusic + row;

            if (index < 0 || index >= musicFileCount) {
                continue;
            }

            int y = 32 + row * 12;

            if (index == selectedMusic) {
                u8g2.setCursor(0, y);
                u8g2.print(">");
            }

            String name = musicFiles[index];

            if (name.startsWith("/music/")) {
                name.remove(0, 7);
            }

            if (name.length() > 17) {
                name = name.substring(0, 17);
            }

            u8g2.setCursor(10, y);
            u8g2.print(name);
        }

    } while (u8g2.nextPage());
}

void musicMenu() {
    scanMusicFiles();
    while (currentOptionPage == 5) {
        handleMusicSelect();

        drawMusicMenu();

        if (digitalRead(JOY_SW) == LOW) {
            if (musicFileCount > 0) {
                Serial.print("Selected: ");
                Serial.println(musicFiles[selectedMusic]);
                audio.connecttoFS(SD, musicFiles[selectedMusic]);
            }

            delay(200);
        }

        hanleQuitMenuEvent();

        delay(1);
    }
}

void setup() {
  // debug
  Serial.begin(115200);

  // Initialize display
  u8g2.begin();
  u8g2.setContrast(255);

  // fm module
  Wire.begin();
  Wire.setClock(100000);

  // ram check
  if (psramFound()) {
        Serial.println("PSRAM found!");
        Serial.printf("PSRAM size: %u bytes\n", ESP.getPsramSize());
        Serial.printf("Free PSRAM: %u bytes\n", ESP.getFreePsram());
    } else {
        Serial.println("No PSRAM found.");
  }

  // scan i2c devices
  byte error, address;
  int nDevices;

  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address<16) Serial.print("0");
      Serial.println(address,HEX);
      nDevices++;
    }
  }
  if (nDevices == 0) Serial.println("No I2C devices found\n");

  // Set font and en-able unicode support
  u8g2.enableUTF8Print();
  u8g2.setFont(u8g2_font_6x12_tf);

  u8g2.clearBuffer();

  // Joystick 
  pinMode(JOY_SW, INPUT_PULLUP);
  analogReadResolution(12);

  // Set center position of the joystick 
  Serial.println("Correcting joystick... (Don't touch joystick when processing...)");

  int rawX = analogRead(JOY_X);
  int rawY = analogRead(JOY_Y);
  int cx = 2048;
  int cy = 2048;

  // pcm5102
  I2S.setPins(
    I2S_BCLK,
    I2S_LRCK,
    I2S_DOUT,
    -1,
    -1
  );
  audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DOUT);
  audio.setVolume(dacVolume);
  
  if (!I2S.begin(
        I2S_MODE_STD,
        SAMPLE_RATE,
        I2S_DATA_BIT_WIDTH_16BIT,
        I2S_SLOT_MODE_STEREO
  )) {
    Serial.println("Unable to init PCM5102!\n");
  } else {
    Serial.println("Found PCM5102\n");
  }

  // Init SPI bus for sd card reader
  SPI.begin(SD_SCK, SD_MISO, SD_MOSI);

  // sd card
  if (!SD.begin(SD_CS, SPI)) {
    Serial.println("Unable to init SD card module. Did the SD module is installed?\n");
    return;
  } else {
    Serial.println("Found SD card module.");

    uint8_t cardType = SD.cardType();

    if (cardType == CARD_NONE) {
      Serial.println("No SD card detected.");
    } else {
      Serial.printf("SD Card Size: %llu MB\n", SD.cardSize() / (1024ULL * 1024ULL));
      scanMusicFiles();

      for (int i = 0; i < musicFileCount; i++) {
          Serial.println(musicFiles[i]);
      }
    }
  }

  Serial.println("Initialization Finished");
}

void loop(void) {
  if (currentOptionPage == 0) {
    homeMenu();
  }
  else if (currentOptionPage == 1) {
    joyStickDebugMenu();
  }
  else if (currentOptionPage == 2) {
    fmBroadcast();  
  }
  else if (currentOptionPage == 3) {
    pcm5102DebugMenu();
  } 
  else if (currentOptionPage == 4) {
    volumeMenu();
  }
  else if (currentOptionPage == 5) {
    musicMenu();
  }
}