#include <Wire.h>
#include <ErriezSerialTerminal.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

// Newline character '\r' or '\n'
char newlineChar = '\n';
char delimiterChar = ' ';
SerialTerminal term(newlineChar, delimiterChar);

// Code ref table
/*

E   - Error
I   - Info
N   - Not
&   - And
@   - At
INT - Initialization
UNK - Unknown
ADR - Address
DVC - Device
BUS - Bus
SCN - Scanning
FND - Found
RCV - Received
DTA - Data
PRC - Processing
TRG - Target
VAL - Value
SST - Subset
AOR - And or
NSP - Not Specified
WHL - Withholds
INI - Incorrect Initializer

ex: E(RCV DTA: TRG & VAL WHL INI & SST VAL NSP)
Means: Error(Received Data: Target and Value withholds Incorrect Initializer and Subset Value Not Specified)

CI! - Compiler info is about to print.
C - Compiler type
V - Compiler version
F - Compiled file
D - Compile date
T - Compile time
P - Compiler Platform
A - Compiler Architecture
CS - C Standard

CHCI - Check code input
CHCC - Check is code correct
CPAS - Code is correct
CFAL - Code is incorrect

MNUP - Menu Printer

*/

// Define the LCD address, width, and height
#define LCD_ADDRESS 0x27
#define LCD_COLUMNS 20
#define LCD_ROWS 4
bool hasLCD = false;

// Types.
typedef void (*CallbackFunction)();
typedef struct {
  const char* menuItem;
  CallbackFunction callback;
} menuItem;

// Functions.
void enableScooter();
void openTrunk();
void menuOption3();

// Helper functions.
void printMenu(menuItem menuItems[], int itemCount, int cItem);
void i2cScanBus();
bool i2cAdrExists(byte adr);

// Debug functions.
void unknownCommand(const char* command);
void cmdHelp();
void cmdManual();
void cmdEnterDebug();
void cmdFMem();
void kpadTest();
void srlMsgTest();

// Pin definitions
#define DIRSW_CLK 11  // Directional switch - Movement clock
#define DIRSW_DIR 12  // Directional switch - Direction of movement
#define DIRSW_BTN A0  // Directional switch - Pushed down
#define BACK_BTN 13   // Back button
#define SPKR_PIN A3

// Relays
#define RELAY_UTIL_POWER 10 // Misc power - Blinkers, Brake lights, etc.
#define RELAY_TRUNK_OPEN 9  // Opens the trunk.
#define RELAY_ENGINE_KILL 8 // Engine run relay.

// Status's
#define DIRSW_DIR_BCK LOW
#define DIRSW_DIR_FWD HIGH
#define DIRSW_BTN_DWN LOW
#define BACK_BTN_DOWN HIGH

// Set up the LCD
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);

// Main menu
menuItem mainMenu[] = {
  {"Enable Scooter", enableScooter},
  {"Open Trunk", openTrunk},
  {"Power Down", menuOption3},
  {"Another option ", menuOption3},
  {"Settings", menuOption3},
  {NULL, NULL}  // Sentinel value to mark the end of the menu
};

// Debug menu
menuItem debugMenu[] = {
  {"Test Relays", NULL},
  {"Test KPad", kpadTest},
  {"Hard Reset", NULL},
  {"System Info", NULL},
  {"Get SRL Msg", srlMsgTest},
  {NULL, NULL}  // Sentinel value to mark the end of the menu
};

// Define the keypad layout
const byte ROWS = 4; // Four rows
const byte COLS = 4; // Four columns
const char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

// Define the row and column pins
byte rowPins[ROWS] = {7, 6, 5, 4}; // Connect to the row pins of the keypad
byte colPins[COLS] = {3, 2, A2, A1}; // Connect to the column pins of the keypad

// Create the Keypad object
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// The correct code
const String correctCode = "BADCAB";
String enteredCode = "";
int failCount = 0;
const int maxFails = 3;

