//#include "Button.h"
#include <Wire.h>
#include "Pitches.h"
#include <Adafruit_TPA2016.h>  // Amplifier
#include <RotaryEncoder.h>
#include <LiquidCrystal_I2C.h>

const int PIN_BTN_SNOOZE = 3, PIN_BUTTON_MODE = 10, PIN_BUTTON_CALIB = 9, PIN_BTN_PROFIL = 8;
const int PIN_ENCODER_CLK = 2, PIN_ENCODER_DT = 1;  //1 , 2
const int PIN_SPEAKER = 0;                          // PA2
const int LCD_I2C_adress = 0x27;//0x20;
//const int SPEAKER_I2C_adress = 0x58; // Adress 0x58
//const int PIN_I2C_SDA = 6, PIN_I2C_SCL = 7;

// CLOCK Predefines //
#define max_hours 24
#define max_minutes 60
#define max_seconds 60
#define max_milliseconds 1000

// ENCODER last Position
int last_pos = 0;

//PROFILE CREATION //
int profileIndex = 0;  // lengt+1 max

// CLOCK CALI //
bool cali_active = false;
bool cali_press = false;
int caliModeIndex = 0;
int profileModeIndex = 0;
#define profilerMax 3

#define accecptCounterMax 4 // COUNTER MAX of the ENUM
enum cali : int {
  _hour = 0,
  _minute = 1,
  _seconds = 2,
  _normal = 3,
};


// SET CURRENT WINDOW MODE //
enum window : int {
  _clock = 0,
  _cali = 1,
  _profil = 2,
  _alarm = 3,
  _gain = 4,

};

// Encoder last Position for each Mode
long lastProfil_Pos = 0;
long lastCali_Pos = 0;

// DisplayTimer //
unsigned long  display_until= 0;
#define refreshtime 500 

// SHOW GAIN TIMER //
unsigned long show_gain_until = 0;
#define maxGainIntervall 1000

// SET CURRENT MODE OF NORMAL //
unsigned long switch_until = 0;  // ROTATION COUNTER
int rotationIndex = 0;           //Rotation INDEX
int modeIndex = 0;
#define modeIndexMax 4
#define rotationIntervall 1000

enum normal : int {
  _time = 0,
  _date = 1,
  _datetime = 2,
  _rotation = 3
};

// CHECK WEEK DAY //
enum weekday : int {
  sunday = 1,
  monday = 2,
  tuesday = 3,
  wednesday = 4,
  thursday = 5,
  friday = 6,
  saturday = 7
};


window window_status = window::_clock;  // CURRENT STATUS

// REFRESH INTERVALL //
#define button_refresh_intervall 100
class Button {
private:
  unsigned long check_intervall = button_refresh_intervall;
  unsigned long ignore_until_time;
  bool state = HIGH;  // Current State
  bool m_hasChanged;
  int m_PIN = 0;
  bool PRESSED = LOW;
  bool RELEASED = HIGH;
public:
  Button(int PIN)
    : m_PIN(PIN) {
    pinMode(m_PIN, INPUT);
  }

  enum PULL_STATE {
    PULLUP = LOW,
    PULLDOWN = HIGH
  };

  Button(int PIN, Button::PULL_STATE pull_state)
    : m_PIN(PIN) {
    pinMode(m_PIN, INPUT);
    setState(pull_state);
  }

  void setState(bool pull_state) {
    this->PRESSED = pull_state;
    this->RELEASED = !this->PRESSED;
  }

  bool press() {
    // Compares against current State of Button
    return this->read() == this->PRESSED;
  }

  bool pressed() {
    // Compares against current State of Button
    return this->read() == this->PRESSED && this->hasChanged();
  }

  // SET to Interrupt? //
  bool read() {
    // After past Countdown Read
    bool afterUntil = (ignore_until_time <= millis());

    // And State has Changed
    bool tmp_hasChanged = digitalRead(m_PIN) != state;

    // Change State of Button
    if (afterUntil && tmp_hasChanged) {
      ignore_until_time = millis() + check_intervall;
      state = !state;
      m_hasChanged = true;
    }
    // Return Current State => High or Low
    return state;
  }

