// ESP32 Clock Radio with Web Page for configuration
// 2024 Gord Payne
// *** need to use Partition Scheme Big App with minimal SPIFFS
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include <esp32-hal-timer.h>

#define ESP_DRD_USE_SPIFFS true
#include <TM1637Display.h>
// TM1637 Module connection pins (Digital Pins)
#define CLK 23 // display clk was 12
#define DIO 18// display data was 13
TM1637Display display(CLK, DIO);
//#include <FS.h>
#include <SPIFFS.h>
#include <WiFiManager.h> // for web configuration
#include "displayText.h" // radio station LED display mapping
#include <ArduinoJson.h> // for settings file structure

const int restartDay = 7; // if time day % 7 = 0 restart the clock
const int restartHour = 9;
const int restartMinute = 0;
int nightOff = 21; // turn off display at this hour
int dayOn = 6;// turn on display at this hour

// JSON configuration file
#define JSON_CONFIG_FILE "/settings.json"  // settings file
int const jsonFileSize = 1024; // referenced in configSave header class
// Flag for saving data
int alarmPin = 21; // for forcing into WiFi Manager on power up

// configuration settings data
char ssid[20] = ""; // default ssid
char password[20] = ""; // default password
char hsSSID[20] = "hotspot";
char hsPSWD[20] = "hsPassword";
char cDayOn[3];// convert dayOn int to string for display on webpage
char cNightOff[3];// convert nightOff int to string for display on webpage

char *metaData[50];
int maxVolume = 21; // max for the amp is 21
int tMin = 30; // was 10 touch difference minimum threshold
int tMax = 85; // touch difference maximum threshold
int tAdj = 5;
#include <esp_wifi.h>// native Espressif IDF library
#include "Audio.h"
#include "AiEsp32RotaryEncoder.h"

// Rotary Encoder
#define ROTARY_ENCODER_A_PIN 16 // CLK  RX2 on Dev Board
#define ROTARY_ENCODER_B_PIN 17 //DT   TX2 on Dev Board
#define ROTARY_ENCODER_BUTTON_PIN 5 //SW
#define ROTARY_ENCODER_STEPS 4 // sensitivity for rotation

// Audio Amp pins
// Define I2S connections
#define I2S_DOUT  22
#define I2S_BCLK  26
#define I2S_LRC   25
// backup alarm speaker and tone generation (OPTIONAL)
#define SPKR 4 (OPTIONAL)
hw_timer_t* timer = NULL;
bool value = true;
int frequency = 2300;


void IRAM_ATTR onTimer() {
  value = !value;
  digitalWrite(SPKR, value);
}
///////////////

// Alarm ON/OFF switch
#define alarmSwitch 36
// top panel buttons
#define alarmBtn 21
#define radioBtn 19
#define tBar 32 // touch bar
#define volumePot 34 // volume potentiometer

// station change variables
boolean changed = false;// for encoder changed
long lastChangeTime; // for encoder rotation
long cThresh = 1000; // threshold for changing station
// instantiate rotary encoder
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);
void IRAM_ATTR readEncoderISR()// encoder interrupt
{
  rotaryEncoder.readEncoder_ISR();
  changed = true;
  lastChangeTime = millis();
}
Audio audio;

#include <EEPROM.h>
// location 0: Alarm Hour,
//location 1: Alarm Minutes,
//location 2: saved radio index

#define EEPROM_SIZE 3
#include "time.h" // ntp time server library



const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = -18000; // set based on your time zone.
const int   daylightOffset_sec = 3600; // 0 if no Daylight Savings Time



// volume potentiometer
int volume = 0;
int lastVol = 0;
long lastPotRead;
long potThresh = 300; // for sensitivity of how often to sample the pot


boolean radioON = false;
int alrVal, radVal;
int barT;
int bAvg;// touch bar average for calibration
int encMode = 2;// encoder mode - starts in CLOCK DISPLAY MODE
int hourMax = 23;
int minuteMax = 59;
int aHour = 0;// alarm hour and minute
int aMinute = 0;
String theHour;// current time values extracted from NTP time record
String theMinute;
String theDay;
int curDay, curHour, curMinute, curSeconds;// current values for alarm and reset calcs
int curTime; // for time display (calculation for 4 digits)
int theVal;// current rotary encoder value
int lastVal = 0; // previous rotary encoder value;
int  alarmVal;// integer version of alarm time for display
long lastChk;// last encoder check time
long rThresh = 15;// encoder change threshold
long lastPress;
long bThresh = 500;// button press threshold
long lastTouch;
long tThresh = 800;// touch bar threshold