const byte zero[] = {
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000
};
const byte one[] = {
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000
};
const byte two[] = {
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000
};
const byte three[] = {
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100
};
const byte four[] = {
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110
};
const byte five[] = {
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

void lampTest() {
  // LCD Test
  for(int i=0; i<5+1; i++) {
    for(int y=0; y<LCD_ROWS; y++) {
      for(int x=0; x<LCD_COLUMNS; x++) {
        lcd.setCursor(x,y);
        lcd.write(i);
      }
    }
    delay(100);
  }
  delay(1000);
  lcd.clear();
}

void setup() {
  // Debug serial.
  Serial.begin(115200);

  // Wire clock.
  Wire.setClock(100000);

  // Setup inputs
  pinMode(DIRSW_CLK, INPUT);
  pinMode(DIRSW_DIR, INPUT);
  pinMode(DIRSW_BTN, INPUT_PULLUP);
  pinMode(BACK_BTN, INPUT);

  // Setup outputs.
  pinMode(SPKR_PIN, OUTPUT);
  pinMode(RELAY_UTIL_POWER, OUTPUT);
  pinMode(RELAY_TRUNK_OPEN, OUTPUT);
  pinMode(RELAY_ENGINE_KILL, OUTPUT);

  // Terminal handler
  term.setDefaultHandler(unknownCommand);

  // Terminal commands.
  term.addCommand("?", cmdHelp);
  term.addCommand("M", cmdManual);
  term.addCommand("D", cmdEnterDebug);
  term.addCommand("F", cmdFMem);

  // Power tone.
  tone(SPKR_PIN, 100, 80);
  delay(80);
  tone(SPKR_PIN, 400, 80);
  delay(80);
  tone(SPKR_PIN, 800, 80);

  // Scan full I2C Bus.
  i2cScanBus();

  // Check for required I2C Devices
  hasLCD = i2cAdrExists(LCD_ADDRESS);
  if(!hasLCD) {
    // Serial error.
    Serial.println(F("E(INT: \"Display\" N FND)"));

    // Error tone
    tone(SPKR_PIN, 300, 500);
    delay(1000);
    tone(SPKR_PIN, 300, 500);
    delay(1000);
  }

  // Initialize the LCD
  lcd.begin(LCD_COLUMNS, LCD_ROWS);
  lcd.createChar(0, zero);
  lcd.createChar(1, one);
  lcd.createChar(2, two);
  lcd.createChar(3, three);
  lcd.createChar(4, four);
  lcd.createChar(5, five);
  lcd.backlight();
  lampTest();

  // Make a detailed info.
  char* infoStr = getCompileInfo();

  // Print info
  lcd.setCursor(0,0);
  lcd.print(F("Digital Key System"));

  // More info
  lcd.setCursor(0,1);
  lcd.print(F("(C)2024 Dakotath"));
  lcd.setCursor(0,3);
  scrollMessage(3, "For Yamaha Zuma Scooter", 100, 20);
  scrollMessage(3, infoStr, 50, 20);
  free(infoStr);

  // Clear.
  lcd.clear();
  lcd.print(F("Enter security code:"));
}

// Main unlocked loop.
void loopUnlocked() {
  // Menu.
  int menuItemCount = sizeof(mainMenu) / sizeof(mainMenu[0]);
  int menuIndex = 0;
  printMenu(mainMenu, menuItemCount, menuIndex);
  while(true) {
    // Term Handler.
    term.readSerial();

    // Directional switch moved.
    if(digitalRead(DIRSW_CLK) == LOW) {
      tone(SPKR_PIN, 500, 20);
      if(digitalRead(DIRSW_DIR) == DIRSW_DIR_BCK) {
        menuIndex--;
      } else {
        menuIndex++;
      }

      // Display the menu 
      printMenu(mainMenu, menuItemCount, menuIndex);
    }

    // Selected menu entry
    if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
      // execute function
      mainMenu[menuIndex].callback();
    }
  }
}

void loop() {
  // Call the function to check the code and handle the result
  bool isCodeCorrect = checkCode();

  if (isCodeCorrect) {
    displayMessage(F("Yes Sir!"));
    delay(2000);
    loopUnlocked();
    reset();
  } else if (failCount >= maxFails) {
    displayMessage(F("Max Attempts Reached"));
    delay(2000);
    reset();
  }

  // Read from terminal.
  term.readSerial();
}

