#include <PinChangeInterrupt.h>
#include <RotaryEncoder.h>
#include <LiquidCrystal_I2C.h>
#include <TimerOne.h>
#include <EEPROM.h>
#include <TimeAlarms.h>
#include <time.h>

RotaryEncoder *encoder = nullptr;
LiquidCrystal_I2C lcd(0x27, 20, 4);

#define buttonPin 6
#define rotAPin 7
#define rotBPin 8
#define buzzerPin 9
#define hotPlatePin 10 //Can use 9 or 10. Timer1 will interfere with normal pwm functions on these pins

volatile bool buttonFlag;

uint8_t block[8]  = {0xff,0xff,0xff,0xff,0xff,0xff,0xff};

//we need some structs now to store each step and to store the programs
struct Step {
  bool buzz;
  byte timer;
  byte power;
};

struct Program {
  byte numSteps;
  byte index;
  Step steps[30];
};

void setup() {
  //Rotary encoder setup
  encoder = new RotaryEncoder(rotAPin, rotBPin, RotaryEncoder::LatchMode::FOUR3);
  attachPCINT(digitalPinToPCINT(rotAPin), checkPosition, CHANGE);
  attachPCINT(digitalPinToPCINT(rotBPin), checkPosition, CHANGE);

  //Button setup
  pinMode(buttonPin, INPUT_PULLUP);
  attachPCINT(digitalPinToPCINT(buttonPin), buttonFlagger, RISING);

  //hotplate setup
  pinMode(hotPlatePin, OUTPUT);
  Timer1.initialize(1000000); //initialize timer to 1 second
  Timer1.start();
  Timer1.pwm(hotPlatePin, 0);

  //LCD setup
  lcd.init();
  lcd.backlight();

  //buzzer setup
  pinMode(buzzerPin, OUTPUT);

  //Custom char for power indicator, of course
  lcd.createChar(0, block);

  //Why am I doing this
  randomSeed(analogRead(0));

  //Serial.begin(115200);

  for(int x = 1; x <= 9; x++){
    resetProgram(x);
  }

  //lcdPrint(1,1, giveRandom(80*2));
}

void loop() {
  // put your main code here, to run repeatedly:
  //lcdPrint(1,1, giveRandom(80*2));
  stoveLoop();
  //lcdPrint(1,1, giveRandom(80*2));
  programSelectLoop();
}

////////////////////////////////////////////////////////////////////////////////////////
//                          Interrupt stuff
void checkPosition() { //Call this as interrupt fuction to make encoder work
  encoder->tick();
}

void buttonFlagger(){
  buttonFlag = 1;
}

int flagChecker(){
  if (buttonFlag == 1) {
    buttonFlag = 0;
    return 2;
  }
  int direction = (int)(encoder->getDirection());
  return direction;
}
////////////////////////////////////////////////////////////////////////////////////////////
void lcdPrint(int x, int y, String text) {
  lcd.setCursor(x - 1, y - 1);
  lcd.print(text);
}

void clear(){
  lcd.setCursor(0,0);
  for (int x = 0; x < 80; x++){
    lcd.print(" ");
  }
}

void updatePower(long power, bool ring = false){
  //takes a number between 0 and 100 and updates duty cycle with numbers 0 to 1023
  int duty;
  digitalWrite(buzzerPin, ring);
  duty = power * 1023 / 100;
  Timer1.setPwmDuty(hotPlatePin, duty);
}

String giveRandom(int numChars){//This program is solely to generate random characters for the screen transition
  String randomString = "";

  for (int i = 0; i < numChars; i++) {
    char randomChar = random(32, 127); // Generate a random ASCII character
    randomString += randomChar;
  }

  return randomString;
}

void powerVisual(byte power){//This function does the bar at the bottom of the screen for power
  byte numBlocks = power/5;
  lcd.setCursor(0,3);
  for (byte x = 0; x < numBlocks; x++){
    lcd.write(0);
  }
}