long lastTimeChk;
long radioStarted; // for settng display back to time while radio playing
boolean stationDisplay = false;
boolean alarmON = false;
int alarmSval;
String cStn;// current station displayed
int ndx; // index for encoder (radio station or hour/minute value)
boolean alarmSet = false;
boolean radioMode = false;

boolean nightMode = false; // Night Mode is for having the display off during 'sleep' hours
int bright = 1;
int dOn = true;
boolean alarmFail = false;
long alarmPlayStart;
long alarmPlayLimit = 3600000; // 1 hour

const int nStations = 10;
// Radio station ID numbers for display
char stations[nStations][5] =
{
  "---",
  "---",
  "---",
  "---",
  "---",
  "---",
  "---",
  "---",
  "---",
  "---"
};

char *new_radio;// current station to connect to
// URLs for the radio stations
char radiopresets[nStations][90] =
{
  "url",
  "url",
  "url",
  "url",
  "url",
  "url",
  "url",
  "url",
  "url",
  "url"
};
//////////////// END OF SETTINGS DATA


// header file containing WiFi Manager code and Save/Load methods

#include "configSave.h"
void setup()
{
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
  Serial.begin (115200);
  pinMode(alarmSwitch, INPUT);
  pinMode (alarmBtn, INPUT);
  pinMode (radioBtn, INPUT);
  pinMode(SPKR, OUTPUT);    // backup alarm speaker
  // setupTimer();// tone generator timer
  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  audio.setVolume(0);
  delay(10);
  display.setBrightness(0x00);
  display.clear();

  SPIFFS.begin(true);// check for existence of the config file
  boolean sE = SPIFFS.exists( JSON_CONFIG_FILE);
  SPIFFS.end();
  // try to load the existing config file
  boolean fileLoaded;
  if (!sE) { // if missing, create the file
    saveConfigFile();
  } else {
    fileLoaded = loadConfigFile();// load the config file
  }
  if (fileLoaded) Serial.println("Preferences loaded");
  radVal = digitalRead(radioBtn);// check for button down to invoke config settings mode
  if (radVal == HIGH) {
    renderText("CFG");// show in configuration mode
    setupNetworkAndFile();// enter settings configuration mode

  }


  lastTimeChk = millis();
  // slight delay to minimize chance of hang after weekly restart
  while (millis() - lastTimeChk < 2000) {
    delay(100);
  }
  display.setBrightness(0x00);


  delay(800);

  EEPROM.begin(EEPROM_SIZE);
  delay(50);
  aHour = EEPROM.read(0);// read alarm Hour
  if (aHour > hourMax)aHour = 7;
  delay(50);
  aMinute = EEPROM.read(1);// read alarm Minute
  if (aMinute > minuteMax)aMinute = 0;
  delay(50);
  ndx = EEPROM.read(2);// read saved radio station index
  volume = EEPROM.read(3);// read saved alarm volume level
  if (ndx > 9) ndx = 0;
  for (int i = 0; i < 20; i++) {// get threshold for bar touch wire
    bAvg = bAvg + touchRead(tBar);
    delay(15);
  }
  bAvg = bAvg / 20;
  alrVal = digitalRead(alarmBtn);// check for button down to invoke hotspot mode
  if (alrVal == HIGH) { // put in hotspot mode for demo/travel
    // Serial.println("Starting Hotspot");
    renderText(" HS ");
    WiFi.begin(hsSSID, hsPSWD);
    while (WiFi.status() != WL_CONNECTED) {
      delay(200);
      //  Serial.print(".");
    }
    //   Serial.println(" CONNECTED - getting time");
  } else {
    WiFi.begin(ssid, password); // standard network login
  }
  renderText("SYnc");// show in synchronizing time mode
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    //    Serial.print(".");
  }
  Serial.println(" CONNECTED - getting time");
  delay(1000);
  //init and get the time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();
  //disconnect WiFi as it's no longer needed
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);

  rotaryEncoder.begin();
  rotaryEncoder.setup(readEncoderISR);
  rotaryEncoder.setBoundaries(-500, 500, true); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
  rotaryEncoder.setAcceleration(250);
  display.setBrightness(0x00);
  cStn = String(stations[ndx]);
  renderText(cStn);
  delay(600);
  alarmVal = aHour * 100 + aMinute;
  display.showNumberDecEx(alarmVal, 0b01000000, false, 4, 0);// show saved alarm val;
  delay(600);
  lastPress = millis();
  lastTouch = millis();
  lastTimeChk = millis();
  lastPotRead = millis();
  alarmPlayStart = millis();
  displayTime();
  alarmSval = digitalRead(alarmSwitch);// get current alarm switch position
  //Serial.println(alarmSval);
  // Serial.println("finished setup");
  lastChangeTime = millis();
}