  bool hasChanged() {
    // Makes sure that the CHECK only ACCURES one time after Check
    if (m_hasChanged) {
      m_hasChanged = false;
      return true;
    }
    return false;
  }
};


class Time {
protected:
  unsigned int m_hours = 0;
  unsigned int m_minutes = 0;
  unsigned int m_seconds = 0;
  unsigned long m_milliseconds = 0;
public:
  Time() {}

  Time(int hours, int minutes, int seconds)
    : m_hours(hours),
      m_minutes(minutes),
      m_seconds(seconds) {
    m_milliseconds = 0;
  }

  // Adder
  void addMilliseconds(unsigned long milliseconds) {
    unsigned long tmp_milliseconds = this->m_milliseconds + milliseconds;
    if (tmp_milliseconds >= max_milliseconds) {
      addSeconds((int)tmp_milliseconds / max_milliseconds);
    }
    this->m_milliseconds = tmp_milliseconds % max_milliseconds;
  }

  void addSeconds(int seconds) {
    int tmp_seconds = this->m_seconds + seconds;

    if (tmp_seconds >= max_seconds) {
      addMinutes((int)tmp_seconds / max_seconds);
    }

    this->m_seconds = (tmp_seconds) % max_seconds;
  }

  void addMinutes(int minutes) {
    int tmp_minutes = (this->m_minutes + minutes);
    if (tmp_minutes >= max_minutes) {
      addHours((int)tmp_minutes / max_minutes);
    }
    this->m_minutes = tmp_minutes % max_minutes;
  }

  void addHours(int hours) {
    this->m_hours = (this->m_hours + hours) % max_hours;
  }

  //SET
  void setMilliseconds(unsigned long milliseconds) {
    this->m_milliseconds = milliseconds % max_milliseconds;
  }

  void setSeconds(unsigned int seconds) {
    this->m_seconds = (seconds) % max_seconds;
  }

  void setMinutes(unsigned int minutes) {
    this->m_minutes = minutes % max_minutes;
  }

  void setHours(unsigned int hours) {
    this->m_hours = hours % max_hours;
  }

  //GETTER
  unsigned long milliseconds() {
    return this->m_milliseconds;
  }

  unsigned int seconds() {
    return this->m_seconds;
  }

  unsigned int minutes() {
    return this->m_minutes;
  }

  unsigned int hours() {
    return this->m_hours;
  }

  String toString() {
    return (String)this->m_hours + ":" + this->m_minutes + ":" + this->m_seconds;
  }
};

class Clock : public Time {
  unsigned long TIME = 0;
  
public:
  bool preventUpdate = false;
  Clock(int hours, int minutes, int seconds)
    : Time(hours, minutes, seconds) {
    TIME = millis();
  }

  void updateLoop() {
    // Immer  millis >= TIME
    unsigned long offsetTime = millis() - TIME;  // Diffrenz in ms
    if(!preventUpdate){
      addMilliseconds(offsetTime);
    }
    
    TIME = millis();
  }
};

// TODO Extend with DB libary
class Speaker {
private:
  int m_PIN = 0;
  int gain = -5;  // Default
  bool rightchannel = true, leftchannel = false;
  Adafruit_TPA2016 audioamp;  // Adress 0x58
public:
  Speaker() {}
  Speaker(int PIN)
    : m_PIN(PIN) {
    audioamp = Adafruit_TPA2016();
    audioamp.begin();

    audioamp.setGain(gain);
    // Sets the Channel for the Speaker
    audioamp.enableChannel(rightchannel, leftchannel);

    //Limitsthe Speaker Overdrive
    audioamp.setLimitLevelOn();
    audioamp.setLimitLevel(25);  // range from 0 (-6.5dBv) to 31 (9dBV)

    audioamp.setAttackControl(5);
    audioamp.setHoldControl(0);
    audioamp.setReleaseControl(11);
  }

  void tone(unsigned int frequency) {
    ::tone(m_PIN, frequency);
  }

  void tone(unsigned int frequency, unsigned long duration) {
    ::tone(m_PIN, frequency, duration);
  }

  void noTone() {
    ::noTone(m_PIN);
  }