void stoveLoop(){
  //This is where the program initializes and this is where we live when we use the stove manually.
  //We want to listen for a button press to move to the program select, and we want to listen to 
  //encoder rotation to change the duty cycle of the stove.
  lcd.blink_off();

  int flag = 0;
  int power = 0;

  lcdPrint(1,1,"Manual Stove Control                                                            ");

  while (true){
    flag = flagChecker(); //Check to see if user did something
    switch (flag) {
      case 0:
        break;
      case -1:
        if (power > 0) {
          power -= 9;
          power = constrain(power, 0, 100);
          lcdPrint(1,4,"                    ");
          updatePower(power);
        }
        break;
      case 1:
        if (power < 100) {
          power += 10;
          power = constrain(power, 0, 100);
          updatePower(power);
        }
        break;
      case 2:
        updatePower(0); //Shuts off stove
        return;
        break;
      
    }
  lcdPrint(9,3,((String)(power) + " %     "));

  powerVisual(power);

  }
}


void programSelectLoop(){
  //this is gonna be weird. I need a return button at the top and a series of program buttons below.
  //I can probably just use an array of strings to hold the different options and include some basic logic for moving around.
  const char progSelectOptions[12][11] = {
    "Programs  ",
    " Return   ",
    " Program 1",
    " Program 2",
    " Program 3",
    " Program 4",
    " Program 5",
    " Program 6",
    " Program 7",
    " Program 8",
    " Program 9",
    " Reset ALL"
  };

  byte selectedIndex = 1;
  byte screenIndex = 0;
  byte maxScreenIndex = 8;
  int flag = 0;

  lcd.blink_off();
  clear();
  for (byte i = 1; i < 5; i++){
    lcdPrint(1,i,progSelectOptions[i+screenIndex-1]);
  }
  lcdPrint(1, selectedIndex-screenIndex+1, ">");

  while (true) {

    flag = flagChecker(); //Check to see if user did something
    delay(20); //This helps with user input somehow

    switch (flag) {
      case 0:
        continue;
        break;
      case 1:
        if (selectedIndex - screenIndex == 0 && screenIndex > 0) screenIndex--;
        if (selectedIndex > 1) selectedIndex--;
        break;
      case -1:
        if (selectedIndex - screenIndex == 3 && screenIndex < maxScreenIndex) screenIndex++;
        if (selectedIndex < 11) selectedIndex++;
        break;
      case 2:
        if (selectedIndex == 1) {
          return; // Go back to stove screen
        }
        if (selectedIndex > 1 && selectedIndex < 11){
          //lcdPrint(1,1, giveRandom(80*2));
          programChoices(selectedIndex - 1); //Enter options for selected program
        } 
        if (selectedIndex == 11) {
          for(int x = 1; x <= 9; x++){
            resetProgram(x);
          }
        }
        break;
    }
      
    //Display stuff
    clear();
    for (byte i = 1; i < 5; i++){
      lcdPrint(1,i,progSelectOptions[i+screenIndex-1]);
    }
    lcdPrint(1, selectedIndex - screenIndex + 1, ">");

  }
}