void updateProgressBar(unsigned long count, unsigned long totalCount, int lineToPrintOn) {
  double factor = totalCount/100.0;          
  int percent = (count+1)/factor;
  int number = percent/5;
  int remainder = percent%5;
  if(number > 0)
  {
    for(int j = 0; j < number; j++)
    {
      lcd.setCursor(j,lineToPrintOn);
      lcd.write(5);
    }
  }
  lcd.setCursor(number,lineToPrintOn);
  lcd.write(remainder);
  if(number < LCD_COLUMNS)
  {
    for(int j = number+1; j <= LCD_COLUMNS; j++)
    {
      lcd.setCursor(j,lineToPrintOn);
      lcd.write(0);
    }
  }
}

// Function to get the free memory available
int freeMemory() {
    extern int __heap_start;
    extern int *__brkval;
    int v;
    return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}

// Function to return compile-time information as a single line string
char* getCompileInfo(void) {
    // Define the maximum length of the output string
    const int maxLength = 120;
    char* info = malloc(maxLength);
    if (info == NULL) {
        return NULL; // Allocation failed
    }

    // Compiler Information
    const char* compiler = "unknown";
    #ifdef __GNUC__
        compiler = "GCC";
    #elif _MSC_VER
        compiler = "MSVC";
    #endif

    // Define a macro for the architecture
    const char* architecture = "?";
    #if defined(__x86_64__) || defined(_M_X64)
        architecture = "x86_64";
    #elif defined(__i386__) || defined(_M_IX86)
        architecture = "x86";
    #elif defined(__arm__) || defined(_M_ARM)
        architecture = "ARM";
    #elif defined(__avr32__) || defined(_M_AVR)
        architecture = "AVR";
    #elif defined(__aarch64__)
        architecture = "ARM64";
    #endif

    // Define the C Standard
    const char* cStandard = "?";
    #if __STDC_VERSION__ >= 201112L
        cStandard = "C11|>";
    #elif __STDC_VERSION__ >= 199901L
        cStandard = "C99";
    #else
        cStandard = "C89/C90";
    #endif

    // Determine platform
    const char* platform = "?";
    #ifdef _WIN32
        platform = "W32";
    #elif _WIN64
        platform = "W64";
    #elif __unix__
        platform = "UNX";
    #elif __APPLE__
        platform = "MOS";
    #elif __linux__
        platform = "LNX";
    #endif

    // Format the information into a single string
    snprintf(info, maxLength,
        "C: %s, V: %s, F: %s, D: %s, T: %s, P: %s, A: %s, CS: %s, FM: %dB",
        compiler,
        __VERSION__,
        __FILE__,
        __DATE__,
        __TIME__,
        platform,
        architecture,
        cStandard,
        freeMemory());
    
    // Write info to serial.
    Serial.println("CI!");
    Serial.println(info);

    return info;
}

// Sir, what would you like to eat?
void printMenu(menuItem menuItems[], int itemCount, int cItem) {
  lcd.clear(); // Clear the display at the beginning

  int maxLines = LCD_ROWS; // Number of lines on the LCD
  int startLine = (cItem < maxLines) ? 0 : (cItem - maxLines + 1); // Determine start line for scrolling

  // Ensure startLine does not exceed available items
  if (startLine + maxLines > itemCount) {
    startLine = itemCount - maxLines;
  }

  for (int i = startLine; i < startLine + maxLines && i < itemCount; i++) {
    bool isSelected = (cItem == i);
    
    // Format the menu entry with selection marker
    char menuEntry[17]; // Use a buffer size that fits within the LCD width
    snprintf(menuEntry, sizeof(menuEntry), "%c%s", isSelected ? '>' : ' ', menuItems[i].menuItem);
    
    // Set line on LCD
    lcd.setCursor(0, i - startLine); // Adjust the line index for scrolling
    lcd.print(menuEntry);

    // Debug
    char* dbgInf = malloc(40);
    sprintf(dbgInf, "MNUP: %s", menuEntry);
    Serial.println(dbgInf);
    free(dbgInf);
  }
  Serial.println();
}

// Function to display a message on the LCD and clear the screen
void displayMessage(const String &message) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(message);
}