  // MAX PEAK Loudness
  // Takes in Gain as a value
  void setGain(int new_gain) {
    // Mini//
    if (new_gain < -28) {
      new_gain = -28;
    }
    // MAX //
    if (new_gain > 30) {
      new_gain = 30;
    }

    gain = new_gain;
    audioamp.setGain(gain);
  }

  int getGain() {
    return gain;
  }
};


class Profile : public Time {
  // ALARM CHECK
  /*
   Alarm changed if
   1. Stop Alarm
   2. refress Alarm
      - Alarm begin => true
      - Alarm end => false
  */
  bool _isAlarm = false;
  bool _alarmBeginnCalled = false;  //CALLED if ALARM TRIGGER WAS CALLED
  //bool _alarmEndCalled = false; // CALLED IF ALARM CLOSED TRIGGER CALLED
  

  int max_alarm_duration = 10;  // in minutes
  Time endTime;
  /*
    note durations: 4 = quarter note, 8 = eighth note, etc.:
    to calculate the note duration, take one second divided by the note type.
    e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
    int noteDuration = 1000 / noteDurations[thisNote];
  */
  static const int melodyLength = 8;
  int melody[melodyLength] = {
    NOTE_FS6, NOTE_FS6, NOTE_FS6, NOTE_FS6, 0, 0, 0, 0
  };
  int noteDurations[melodyLength] = {
    16, 16, 16, 16, 16, 16, 16, 16
  };
public:
  int currentNote = 0;
  unsigned long currentMax = 0;
  unsigned long after_time = 0;
  bool noteChanged = true;
  bool _alarmClosed = false;

  // EMPTY
  Profile() {}

  Profile(int hours, int minutes)
    : Time(hours, minutes, 0) {
    endTime = Time(this->m_hours, this->m_minutes, 0);
    endTime.addMinutes(max_alarm_duration);
  }

  bool isAlarm(::Clock &clock, Speaker &speaker) {
    refreshAlarm(clock, speaker);
    return _isAlarm;
  }

  void refreshAlarm(::Clock &clock, Speaker &speaker) {
    bool alarmBegin = clock.hours() == this->m_hours && clock.minutes() == this->m_minutes;                // START TIME WINDOW (Note no Seconds) => Causes Many TRUE STATES
    bool alarmEnd = clock.hours() == this->endTime.hours() && clock.minutes() == this->endTime.minutes();  // END TIME WINDOW (Note no Seconds)=> Causes Many TRUE STATES

    // Called Once
    if (alarmBegin && !_alarmBeginnCalled && !_alarmClosed) {
      _isAlarm = true;
      _alarmBeginnCalled = true;
    }

    // Called in Loop
    if (alarmEnd) {
      _isAlarm = false;
      _alarmClosed = false;
    }

    if (!_isAlarm) {
      resetAlarm(speaker);
    }
  }

  void stopAlarm() {
    _alarmClosed = true;
    _isAlarm = false;
  }

  // RESET ALL ALARM FLAGS
  void resetAlarm(Speaker &speaker) {
    _isAlarm = false;
    _alarmBeginnCalled = false;  // FLAG to Prevent Double Calling beginn
    setCurrentNote(0);
    speaker.noTone();
  }

  void loopPlayAlarm(Speaker &speaker) {
    int noteDuration = 1000 / noteDurations[currentNote];
    speaker.tone(melody[currentNote], noteDuration);
    // to distinguish the notes, set a minimum time between them.
    // the note's duration + 30% seems to work well:
    int pauseBetweenNotes = noteDuration * 1.30;
    delay(pauseBetweenNotes);
    // stop the tone playing:
    speaker.noTone();
    setCurrentNote(currentNote + 1);
  }

  void setCurrentNote(int note) {
    currentNote = note % melodyLength;
    noteChanged = true;
  }

  void setMinutes(unsigned int minutes) {
    this->m_minutes = minutes % max_minutes;
    // SET ENDTIMER //
    this->endTime.setMinutes(minutes);
    this->endTime.addMinutes(max_alarm_duration);
  }

  void setHours(unsigned int hours) {
    this->m_hours = hours % max_hours;
    // SET ENDTIMER //
    this->endTime.setHours(hours);
    this->endTime.addMinutes(max_alarm_duration);
  }

  String toString() {
    return (String)this->m_hours + ":" + this->m_minutes;
  }
};