void programChoices(byte programIndex){
  //We choose whether to run the program or edit it here.

  const char progSelectOptions[4][21] = {
    "     Program #      ",
    " Run                ",
    " Edit               ",
    " Return             "
  };


  byte selectedIndex = 1;
  int flag = 0;

  lcd.blink_off();

  for (byte i = 1; i < 5; i++){
    lcdPrint(1,i,progSelectOptions[i - 1]);
  }
  lcdPrint(15, 1, (String)(programIndex));
  lcdPrint(1, selectedIndex + 1, ">");

  while (true) {

    flag = flagChecker();
    delay(20);

    switch (flag) {
      case 0:
        continue;
        break;
      case -1:
        if (selectedIndex < 3) selectedIndex++;
        break;
      case 1:
        if (selectedIndex > 1) selectedIndex--;
        break;
      case 2:
        if (selectedIndex == 3) {
          //lcdPrint(1,1, giveRandom(80*2));
          return;
        }
        else if (selectedIndex == 2) {
          //lcdPrint(1,1, giveRandom(80*2));
          programEditLoop(programIndex);
          lcd.blink_off();
          break;
        }
        else if(selectedIndex == 1) {
          //lcdPrint(1,1, giveRandom(80*2));
          programRunLoop(programIndex);
          break;
        }
        break;
      }

    //Display stuff
    for (byte i = 1; i < 5; i++){
      lcdPrint(1,i,progSelectOptions[i - 1]);
    }
    lcdPrint(15, 1, (String)(programIndex));
    lcdPrint(1, selectedIndex + 1, ">");
  }


}

const short progLocations[9] = {
  142,
  234,
  326,
  418,
  510,
  602,
  694,
  786,
  878
};

void resetProgram(byte progNumber){
  //ProgNumber starts at 1
  Program emptyProg;
  Step emptyStep;
  emptyStep.buzz = false;
  emptyStep.power = 0;
  emptyStep.timer = 0;
  emptyProg.index = progNumber;
  emptyProg.numSteps = 1;
  for(int x = 0; x < 30; x++){
    emptyProg.steps[x] = emptyStep;
  }
  EEPROM.put(progLocations[progNumber-1],emptyProg);
}

