/*
* SSUP (Super Spiker Ultra Plus)
* Copyright (C) 2024-2026  Gabriel Weingardt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License,
* or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
 * For further information, see <https://weingardt.dev/ssup/>
 * There you'll find documentation and how to update the device.
 */

#include <Wire.h>
#include <EEPROM.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "assets.h"

/* Screen dimension defines */
#define IMAGEX 128
#define IMAGEY 32
#define TEXTX 21
#define TEXTY 4

Adafruit_SSD1306 display(IMAGEX, IMAGEY, &Wire, -1);

const char PROGMEM HELP[] = \
"--- SSUP Help---\n\
Please see https://weingardt.dev/ssup for more info\n\
There you'll also find a manual and instructions\n\n\
g = Print System Info\n\
r = Reload Standart Configuration\n\
s = Enter Data Transfer Mode\n\
x = Exit Data Transfer Mode\n\
f = TOC Clear\n\
c = Clear EEPROM\n\
p = Print Page Content\n\
t = Print TOC Content\n\
h = Help\n\
u = Unlock Device\n\
l = Lock Device";



/* Serial BAUD rate */
#define BAUD 9600

/* EEPROM addresses for settings */
#define E_PAGE 0
#define E_CONTRAST 1
#define E_LOCK 2
#define E_EEPROM_SIZE 3
#define E_TOC_SIZE 4
#define E_SCREEN_ADDR 5
#define E_EEPROM_ADDR 6
#define E_THRESHHOLD 7

/* Calculator button pins */
#define ROW 3
#define D1 A3
#define D2 A2
#define D3 A0
#define D4 A1

#define LEFT 0b1000
#define SUPER 0b0100
#define TOGGLE 0b0010
#define RIGHT 0b0001

/* Serial flow control values and device info */
#define WAIT 0xFA
#define RELEASE 0xFB
#define MODEL 1
#define FIRMWARE 4

/* Setting variables */
uint8_t S_EEPROM_Address;
uint8_t S_SCREEN_Address;
uint16_t S_EEPROM_Size;
uint16_t S_TOC_SIZE;
uint8_t S_THRESHHOLD;

/* Variables */
uint8_t currentIndex = 0;
uint8_t currentPress = 4;
uint8_t fetchTrack = 0;
bool contrast = true;
bool locked = false;
bool dataTransferMode = false;
bool displayOn = true;
bool nextPage = false;

void setup() {
  /* Begin serial and I2C wire for EEPROM */
  Serial.begin(BAUD);
  Wire.begin();
  Wire.setClock(400000);

  /* Init settings */
  loadSettings();

  /* Say hello and lock status */
  Serial.println("#SSUP\nType 'h' for help");
  if (locked) Serial.println("!SSUP LOCK\n#This device is locked! Send 'u' to unlock it");

  /* Init calculator scan pins */
  pinMode(D1, INPUT_PULLUP);
  pinMode(D2, INPUT_PULLUP);
  pinMode(D3, INPUT_PULLUP);
  pinMode(D4, INPUT_PULLUP);
  pinMode(ROW, OUTPUT);

  /* Init display
    * Note: If less than ~768 bytes of dynamic memory is avaiable,
   * the display will fail to init. The screen library needs 1k 
   * of dynamic memory during runtime! 
   */
  if (!display.begin(SSD1306_SWITCHCAPVCC, S_SCREEN_Address)) {
    Serial.print("!DISPLAYBAD: ");
    Serial.println(display.getWriteError());
    Serial.println("#See manual");
  }

  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.cp437(true);

  /* Print welcome screen */
  display.clearDisplay();
  display.setCursor(38, 12);
  display.write("@");
  display.print(BAUD);
  display.write(" Baud");

  scanButtons();
  if ((currentPress & RIGHT) != 0) {
    //display.fillRect(102, 12, 16, 16, SSD1306_BLACK);
    display.drawBitmap(0, 0, LOGO, 128, 32, 1);
    //display.drawBitmap(102, 12, WHAT, 16, 16, 1);
    display.display();
    delay(4000);
    if (currentPress == (RIGHT | LEFT))
      for (;;);
  } else if ((currentPress & LEFT) != 0) {

    /* Restore settings on left press */
    display.setCursor(0, 0);
    display.print("Loading Default Settings");
    resetSettings();
  }

  display.display();
  Wire.begin();

  delay(6000);

  /* Display first page */
  currentIndex = 0;
  displayPage();
}