Speaker speaker; 
Clock clock(22, 57, 50);
RotaryEncoder encoder(PIN_ENCODER_CLK, PIN_ENCODER_DT, RotaryEncoder::LatchMode::TWO03);  //FOUR3  // FOUR0 // TWO03
Button btn_snooze(PIN_BTN_SNOOZE, Button::PULL_STATE::PULLUP);
Button btn_cali(PIN_BUTTON_CALIB, Button::PULL_STATE::PULLUP);
Button btn_profil(PIN_BTN_PROFIL, Button::PULL_STATE::PULLUP);
Button btn_mode(PIN_BUTTON_MODE, Button::PULL_STATE::PULLUP);
//Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
LiquidCrystal_I2C lcd(LCD_I2C_adress,16,2);
Profile profile(22, 58);

void setup() {
  Wire.begin();
  while (!Serial) { delay(10); }
  Serial.begin(9600);

  speaker = Speaker(PIN_SPEAKER);
  lcd.init();
  lcd.backlight();

  attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), updateEncoder, CHANGE);
}


void loop() {
  // GENERAL ACTIONS //
  //encoder.tick();
  
  // REFRESH ACTION STATE //
  stateAlarm();
  updateStateProfil();
  //encoder.tick();
  stateCali();
  updateMode();
  stateGain();  //Encoder was MOVMET to set GAIN
  //encoder.tick();
  // MAKES ACTION BASE ON MODE //
  
  action();
  clock.updateLoop();
  //encoder.tick();
}

void action() {
  //encoder.tick();
  switch (window_status) {
    // Normal Updates
    case window::_clock:
      clock.preventUpdate = false;
      show("NORMAL",  setNormal(modeIndex));
      break;

    // Calibration Updates
    case window::_cali:
      //encoder.tick();
      clock.preventUpdate = true;
      updateCali();
      show("Calibration", setClock(caliModeIndex));
      break;

    // Profile Update
    case window::_profil:
      //encoder.tick();
      updateProfil();
      show("SET PROFIL", setProfil(profileModeIndex, profile));
      break;
    // GAIN SET
    case window::_gain:
      //encoder.tick();
      show("GAIN", (String) setGain() + "db");
      break;

    // ALARM Active
    case window::_alarm:
      show("ALARM", (String) "Its " + profile.toString());
      profile.loopPlayAlarm(speaker);
      break;
  }
}

// UPDATE ACTION STATE //
void stateCali() {
  bool windowNotBussy = (window_status == window::_clock) || (window_status == window::_cali);
  if (windowNotBussy) {

    // MUST BE PRESSE, else it will Block other Button Request
    if (btn_cali.press() && !cali_active) {
      cali_active = true;
      cali_press = true;
    }else if(!btn_cali.press() &&cali_active ){
      cali_press = false;
    }

    if (cali_active) {
      window_status = window::_cali;
    }

    if (!cali_active) {
      window_status = window::_clock;
      caliModeIndex = 0;
    }
  }
}

void stateProfil() {
  bool windowNotBussy = (window_status == window::_clock) || (window_status == window::_profil);
  if (windowNotBussy) {
    if (profileIndex >= 1) {
      window_status = window::_profil;
    } else {
      window_status = window::_clock;
      profileModeIndex = 0;
    }
  }
}

void stateGain() {
  bool windowNotBussy = (window_status == window::_clock) || (window_status == window::_gain);
  if (windowNotBussy) {
    if (hasChanged()) {
      window_status = window::_gain;
      show_gain_until = millis() + maxGainIntervall;
    }

    if (show_gain_until <= millis()) {
      window_status = window::_clock;
    }
  }
}

void stateAlarm() {
  if (profile.isAlarm(clock, speaker)) {
    window_status = window::_alarm;
  }

  if (btn_snooze.press()) {
    window_status = window::_clock;
    profile.stopAlarm();
  }
}

// ALSO pos Passibil as Referenz
// CALC Offset since Last Call of Position
int getEncoderOffset() {
  int newPos = encoder.getPosition();
  // TEMP SAVE
  int value = newPos - last_pos;
  last_pos = newPos;  // Reset Value
  return value;
}