void updateEditDisplay(Program tempProg, int sind){
  int windowLength = tempProg.numSteps + 3;
  int row = 0;
  
  for (int x = sind; x < sind + 4; x++){

    if (x == 0){

      char labelLine[21];
      sprintf(labelLine, "Return    Program #%i", tempProg.index);

      lcd.setCursor(0, row);
      lcd.print(labelLine);

    }
    else if (x == windowLength - 2) {
      lcd.setCursor(0, row);
      lcd.print("Add Step            ");
    }
    else if (x == windowLength - 1){
      lcd.setCursor(0, row);
      lcd.print("Remove Step         ");
    }
    else {  //Write out step information

      char lineString[21]; 

      char buzzChar = tempProg.steps[x - 1].buzz ? 'R' : 'S';
      String timeString = formatTime(nonLinearTime(tempProg.steps[x - 1].timer));

      sprintf(lineString, "%02u: %c %3u%% %s", x, buzzChar, tempProg.steps[x - 1].power, timeString.c_str());

      lcd.setCursor(0, row);
      lcd.print(lineString);

    }                           
      
    row++;
  }
}
void programEditLoop(int programIndex){
  // The program index is the displayed number. So, the program stored at progLocations[0] has programIndex = 1.
  // Program.index also starts at 1
  int hind = 0; // horizontal index
  int vind = 0; // vertical index
  int sind = 0; // screen index, this is the index of the line that shows up as the first row
  
  int flag = 0;

  Program tempProg;
  tempProg = EEPROM.get(progLocations[programIndex-1], tempProg); //Load program into temp

  updateEditDisplay(tempProg, 0);
  lcd.setCursor(0,0);
  lcd.blink_on();
 
  while (true){

    verticalScrolling:
    
    flag = flagChecker(); //checks for user input
    delay(20);

    byte mind = tempProg.numSteps + 2; //maximum vertical index
    

//Switch for navigating vertically

    switch (flag) {
      case 0:
        continue;
      case -1: //cursor down
        if (vind == mind) continue;
        vind++;
        if (vind > sind + 3) sind++;
        break;
      case 1: //cursor up
        if (vind == 0) continue;
        vind--;
        if (vind < sind) sind = vind;
        break;
      case 2:
        //We're pressing the button on a menu item
        if (vind == 0){
          //return selected, save and exit
          EEPROM.put(progLocations[tempProg.index-1], tempProg);

          return;
        }
        else if (vind == tempProg.numSteps + 1){
          //add selected, add another step
          if (tempProg.numSteps < 30) tempProg.numSteps++;
        }
        else if (vind == tempProg.numSteps + 2){
          //remove selected, remove a step and scroll up one
          if (tempProg.numSteps > 1) {
            tempProg.numSteps--;
            sind--;
            vind--;
          }
        }
        else {
          // Step selected. Move into step and cycle between parameters.
          int flag2 = 0;
          hind = 0;
          const byte hpos[4] = {2,4,9,17};
          byte vpos = vind-sind; //vpos is for tracking vertical position on screen, ranges 0-3

          while (true) {
            horizontalScrolling:
            
            lcd.setCursor(hpos[hind], vpos);
            lcd.blink_on();
            

            flag2 = flagChecker();
            delay(20);

            switch (flag2){
              case 0:
                continue;
              case 1:
                if (hind < 3) hind++;
                break;
              case -1:
                if (hind > 0) hind--;
                break;
              case 2:
                if (hind == 0){
                  goto verticalScrolling;
                }
                else {
                  //Third nested loop!!!!! Whooo, we're getting crazy!

                  int flag3 = 0;

                  while (true) {
                    
                    flag3 = flagChecker();
                    

                    switch (flag3) {
                      case 0:
                        continue;
                      case 2:
                        goto horizontalScrolling;
                        break;
                      case 1:
                        if (hind == 1){
                          tempProg.steps[vind - 1].buzz = !tempProg.steps[vind - 1].buzz;
                        }
                        else if (hind == 2){
                          int power = tempProg.steps[vind - 1].power + 10;
                          power = constrain(power, 0, 100);
                          tempProg.steps[vind - 1].power = power;
                        }
                        else if (hind == 3) {
                          int time = tempProg.steps[vind - 1].timer + 5;
                          time = constrain(time, 0, 255);
                          tempProg.steps[vind - 1].timer = time;
                        }
                        break;
                      case -1:
                        if (hind == 1){
                          tempProg.steps[vind - 1].buzz = !tempProg.steps[vind - 1].buzz;
                        }
                        else if (hind == 2){
                          int power = tempProg.steps[vind - 1].power - 9;
                          power = constrain(power, 0, 100);
                          tempProg.steps[vind - 1].power = power;
                        }
                        else if (hind == 3) {
                          int time = tempProg.steps[vind - 1].timer - 1;
                          time = constrain(time, 0, 255);
                          tempProg.steps[vind - 1].timer = time;
                        }
                        break;
                    }

                    lcd.blink_off();
                    updateEditDisplay(tempProg, sind);
                    lcd.setCursor(hpos[hind],vpos);
                    lcd.blink_on();

                  }
                }
                break;
            }
          }
        }
        break;
    }

    lcd.blink_off();
    updateEditDisplay(tempProg, sind);
    lcd.blink_on();
    lcd.setCursor(0, vind-sind);

  }
}


volatile bool screenUpdate;

void screenUpdateFlag(){
  screenUpdate = true;
}