void loop() {
  scanButtons();

  static float datCount = 0;
  static uint8_t recieved = 0;

  /* Look while data is in seral buffer */
  while (Serial.available()) {
    static bool indexMode = false;
    static bool contentMode = false;
    static bool image = false;

    static uint8_t newIndex;
    static uint16_t startAddress;
    static uint16_t endAddress;

    /* Get recieved data */
    recieved = (uint8_t)Serial.read();

    datCount += (recieved / 100.f);

    if (contentMode) {
      /* If in content mode (data upload mode) */

      /* Stop on character ']' */
      if (recieved == ']') {
        Serial.println("!DONE");
        contentMode = false;

      } else {
        /* Else, interpret the send data */
        static int count = 0;
        count++;

        /* Send '.' every 8 counts for the master to flow control data */
        if (count > 8) {
          count = 0;
          Serial.print(".");
        }

        if (image) {
          /* For the image, decode 2 byte ASCII HEX into a byte */
          static uint8_t num = 0;
          /* Stop digging byte on ';' */
          if (recieved == ';') {
            writeEEPROM(startAddress++, num);
            num = 0;

          } else if (isalnum(recieved)) {
            /* Fetch ASCII hex digit (0..F) into a nibble and construct the byte */
            recieved = tolower(recieved);
            uint8_t fetch = ((recieved - ((recieved >= '0' && recieved <= '9') ? '0' : 'a')) & 0xF);
            
            /* First, shift previous fetch and then OR the new nibble */
            num = num << 4;
            num |= fetch;
          }

          /* Else, just write the ASCII data */
        } else if (recieved >= 0x20) writeEEPROM(startAddress++, recieved);

        /* On overflow, request temporary pause from master */
        if (startAddress > endAddress) Serial.println("!");
      }


    } else if (indexMode) {
      /* Index mode fetches the allocation mode for the next entry */
      if (recieved == ';') {
        /* If ';' was recieved, alloc entry and reset index mode, now comes the data */
        indexMode = false;
        contentMode = true;
        newIndex = allocPage(image);
        startAddress = (getPageAddress(newIndex) & 0x7FFF);
        endAddress = (startAddress + getPageSize(newIndex));

      } else image = (recieved != 't');  // Determine image or text

    } else {
      /* Interpret the recieved serial characters */

      switch (recieved) {
        case 'g':
          /* Print system info to serial */
          printSystemInfo();
          break;
        case 'r':
          /* Load default values and reset */
          Serial.println("!RESETTING");
          resetSettings();
          setup();
          break;
        case 's':
          /* Set data transfer mode */
          dataTransferMode = true;
          break;
        case 'x':
          /* Exit data transfer mode */
          dataTransferMode = false;
          break;
        case 'f':
          /* Clear fast, only TOC */
          clearTOC();
          break;
        case 'c':
          /* Clear entire EEPROM */
          clearEEPROM();
          break;
        case 'p':
          /* Print all page content to serial */
          for (int i = 0; i < 64; i++) {
            uint16_t addr = getPageAddress(i);
            if (addr != 0) printPageData(i);
          }
          break;
        case 't':
          /* Print TOC entries (addresses and size) */
          for (int i = 0; i < 64; i++) {
            printTOC(i);
          }
          break;
        case 'h':
          /* Print help dialog */
          Serial.println(HELP);
          break;
        case 'u':
          /* Unlock device */
          EEPROM.write(E_LOCK, 0);
          Serial.println("!UNLOCKED");
          setup();
        case 'l':
          /* Lock device */
          EEPROM.write(E_LOCK, 1);
          Serial.println("!LOCKED");
          break;
        case '[':
          /* Start index mode (new page entry) */
          indexMode = true;
          break;
        default:
          Serial.println("?");
          break;
      }
    }
  }

  if (dataTransferMode) {
    /* If in data transfer mode, show data transfer screen animation */
    static float rotCount = 0;

    display.clearDisplay();

    display.setCursor(0, 0);
    display.print("Transfer mode\nHold D2+D3 to \nforcefully abort");

    /* Nice little animation */
    display.drawLine(108, 16, 108 + (cosf(rotCount) * 10), 16 + (sinf(rotCount) * 10), SSD1306_WHITE);
    display.drawLine(108, 16, 108 + (sinf(datCount + rotCount) * 10), 16 + (cosf(datCount + rotCount) * 10), SSD1306_WHITE);

    display.display();
    //delay(3);

    rotCount += 0.125;

    scanButtons();
    if (currentPress == (SUPER | TOGGLE)) dataTransferMode = false;

  } else {
    /* If not in data transfer mode, we are in display/normal mode */

    bool wait = true;
    /* Test the current key press */
    switch (currentPress) {
      case TOGGLE:
        /* Toggle, toggles the display on/off state */
        displayOn = !displayOn;
        display.ssd1306_command(displayOn ? SSD1306_DISPLAYON : SSD1306_DISPLAYOFF);
        delay(200);
        break;

      case LEFT:
        /* Left, decrements the page */
        incrementPage(false);
        break;

      case RIGHT:
        /* Right, increments the page */
        incrementPage(true);
        break;

      case SUPER | TOGGLE:
        /* Super+Toggle toggles the display contrast */
        contrast = !contrast;
        display.ssd1306_command(SSD1306_SETCONTRAST);
        display.ssd1306_command(contrast ? 255 : 0);
        delay(200);

        break;

      case SUPER | RIGHT:
        /* Super+Right will lock the calculator */
        bool abort = false;
        for (int i = 5; i >= 0; i--) {
          display.clearDisplay();
          display.setCursor(0, 0);
          display.print("Device will be locked\nHold D1 to abort!\nHold D4 to lock NOW\n -> Locking in:  ");
          display.print(i);
          display.display();
          delay(1000);

          /* On left press abort, on right press skip counter */
          scanButtons();
          if (currentPress == LEFT) {
            abort = true;
            break;
          } else if (currentPress == RIGHT) break;
        }

        /* Write lock and reset */
        if (!abort) {
          EEPROM.write(E_LOCK, 1);
          setup();
        }

      case SUPER | LEFT:
        /* Super+Left resets the index to 0 */
        currentIndex = 0;
        displayPage();
        break;

      default:
        wait = false;
        break;
    }
    if (wait && currentPress != 0) delay(100);
  }
}