bool hasChanged() {
  int newPos = encoder.getPosition();
  return last_pos != newPos;
}

// UPDATE VALUE fur Cali //
void updateCali() {
  if (btn_cali.pressed() && !cali_press) {
    caliModeIndex++;
    caliModeIndex = caliModeIndex % accecptCounterMax;
  }

  if (caliModeIndex == cali::_normal) {
    cali_active = false;
  }
}

// UPDATE VALUE fur Profil //
void updateProfil() {
  if (btn_cali.pressed()) {
    profileModeIndex++;
    profileModeIndex = profileModeIndex % profilerMax;
  }
}


void updateStateProfil() {
  if (btn_profil.pressed()) {
    profileIndex++;
    profileIndex = profileIndex % 2;  //(profiles.length + 1);
  }
  stateProfil();
}

void updateMode() {
  if (btn_mode.pressed()) {
    modeIndex++;
    modeIndex = modeIndex % modeIndexMax;
  }
}


// SET VALUES //
String setNormal(int modeIndex) {
  switch (modeIndex) {
    case ::_time:
      return (String)clock.toString();
      break;
    case ::_date:
      return (String) "2024-13-2";
      break;
    case ::_datetime:
      return (String)clock.toString() + " " + "13-2";
      break;
    case ::_rotation:
      // CHECK FOR TIME DELAY
      if (switch_until <= millis()) {
        switch_until = millis() + rotationIntervall;
        rotationIndex++;
        rotationIndex = rotationIndex % (modeIndexMax - 1);
      }
      return setNormal(rotationIndex);
      break;
  }
}

String setGain(){
  if (hasChanged()) {
        int gain = speaker.getGain() + getEncoderOffset();
        speaker.setGain(gain);
  }
  return (String) speaker.getGain();
}

String setProfil(int &cale_modeProfil, Profile &profile) {
  String text = profile.toString();
  long newPos = encoder.getPosition();
  if (lastProfil_Pos != newPos) {
    long value =  lastProfil_Pos- newPos;
    int tmp;

    // SWITCH CASE based on CALI MODE//
    switch (cale_modeProfil) {
      case cali::_hour:
        tmp = (((profile.hours() + value)%max_hours)+max_hours)%max_hours;
        profile.setHours(tmp);
        text = (String) "|" + profile.hours() + "|:" + profile.minutes();
        break;

      case cali::_minute:
        tmp = (((profile.minutes() + value)%max_minutes)+max_minutes)%max_minutes;
        
        profile.setMinutes(tmp);
        text = (String) profile.hours() + ":|" + profile.minutes() + "|";
        break;
    }
    profile._alarmClosed = false;
    lastProfil_Pos = newPos;
  }
  
  return text;
  
}

void updateEncoder(){
  encoder.tick();
}

void show(String mode, String text) {
  Serial.println("------ Display --------");
  Serial.println((String) "Profil State:" + mode);
  Serial.println((String)text);
  if(display_until <= millis()){
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print((String)"State:" + mode);
    lcd.setCursor(0,1);
    lcd.print((String)text);
    display_until =millis() + refreshtime;
  }
}

String setClock(int &cal_mode) {
  String text = clock.toString();
  //encoder.tick();
  long newPos = encoder.getPosition();
  if (lastCali_Pos!=newPos) {
    long value = lastCali_Pos-newPos;
    int tmp;

    switch (cal_mode) {
      case cali::_hour:
        tmp = (((clock.hours() + value)%max_hours)+max_hours)%max_hours;
        clock.setHours(tmp);
        text = (String) "|" + clock.hours() + "|:" + clock.minutes() + ":" + clock.seconds();
        break;
      case cali::_minute:
        tmp = (((clock.minutes() + value)%max_minutes)+max_minutes)%max_minutes;
        clock.setMinutes(tmp);

        text = (String) clock.hours() + ":|" + clock.minutes() + "|:" + clock.seconds();
        break;
      case cali::_seconds:
        tmp = (((clock.seconds() + value)%max_seconds)+max_seconds)%max_seconds;        
        clock.setSeconds(tmp);
        text = (String)clock.hours() + ":" + clock.minutes() + ":|" + clock.seconds() + "|";
        break;
    }
    lastCali_Pos = newPos;
  }
  return text;
}