void programRunLoop(byte programIndex){
  //Load Program
  Program runningProgram;
  EEPROM.get(progLocations[programIndex - 1], runningProgram);

  //Calculate program duration
  //Steps are displayed 1-30 and stored 0-29
  unsigned long programDuration = 0;
  for (int x = 0; x < runningProgram.numSteps; x++){
    programDuration += nonLinearTime(runningProgram.steps[x].timer);
  }


//Initialize display///////////////////////////////////////////////////////////
  char runningDisplay[4][21];

  sprintf(runningDisplay[0], "Prog %i in progress", runningProgram.index); 
  sprintf(runningDisplay[1], "Step %02u of %02u", 1, runningProgram.numSteps);
  sprintf(runningDisplay[2], "%s|%s", formatTime(nonLinearTime(runningProgram.steps[0].timer)).c_str(), formatTime(programDuration).c_str());
  sprintf(runningDisplay[3], "Heat %3u%%", runningProgram.steps[0].power);

  for (int x = 0; x<4; x++){
    lcd.setCursor(0,x);
    lcd.print(runningDisplay[x]);
  }
/////////////////////////////////////////////////////////////////////////////////
//Loop for going through steps

  int flag = 0;  //Used for receiving button press to abandon program.. Maybe ask for two button presses?
  long progStartTime = millis();
  
  //Steps are stored 0-29 and printed as 1-30. We will work from the storage perspective.
  for (int currentStep = 0; currentStep < runningProgram.numSteps; currentStep++){
  
    updatePower(runningProgram.steps[currentStep].power, runningProgram.steps[currentStep].buzz); //Update the stove's power

    long stepStartTime = millis();

    while (true){

      //check for breaks
      flag = flagChecker;
      if (flag == 2){
        updatePower(0);
        return;
      }

      //Compute step time so far and check for program advance
      long stepTime = millis() - stepStartTime;
  
      long advanceTime = timeOfAdvance(runningProgram, currentStep); //in seconds
      long programTime = (millis() - progStartTime); //in milliseconds
      long remainingProgramTime = (programDuration * 1000 - programTime); //in milliseconds
    
      if (ceilDiv(programTime, 1000) > advanceTime){
        updatePower(0);
        break;
      }

      if (remainingProgramTime < 100) {
        updatePower(0);
        return;
      }

      //Update screen step, timers, and heat
      sprintf(runningDisplay[1], "Step %02u of %02u", currentStep + 1, runningProgram.numSteps);
      sprintf(runningDisplay[2], "%s|%s", formatTime(nonLinearTime(runningProgram.steps[currentStep].timer) - stepTime / 1000).c_str(), formatTime(ceilDiv(remainingProgramTime, 1000)).c_str());
      sprintf(runningDisplay[3], "Heat %03u%%", runningProgram.steps[currentStep].power);

      for (int x = 1; x<4; x++){
        lcd.setCursor(0,x);
        lcd.print(runningDisplay[x]);
      }

      delay(50);

    }


  }

}

unsigned long nonLinearTime(byte compressedTime){ //This is my silly way of having fewer big numbers
  unsigned long result = 0;

  for(int i = 1; i <= compressedTime + 1; i++){
    result++;
    if (i > 10) result++;
    if (i > 15) result += 2;
    if (i > 20) result++;
    if (i > 24) result += 5;
    if (i > 36) result += 10;
    if (i > 57) result += 10;
    if (i > 97) result += 30;
    if (i > 127) result += 240;
    if (i > 175) result += 300;
    if (i > 217) result += 600;
    if (i > 229) result += 2400;
    if (i > 235) result += 3600;
    if (i > 240) result += 7200*5;
  }
  return result;
}

String formatTime(unsigned long totalSeconds) {
    //negative input bug fix
    if (totalSeconds < 1) return String("000:00:00");

    // Calculate hours, minutes, and seconds
    unsigned int hours = totalSeconds / 3600;
    unsigned int minutes = (totalSeconds % 3600) / 60;
    unsigned int seconds = totalSeconds % 60;

    // Format the time into a string "xxx:xx:xx"
    char timeString[10]; // "xxx:xx:xx" + null terminator
    sprintf(timeString, "%03u:%02u:%02u", hours, minutes, seconds);

    return String(timeString);
}

int ceilDiv(int numerator, int denominator) {
    int result = numerator / denominator;
    // If there's a remainder, increment the result to round up
    if (numerator % denominator != 0) {
        result++;
    }
    return result;
}

int timeOfAdvance(Program runningProgram, int currentStep){
//Takes the program and the current step and returns the quantity of time between the
//program start and the next step advance
int result = 0;

for (int x = 0; x <= currentStep; x++){
  result += nonLinearTime(runningProgram.steps[x].timer);
}

return result;

}