void writeEEPROM(uint16_t address, char writebyte) {
  /* Write the given value to EEPROM from desired address */

  Wire.beginTransmission(S_EEPROM_Address);
  Wire.write((byte)(address >> 8));
  Wire.write((byte)(address & 0xFF));
  Wire.write(writebyte);

  Wire.endTransmission();
  delay(2);  // wait to complete the write cycle
}

int readEEPROM(uint16_t address) {
  /* Read one byte from EEPROM from desired address */

  Wire.beginTransmission(S_EEPROM_Address);
  Wire.write((byte)(address >> 8));
  Wire.write((byte)(address & 0xFF));
  Wire.endTransmission();
  Wire.requestFrom(S_EEPROM_Address, (uint8_t)1);
  return Wire.read();
}

void clearEEPROM() {
  /* Clearing the entire EEPROM (fill with zeros) */

  Serial.print("!EEPROM CLEAR ");
  Serial.print((S_EEPROM_Size / 1024) + 1);
  Serial.println("k\n# '.' = 1k");

  for (uint32_t i = 0; i < S_EEPROM_Size; i++) {
    writeEEPROM(i, 0);

    /* For every 256 bytes, refresh display */
    if ((i & 0xFF) == 0) {
      display.clearDisplay();
      display.setCursor(0, 0);

      display.print("Clearing EEPROM ");
      display.print((S_EEPROM_Size / 1024) + 1);
      display.println('k');
      display.print(String(i / 1024));
      display.println("KB cleared");
      display.print((uint8_t)(((float)i / S_EEPROM_Size) * 100));
      display.print("% done    ");
      display.println(i < S_TOC_SIZE ? "(TOC)" : "(Data)");
      display.println("Hold D4 to abort");

      display.display();

      /* If RIGHT was pressed, abort */
      scanButtons();
      if (currentPress == RIGHT) break;

      /* Print status */
      Serial.print(".");
    }
  }

  Serial.println("\n!EEPROM CLEAR DONE");
  displayPage();
}