bool checkCode() {
  char key = keypad.getKey();
  if (key) {
    // Blip.
    tone(SPKR_PIN, 620, 50);

    // Display the pressed key on the LCD
    lcd.setCursor(0, 1);
    lcd.print(F("Code: "));
    lcd.print(enteredCode);
    lcd.print(key);

    // Append the key to the entered code
    enteredCode += key;
    Serial.print(F("CHCI: "));
    Serial.println(enteredCode);

    // Check if the entered code matches the correct code
    if (enteredCode.length() >= correctCode.length()) {
      if (enteredCode.endsWith(correctCode)) {
        Serial.println();
        Serial.println(F("CHCC: CPAS"));

        // Ok tone
        delay(300);
        tone(SPKR_PIN, 700, 100);
        delay(100);
        tone(SPKR_PIN, 620, 50);
        delay(50);

        return true; // Code is correct
      } else if (enteredCode.length() > correctCode.length()) {
        // Code is incorrect
        failCount++;
        enteredCode = ""; // Clear entered code for next attempt
        displayMessage(F("Code Incorrect"));
        Serial.println(F("CHCC: CFAL"));
        delay(2000); // Show the message for 2 seconds
        resetSoft();
        return false;
      }
    }
  }

  return false; // Code is incorrect or not yet complete
}

// Function to scroll text on a specified line
void scrollMessage(int row, String message, int delayTime, int totalColumns) {
  for (int i=0; i < totalColumns; i++) {
    message = " " + message;  
  } 
  message = message + " "; 
  for (int position = 0; position < message.length(); position++) {
    updateProgressBar(position, message.length(), 2);
    lcd.setCursor(0, row);
    lcd.print(message.substring(position, position + totalColumns));
    term.readSerial();
    delay(delayTime);
  }
}

// Function to reset the code entry process
void reset() {
  enteredCode = "";
  failCount = 0;
  displayMessage(F("Enter Code:"));
}
void resetSoft() {
  enteredCode = "";
  displayMessage(F("Enter Code:"));
}

// Real shit.
void enableScooter() {
  lcd.clear();
  lcd.print(F("Enabling Scooter"));
  digitalWrite(RELAY_UTIL_POWER, HIGH);
  digitalWrite(RELAY_ENGINE_KILL, HIGH);
  delay(1000);
  loopUnlocked();
}

void openTrunk() {
  lcd.clear();
  lcd.print(F("Opening Trunk"));
  digitalWrite(RELAY_TRUNK_OPEN, HIGH);
  delay(200);
  digitalWrite(RELAY_TRUNK_OPEN, LOW);
  delay(1000);
  loopUnlocked();
}

void menuOption3() {
  //printf("Menu Option 2 selected.\n");
}

bool i2cAdrExists(byte adr) {
  byte error;

  Wire.beginTransmission(adr);
  error = Wire.endTransmission();

  if (error == 0)
  {
    return true;
  }
  else if (error==4)
  {
    Serial.print(F("E(I2C: UNK E @ ADR 0x"));
    if (adr<16) {
      Serial.print("0");
    }
    Serial.print(adr,HEX);
    Serial.println(")");
    return false;
  }

  return false;
}

// Scan the entire I2C Bus.
void i2cScanBus() {
  byte error, address;
  int nDevices;

  Serial.println("I(I2C: SCN BUS)");

  nDevices = 0;
  for(address = 1; address < 128; address++ )
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0)
    {
      Serial.print(F("I(I2C: DVC FND @ ADR 0x"));
      if (address<16) {
        Serial.print("0");
      }
      Serial.print(address,HEX);
      Serial.println(")");
      nDevices++;
    }
    else if (error==4)
    {
      Serial.print(F("E(I2C: UNK E @ ADR 0x"));
      if (address<16) {
        Serial.print("0");
      }
      Serial.print(address,HEX);
      Serial.println(")");
    }
  }

  if (nDevices == 0) {
    Serial.println("E(I2C: 0 DVC FND)");
  } else {
    Serial.print("I(I2c: ");
    Serial.print(nDevices);
    Serial.println(" DVC FND)");
  }
}

// Terminal.
void unknownCommand(const char *command)
{
    // Print unknown command
    Serial.print(F("Unknown command: "));
    Serial.println(command);
}

void cmdHelp() {
    Serial.println(F("YW50 Digital Key System:"));
    Serial.println(F("\t? - Help"));
}