void loop() {

  if (millis() - lastTimeChk > 60000 ) {// show the current time every minute
    alarmSval = digitalRead(alarmSwitch); // see if alarm switch ON
    // night time or day time mode
    if (curHour < dayOn || curHour >= nightOff) { //turn off display during sleep hours
      nightMode = true;
      display.clear();
      bright = 0x00; // dim for nightime
    } else { // regular daytime hours, display is ON
      bright = 0x00; // normal for day viewing
      nightMode = false;
      dOn = false;// set display flag to OFF
    }
    displayTime();
    lastTimeChk = millis();
  }
  // next volume change interval check
  if (millis() - lastPotRead > potThresh) {
    // map the pot value to the volume level

    volume = map(analogRead(volumePot), 0, 2047, 0, maxVolume);// was 4095
    if (volume > maxVolume) volume = 21; // limit overValue if it happens
    if (abs(volume - lastVol) >= 1) { // reduce pot wandering
      audio.setVolume(volume);
      lastVol = volume;
    }
    lastPotRead = millis();
  }
  audio.loop();

  // TURN ALARM ON if current hour and current minute match the alarm settings and alarmON is false
  if (alarmSval == HIGH && curHour == aHour && curMinute == aMinute  && alarmON == false && radioON == false && alarmSet == false) {
    // curMinute greater than or equal to so that alarm doesn't shut off after 1 minute
    alarmON = true;
    radioON = true;
    audio.setVolume(volume);
    playAlarm();// start streaming the url index saved in EEPROM
    Serial.print("aMinute: ");
    Serial.print(aMinute);
    Serial.print('\t');
    Serial.print("curMinute:");
    Serial.println(curMinute);
    alarmPlayStart = millis();
  }
  if (alarmFail == true) {
    // play the tones
    curMinute ++;
    etone(3136, 500);
    etone(3951, 500);
    etone(4699, 500);
    delay(1000);
  }
  if (millis() - alarmPlayStart > alarmPlayLimit && alarmON == true) {// turn off alarm if not silenced after limit time
    offRoutine();
    timerAlarmDisable(timer);// turn off emergency tone generator
    alarmFail = false;
    alarmON = false;
    radioON = false;
    alarmSet = false;
    timerAlarmDisable(timer);
  }

  barT = 0;// sample touch bar
  for (int j = 0; j < 3; j++) {
    barT = barT + touchRead(tBar);// sample touch bar wire
  }
  barT = barT / 3;

  // was touchBar touched?
  if ((millis() - lastTouch > tThresh) && (bAvg - barT > tMin) && (bAvg - barT < tMax - tAdj)) { // tAdj is a damping value
    // use bar to turn OFF alarm

    if (nightMode == true ) { // handle display OFF conditions
      if (dOn == true) {
        display.clear();
        dOn = false;
        delay(1000);
      } else {
        display.setBrightness(0x00);
        boolean lZeros = false;
        if (curHour == 0) lZeros = true; // want to show leading zeros for midnight hour
        display.showNumberDecEx(curTime, 0b01000000, lZeros, 4, 0);// show saved alarm val;
        delay(5000);
        display.clear();
      }
    }
    lastTouch = millis();
  }

  alrVal = digitalRead(alarmBtn);
  radVal = digitalRead(radioBtn);
  if (alrVal == HIGH && millis() - lastPress > bThresh) {
    radioON = false;
    if (alarmSet == false) {
      display.setBrightness(0x00);
      alarmSet = true;
      dOn = true;// display is ON
      encMode = 1;
      lastVal = 0;
      display.showNumberDecEx(alarmVal, 0b0100000, false, 4, 0);
    } else {
      if (nightMode == true) {
        display.clear();
      } else {
        displayTime();
      }
      alarmSet = false;
      encMode = 2;
    }
    lastPress = millis();
  }
  // if Radio ON button pressed
  if (radVal == HIGH && millis() - lastPress > bThresh) {
    if (radioON == false) {
      display.setBrightness(0x00);
      radioON = true;
      dOn = true;
      encMode = 0;
      cStn = String(stations[ndx]);
      renderText(cStn);
      playAudio();
      radioStarted = millis();
      stationDisplay = true;
    } else {
      // turn off radio
      alarmFail = false;
      alarmON = false;
      radioON = false;
      alarmSet = false;
      offRoutine();
      encMode = 2;
      stationDisplay = false;
      displayTime();
      changed = false;
      Serial.println("Radio OFF");
    }
    lastPress = millis();
  }

  // *** rotary encoder adjustment
  if (rotaryEncoder.encoderChanged()) {
    switch (encMode) {
      case 1:// update alarm values
        updateAlarm();
        alarmVal = aHour * 100 + aMinute;
        display.showNumberDecEx(alarmVal, 0b01000000, false, 4, 0);// show saved alarm val;
        break;
      case 0:// update current radio station
        updateRadio();
        cStn = String(stations[ndx]);
        renderText(cStn);
        lastChangeTime = millis();

        break;
    }
  }
  // end of rotary encoder changed
  // Play current radio station
  if (radioON == true && changed == true && millis() - lastChangeTime > cThresh) {
    renderText("conn");
    playAudio();
    delay(500);
    cStn = String(stations[ndx]);
    stationDisplay = true;
    renderText(cStn);
    changed = false;
    radioStarted = millis();
    lastChangeTime = millis();
  }
  // *** rotary encoder switch pressed
  if (rotaryEncoder.isEncoderButtonClicked())
  {
    switch (encMode) {
      case 1:// save alarm values to EEPROM
        EEPROM.write(0, aHour);
        EEPROM.commit();
        EEPROM.write(1, aMinute);
        EEPROM.commit();
        EEPROM.write(3, volume);// save alarm volume value
        EEPROM.commit();
        alarmVal = aHour * 100 + aMinute;
        display.showNumberDecEx(alarmVal, 0b0100000, false, 4, 0);
        delay(500);
        // back to clock mode
        alarmSet = false;
        encMode = 2;
        displayTime();
        break;
      case 0:// save current radio station to EEPROM
        EEPROM.write(2, ndx);
        EEPROM.commit();
        display.showNumberDecEx(cStn.toInt(), 0b00000000, false, 4, 0);
        delay(500);
        displayTime();
        lastChangeTime = millis();
        break;
    }
  }
  // go back to time display after radio station displayed for 10 seconds
  if (radioON == true && stationDisplay == true && millis() - radioStarted > 20000) {
    displayTime();
    stationDisplay = false;

  }
}