void displayPage() {
  /* Display page at current index */
  display.clearDisplay();
  display.setCursor(0, 0);

  /* Get TOC address and size */
  uint16_t start = (getPageAddress(currentIndex) & 0x7FFF);
  uint16_t end = start + getPageSize(currentIndex);

  /* If text mode, just print the characters */
  if ((getPageAddress(currentIndex) & 0x8000) == 0) {
    for (int16_t i = start; i < end; i++) {
      display.print((char)readEEPROM(i));
    }
  } else {
    /* If it is an image, read bytes and put them into a line buffer and print them out */
    uint8_t buffer[(IMAGEX / 8)];
    uint8_t y = 0;
    uint8_t c = 0;

    for (uint16_t i = start; i < end; i++) {
      /* Fill buffer with values */
      buffer[c++] = readEEPROM(i);

      if (c >= (IMAGEX / 8)) {
        /* Draw a 1 line bitmap from the buffer to the S_SCREEN_Address
         * This draws the image line by line, without needing a big buffer
         */
        display.drawBitmap(0, y++, buffer, IMAGEX - 1, 1, 1);
        c = 0;

        scanButtons();
        if ((currentPress & (LEFT | RIGHT)) != 0) {
          nextPage = true;
          break;
        }
      }

      /* For every 8 lines, print the page */
      if ((y % 8) == 0) display.display();
    }
  }

  display.display();
}

void scanButtons() {
  /* Performs a scan on the calculator button matrix */

  /* Set the ROW pin to OUTPUT and set it LOW */
  pinMode(ROW, OUTPUT);
  digitalWrite(ROW, LOW);

  /* Wait a bit */
  delay(3);

  currentPress = 0;

  /* Read inputs, if analog read exceeds the threshhold, 
   * OR the pressed button to currentPress 
   */
  if (analogRead(D1) < S_THRESHHOLD) currentPress |= LEFT;
  if (analogRead(D2) < S_THRESHHOLD) currentPress |= SUPER;
  if (analogRead(D3) < S_THRESHHOLD) currentPress |= TOGGLE;
  if (analogRead(D4) < S_THRESHHOLD) currentPress |= RIGHT;

  /* Set ROW to INPUT, this set's the pin in tri state mode */
  pinMode(ROW, INPUT);
}

void incrementPage(bool increment) {
  /* Increments or decrements the page */

  /* Dont't modify, if index is 0 or 255 or if next TOC entry is empty */
  if (increment && currentIndex == 255) return;
  else if (!increment && currentIndex == 0) return;
  else if (increment && (getPageAddress((currentIndex + 1)) & 0x7FFF) == 0) return;

  currentIndex += (increment ? 1 : -1);

  nextPage = false;
  displayPage();
  if (nextPage) incrementPage(increment);
}

void printPageData(uint8_t pageIndex) {
  /* Print the page data at the requested index to serial */

  int i = getPageAddress(pageIndex) & 0x7FFF;
  int to = i + getPageSize(pageIndex);

  bool graphicsMode = ((getPageAddress(pageIndex) & 0x8000) != 0);

  Serial.print("!DATA ");
  Serial.println(graphicsMode ? "IMAGE:" : "TEXT:");

  for (; i < to; i++) {
    uint8_t val = readEEPROM(i);

    if (graphicsMode) {
      Serial.print(numToHex(val));
      Serial.print(numToHex(val >> 4));
      Serial.print(';');
    } else {
      Serial.print((char)val);
    }
  }

  Serial.println("\n!DONE");
}