// Manual code execution.
void cmdManual() {
  // Argument
  char* arg;

  // Device to target
  char *tDevice = NULL;
  int dState = 0;

  // Get target device.
  arg = term.getNext();
  if (arg != NULL) {
    tDevice = arg;
  } else {
    Serial.println(F("E: TRG NSP"));
    return;
  }

  // Get State to set.
  arg = term.getNext();
  if (arg != NULL) {
    // Is int?
    if(atoi(arg)) {
      dState = atoi(arg);
    } else {
      Serial.println(F("E(VAL INI)"));
      return;
    }
  } else {
    Serial.println(F("E(VAL NSP)"));
    return;
  }

  // Process info.
  char* dbgLne = malloc(100);
  sprintf(dbgLne, "TRG: %s, VAL: %d", tDevice, dState);
  Serial.println(dbgLne);
  free(dbgLne);

  /*
    Devices:
      DBLK  - Display Backlight
      SPIN  - Sets a pin value
      RPIN  - Reads a pin value
  */

  // Switch
  if(strcmp(tDevice, "DBLK") == 0) { // LCD Backlight
    switch(dState) {
      case 1:
        lcd.noBacklight();
        break;
      case 2:
        lcd.backlight();
        break;
      default:
        Serial.println(F("E(DTA PRC: VAL INI)"));
    }
  } else if(strcmp(tDevice, "SPIN") == 0) { // Set Pin Values
    // We need another argument for the pin value
    int pnState = LOW;
    arg = term.getNext();
    if (arg != NULL) {
      // Is int?
      if(atoi(arg)) {
        pnState = atoi(arg);
      } else {
        Serial.println(F("E(DTA PRC: SST VAL INI)"));
        return;
      }
    } else {
      Serial.println(F("E(DTA PRC: SST VAL NSP)"));
      return;
    }

    // Set it.
    digitalWrite(dState-1, pnState-1);
  } else if(strcmp(tDevice, "RPIN") == 0) { // Read a pin value
    // Pin value.
    int pVal = digitalRead(dState);
    Serial.print(F("VAL: "));
    Serial.println(pVal);
  } else {
    Serial.println(F("E(DTA PRC: TRG INI)"));
  }
}

// Show debug menu
void cmdEnterDebug() {
  // Debug Menu.
  int menuItemCount = sizeof(debugMenu) / sizeof(debugMenu[0]);
  int menuIndex = 0;
  printMenu(debugMenu, menuItemCount, menuIndex);

  // Alert tone
  tone(SPKR_PIN, 800, 40);
  delay(100);
  tone(SPKR_PIN, 800, 40);

  while(true) {
    // Term Handler.
    term.readSerial();

    // Directional switch moved.
    if(digitalRead(DIRSW_CLK) == LOW) {
      tone(SPKR_PIN, 700, 20);
      if(digitalRead(DIRSW_DIR) == DIRSW_DIR_BCK) {
        menuIndex--;
      } else {
        menuIndex++;
      }

      // Display the menu
      printMenu(debugMenu, menuItemCount, menuIndex);
    }

    // Selected menu entry
    if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
      // Wait for release.
      while(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
        // Nothing
      }

      // Blip.
      tone(SPKR_PIN, 900, 30);

      // execute function
      debugMenu[menuIndex].callback();

      // Display the menu again.
      printMenu(debugMenu, menuItemCount, menuIndex);
    }
  }
}

// Keypad test.
void kpadTest() {
  lcd.clear();
  lcd.print(F("Press Keys:"));
  lcd.setCursor(0, 3);
  lcd.print(F("'OK' to go back"));
  lcd.setCursor(0, 1);

  // Loop
  while(true) {
    char key = keypad.getKey();
    if (key) {
      // Display the pressed key on the LCD
      lcd.print(key);

      // Blip
      tone(SPKR_PIN, 750, 20);
    }

    // Go back
    if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
      // Wait for release.
      while(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
        // Nothing
      }
      return;
    }
  }
}

// Serial messages.
void srlMsgTest() {
  lcd.clear();

  // Loop.
  while(true) {
    if(Serial.available() > 1) {
      lcd.write(Serial.read());
    }

    // Go back
    if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
      // Wait for release.
      while(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
        // Nothing
      }
      return;
    }
  }
}

void cmdFMem() {
    Serial.println(F("Free Memory:"));
    Serial.println(freeMemory());
}
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module
GND5VSDASCLSQWRTCDS1307+