// **** Functions

void printLocalTime()// grab and parse the current time
{
  // format  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
  // day name, month, day number, year, hour, minute, seconds
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    // Serial.println("Failed to obtain time");
    display.showNumberDecEx(-999, 0b00100000, false, 4, 0);
    return;
  }

  // Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
  char timeHour[3];
  strftime(timeHour, 3, "%H", &timeinfo);
  char timeMinute[3];
  strftime(timeMinute, 3, "%M", &timeinfo);
  char timeDay[3];
  strftime(timeDay, 3, "%d", &timeinfo);
  theHour = String(timeHour);
  theMinute = String(timeMinute);
  theDay = String(timeDay);
  curHour = theHour.toInt();
  curMinute = theMinute.toInt();
  if (alarmON == true) curMinute = curMinute + 1; // advance so alarm doesn't keep replaying
  curDay = theDay.toInt();



  // *** check for weekly restart time
  // check for weekly restart time on every multiple of day 7
  if ((curDay % restartDay == 0) && (curHour == restartHour) && (curMinute == restartMinute)) {
    // Serial.println("restart path");
    delay(45000);// run down the seconds to minimize multiple restarts
    //  Serial.println("weekly restart");
    ESP.restart();
  }
  int twelveHour;
  if (curHour > 12) {
    twelveHour = curHour - 12;// change to 12 hour time.
  } else {
    twelveHour = curHour;
  }
  curTime = twelveHour * 100 + theMinute.toInt();
}

void connectToWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    //  Serial.print(".");
  }
  // Serial.println("\nWiFi connected");

}

void playAudio()
{
  if (WiFi.status() != WL_CONNECTED) {
    connectToWiFi();
  }
  // Serial.println("connectiontoRadio");
  new_radio = radiopresets[ndx];
  audio.connecttohost(new_radio);
}