void printTOC(int index) {
  /* Print TOC index information to serial */

  uint16_t addr = getPageAddress(index);
  uint16_t size = getPageSize(index);

  /* Don't print if address is empty */
  if (addr != 0) {
    Serial.print("!TOC INFO: ");
    Serial.print(index);
    Serial.print("\tADDR: ");
    Serial.print((addr & 0x7FFF));
    Serial.print("\tSIZE: ");
    Serial.print(size);
    Serial.print((addr & 0x8000) ? "\tIMAGE" : "\tTEXT");
    Serial.println();
  }
}

char numToHex(uint8_t val) {
  /* Convert the lower section of a byte to a hex char */

  val &= 0xF;
  return (val < 0xA) ? ('0' + val) : ('A' + (val - 0xA));
}

uint8_t allocPage(bool image) {
  /* Allocate new Memory Block returning the allocated Index
   * Input specifies the allocation size. A Text Entry (image = false)
   * will allocate 84 bytes, while a Image (image = true) will use 512 bytes
   */

  /* Get free index */
  uint8_t newIndex = findNewTOCIndex();
  uint16_t resultAddress = 0xFFFF;

  /* If the index was 0 (first TOC entry), use the address, after the TOC size
   * Addresses after the TOC size is the usable data
   */
  
  Serial.println("!TOC ALLOC");
  if (newIndex == 0) resultAddress = S_TOC_SIZE;
  else {
    /* If not, search for a suitable address based of TOC Entries */
    uint16_t lastAddress = 0;
    uint16_t end = 0;

    for (int i = 0; i < S_TOC_SIZE / 2; i++) {
      lastAddress = end;
      uint16_t addrs = getPageAddress(i);
      end = (addrs & 0x7FFF) + getPageSize(i);

      if (i != 0 && (addrs & 0x7FFF) == 0) {

        /* If we reached end of TOC, break */
        resultAddress = lastAddress;
        Serial.println("!TOC FULL");
        break;
      }
    }
  }

  /* Update TOC index to found address and set the image/text bit */
  setPageAddress(newIndex, (resultAddress | (image ? 0x8000 : 0)));

  /* Randomizing of new allocated space (optional) */
  /*
  uint16_t end = (getPageSize(newIndex) + resultAddress);
  for (int i = resultAddress; i < end; i++) {
    if (image) writeEEPROM(i, random(255));
    else writeEEPROM(i, random(20, 127));
  }
*/
  Serial.print("!TOC INDEX ");
  Serial.print(newIndex);
  Serial.print(" IS ADDR ");
  Serial.println(resultAddress);

  return newIndex;
}

uint8_t findNewTOCIndex() {
  /* Get next free TOC Index */

  Serial.println("!TOC SCAN");

  int foundIndex = 0;
  for (; foundIndex < S_TOC_SIZE / 2; foundIndex++) {
    uint16_t addrs = getPageAddress(foundIndex);
    if (addrs == 0) break;
  }

  Serial.println("!TOC FOUND INDEX" + String(foundIndex));
  setPageAddress(foundIndex, 0);

  return foundIndex;
}

uint16_t getPageSize(uint8_t pageIndex) {
  /* Get the TOC page size from TOC index
   * Here, the most significant bit of the address byte
   * is used to determine the allocated space.
   * For text, it's just TEXTX * TEXTY (for 21x4 Text = 84 bytes), 
   * for an image it's (IMAGEX / 8) * IMAGEY, because 1 byte can hold 8
   * horizontal pixels (for 128x32 Image = 512 bytes)
   */

  return ((getPageAddress(pageIndex) & 0x8000) == 0 ? (TEXTX * TEXTY) : ((IMAGEX / 8) * IMAGEY));
}

uint16_t getPageAddress(uint8_t pageIndex) {
  /* Get the page address from the TOC index */

  uint16_t retAddrs = readEEPROM(pageIndex * 2);
  retAddrs |= (readEEPROM((pageIndex * 2) + 1) << 8);
  return retAddrs;
}

void setPageAddress(uint8_t pageIndex, uint16_t address) {
  /* Write the given Address to the desired TOC Location */

  writeEEPROM(pageIndex * 2, (byte)(address & 0xFF));
  writeEEPROM((pageIndex * 2) + 1, (byte)(address >> 8));
}

void clearTOC() {
  /* Clear table of contents (TOC)
   * This is a quick erase, deleting just the address entries
   */

  Serial.println("!TOC CLEAR");
  for (int i = 0; i < S_TOC_SIZE; i++) {
    writeEEPROM(i, 0);
  }
  Serial.println("!TOC CLEAR DONE");
}

void printSystemInfo() {
  /* Print system info as JSON to serial */

  Serial.print("!SYSINFO:{");
  Serial.print("\"FIRMWARE\":" + String(FIRMWARE));
  Serial.print(",\"MODEL\":" + String(MODEL));
  Serial.print(",\"TEXTX\":" + String(TEXTX));
  Serial.print(",\"TEXTY\":" + String(TEXTY));
  Serial.print(",\"IMAGEX\":" + String(IMAGEX));
  Serial.print(",\"IMAGEY\":" + String(IMAGEY));

  Serial.println("}");
  getSettings();
}

void loadSettings() {
  /* Loads configuration from EEPROM */

  S_EEPROM_Address = EEPROM.read(E_EEPROM_ADDR);
  S_SCREEN_Address = EEPROM.read(E_SCREEN_ADDR);
  S_EEPROM_Size = (((uint16_t)EEPROM.read(E_EEPROM_SIZE)) * 1024) - 1;
  S_TOC_SIZE = ((uint16_t)EEPROM.read(E_TOC_SIZE)) * 64;
  S_THRESHHOLD = EEPROM.read(E_THRESHHOLD);
  currentIndex = EEPROM.read(E_PAGE);
  contrast = (bool)EEPROM.read(E_CONTRAST);
  locked = (bool)EEPROM.read(E_LOCK);
}

void getSettings() {
  /* Print settings in JSON format to serial */

  Serial.print("!SETTINGS:{");
  Serial.print("\"EEPROM_ADDR\":" + String(EEPROM.read(E_EEPROM_ADDR)));
  Serial.print(",\"SCREEN_ADDR\":" + String(EEPROM.read(E_SCREEN_ADDR)));
  Serial.print(",\"EEPROM_SIZE\":" + String(EEPROM.read(E_EEPROM_SIZE)));
  Serial.print(",\"TOC_SIZE\":" + String(EEPROM.read(E_TOC_SIZE)));
  Serial.print(",\"THRESHHOLD\":" + String(EEPROM.read(E_THRESHHOLD)));
  Serial.print(",\"PAGE\":" + String(EEPROM.read(E_PAGE)));
  Serial.print(",\"CONTRAST\":" + String(EEPROM.read(E_CONTRAST)));
  Serial.print(",\"LOCKED\":" + String(EEPROM.read(E_LOCK)));
  Serial.println("}");
}

void resetSettings() {
  /* Restore settings to default values */

  EEPROM.write(E_PAGE, 0);
  EEPROM.write(E_CONTRAST, 255);
  EEPROM.write(E_LOCK, 0);
  EEPROM.write(E_EEPROM_SIZE, 64);  // x * 1024 = EEPROM SIZE
  EEPROM.write(E_TOC_SIZE, 8);      // x * 64 = TOCSIZE
  EEPROM.write(E_EEPROM_ADDR, 0x50);
  EEPROM.write(E_SCREEN_ADDR, 0x3C);
  EEPROM.write(E_THRESHHOLD, 80);
}


/* Ein geschlossener Regenschirm ist ebenso elegant wie ein offener hässlich ist */