void audio_showstation(const char *info) {
  // Serial.print("station     ");
  // Serial.println(info);
  stationName(info);// call function to see if valid
  // station connected. If not, use emergency backup alarm
}

void stationName(const char *info) {// determine if valid station playing
  Serial.print("stationLength: *");
  String theStation = info;
  Serial.print(theStation);
  Serial.print("*");
  Serial.print('\t');
  Serial.println(theStation.length());
  if (theStation.length() < 2 && alarmON == true) alarmFail = true;// no station is playing
}

// Audio status functions
/*void audio_info(const char *info) {
  Serial.print("info        ");
  metaData = info;
  Serial.println(metaData);

  }*/
void audio_id3data(const char *info) { //id3 metadata
  Serial.print("id3data     "); Serial.println(info);
}
/* void audio_eof_mp3(const char *info) { //end of file
  Serial.print("eof_mp3     "); Serial.println(info);
  }

  void audio_showstreaminfo(const char *info) {
  Serial.print("streaminfo  "); Serial.println(info);
  }
  void audio_showstreamtitle(const char *info) {
  Serial.print("streamtitle "); Serial.println(info);
  }
  void audio_bitrate(const char *info) {
  Serial.print("bitrate     "); Serial.println(info);
  }
  void audio_commercial(const char *info) { //duration in sec
  Serial.print("commercial  "); Serial.println(info);
  }
  void audio_icyurl(const char *info) { //homepage
  Serial.print("icyurl      "); Serial.println(info);
  }
  void audio_lasthost(const char *info) { //stream URL played
  Serial.print("lasthost    "); Serial.println(info);
  }
  void audio_eof_speech(const char *info) {
  Serial.print("eof_speech  "); Serial.println(info);
  }*/

void offRoutine(void)
{
  //Serial.println("Streamer will shutdown in 1 second");
  delay(1000);
  audio.stopSong();
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

void updateAlarm() {// adjust curernt alarm time
  theVal = rotaryEncoder.readEncoder();
  if (theVal > lastVal) { // time moving forward
    aMinute ++;
    if (aMinute > 59) {
      aHour++;
      aMinute = 0;
      if (aHour >= 24) aHour = 0;
    }
  } else  { // time moving backward
    aMinute --;
    if (aMinute < 0) {
      aHour--;
      aMinute = 59;
      if (aHour <= 0) aHour = 23;
    }
  }

  lastVal = theVal;
}

void updateRadio() { //adjust current radio station
  int theVal = rotaryEncoder.readEncoder();
  if (theVal > lastVal) { // station moving forward
    ndx ++;
    changed = true;
    if (ndx > nStations - 1) {
      ndx = 0;
    }
  } else if (theVal <= lastVal) {
    ndx --; changed = true;
    if (ndx < 0) ndx = nStations - 1;
  }
  lastVal = theVal;
  cStn = String(stations[ndx]);
  renderText(cStn);
  lastChangeTime = millis();
}

void playAlarm() {

  WiFi.mode(WIFI_STA);// native Espressif IDF library
  WiFi.begin(ssid, password);
  //  Serial.print("Connecting to: ");
  while (WiFi.status() != WL_CONNECTED && alarmFail == false)
  {
    //   Serial.print(".");
    delay(500);
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED)
  {
    ndx = EEPROM.read(2);// grab the saved preset
    new_radio = radiopresets[ndx];
    while (!audio.isRunning()) {
      //     Serial.print("*");
      audio.connecttohost(new_radio);
    }
  }
}

void displayTime() {// set brightness and display the current time or clear the screen if at night
  printLocalTime();

  if (nightMode == false) {
    display.setBrightness(0x00);
    display.showNumberDecEx(curTime, 0b01000000, false, 4, 0);// show saved alarm val;
  }
}

//////////// Backup Alarm Speaker tone generation functions OPTIONAL
void setupTimer() {
  // Use 1st timer of 4  - 1 tick take 1/(80MHZ/80) = 1us so we set divider 80 and count up
  timer = timerBegin(0, 80, true);//div 80
  timerAttachInterrupt(timer, &onTimer, true);
}

void setFrequency(long frequencyHz) {
  timerAlarmDisable(timer);
  timerAlarmWrite(timer, 1000000l / frequencyHz, true);
  timerAlarmEnable(timer);
}

void etone(long frequencyHz, long durationMs) {
  setFrequency(frequencyHz);
  delay(durationMs);
}
