#include <Arduino.h>
#include <U8g2lib.h>
#include "stats_font1x1.h"
#include "stats_font2x2.h"
#include "glyphs.h"
#include "stats_dto.h"

#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif

#define DEBUG
#ifdef DEBUG
  #define D
#else
  #define D for(;0;)
#endif

#define SCROLL_SPEED 2
#define NO_DATA_TIMEOUT 5 * 1000

// I²C pins        SCL, SDA
#define DISP1_PINS 22,  21
#define DISP2_PINS 33,  32


#define USE_ESP32_I2C_HAL
#ifdef USE_ESP32_I2C_HAL
  #include "u8g2_esp32_hal.h"
  #include "u8g2_esp32_hal.c"

  class U8G2_CUSTOM_SSD1306_128X32_UNIVISION_F_ESP32_HAL_I2C : public U8G2 {
    public: U8G2_CUSTOM_SSD1306_128X32_UNIVISION_F_ESP32_HAL_I2C(const u8g2_cb_t *rotation, uint8_t clock, uint8_t data, uint8_t reset = U8X8_PIN_NONE, uint8_t i2c_port = U8X8_PIN_NONE) : U8G2() {
      u8g2_esp32_hal_t esp_hal = U8G2_ESP32_HAL_DEFAULT;
      esp_hal.sda = (gpio_num_t) data;
      esp_hal.scl = (gpio_num_t) clock;
      if (reset != U8X8_PIN_NONE)
        esp_hal.reset = (gpio_num_t) reset;
      if (i2c_port != U8X8_PIN_NONE)
        esp_hal.master_num = (i2c_port_t) i2c_port;

      u8g2_esp32_hal_init((u8x8_t *)(&u8g2), esp_hal);

      u8g2_Setup_ssd1306_i2c_128x32_univision_f(&u8g2, rotation, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
    }
    public: ~U8G2_CUSTOM_SSD1306_128X32_UNIVISION_F_ESP32_HAL_I2C() {
      u8g2_esp32_hal_free((u8x8_t *)(&u8g2));
    }
  };

  U8G2_CUSTOM_SSD1306_128X32_UNIVISION_F_ESP32_HAL_I2C u8g2_d1(U8G2_R0, DISP1_PINS, U8X8_PIN_NONE, 0);
  U8G2_CUSTOM_SSD1306_128X32_UNIVISION_F_ESP32_HAL_I2C u8g2_d2(U8G2_R0, DISP2_PINS, U8X8_PIN_NONE, 1);
#else
  U8G2_SSD1306_128X32_UNIVISION_F_SW_I2C u8g2_d1(U8G2_R0, DISP1_PINS, U8X8_PIN_NONE);
  U8G2_SSD1306_128X32_UNIVISION_F_SW_I2C u8g2_d2(U8G2_R0, DISP2_PINS, U8X8_PIN_NONE);
#endif


u8g2_uint_t f1x1h = 8;
u8g2_uint_t f2x2h = 16;

char tempLine0Format[] = GLYPH_AMD "%2d" GLYPH_DEGC " "
                   GLYPH_VR_MOS "%2d" GLYPH_DEGC " "
                   GLYPH_GPU "%2d" GLYPH_DEGC " "
                   GLYPH_CHIPSET "%2d" GLYPH_DEGC " "
                   GLYPH_MOBO "%2d" GLYPH_DEGC " "
                   GLYPH_SSD "%2d" GLYPH_DEGC " ";
                   
char tempLine1Format[] = GLYPH_AMD " %s" GLYPH_HZ " "
                   GLYPH_LOAD " %2d.%02d %4d%%";


char fansLine0Format[] = GLYPH_AIO "%4d" GLYPH_RPM " "
                   GLYPH_GPU "%4d" GLYPH_RPM " "
                   GLYPH_CHIPSET "%4d" GLYPH_RPM " "
                   GLYPH_FAN_TOP "%4d" GLYPH_RPM " "
                   GLYPH_FAN_BOTTOM "%4d" " "
                   GLYPH_FAN_BACK "%4d" GLYPH_RPM " ";

char fansLine1Format[] = GLYPH_RAM " %2d%%  %s  %4dM" GLYPH_HZ;

char waiting_string[] = "Waiting for data from the computer...    ";

u8g2_uint_t waiting_width;
u8g2_uint_t waiting_offset = 0;
u8g2_uint_t waiting_offset1;

u8g2_uint_t temp_page_offset = 0;
u8g2_uint_t fans_page_offset = 0;

struct line_buffers {
  char line0[300];
  char line1[30];
  char line2[30];
};

// Two buffers to swap around and avoid blocking rendering
struct line_buffers tempLines_1 = {0};
struct line_buffers tempLines_2 = {0};
struct line_buffers fansLines_1 = {0};
struct line_buffers fansLines_2 = {0};

struct line_buffers *tempLines = &tempLines_1;
struct line_buffers *tempLines_next = &tempLines_2;
struct line_buffers *fansLines = &fansLines_1;
struct line_buffers *fansLines_next = &fansLines_2;

SemaphoreHandle_t stringsMutex;

stats_t stats;

byte bytes_read = 0;
bool stats_ever_received = false;
unsigned long last_received = 0;


bool receiveStats() {
  size_t to_read = sizeof(stats_t) - bytes_read;
  
  if (Serial.available() < 4) return false;
  if (bytes_read == 0 && !Serial.find(MAGIC_START_ASSTR, 4)) {
    return false;
  } else if (bytes_read == 0) {
    // Put back magic since Serial.find() strips it out
    stats.magic_start = MAGIC_START;
    bytes_read = 4;
    // It also says it read them even though it doesn't place them in the buffer
    to_read += 4;
  }
  
  D Serial.println(F("Magic found, accepting data"));
  
  byte *buffer = (byte *) &stats;
  bytes_read += Serial.readBytes(buffer + bytes_read, to_read);

  D Serial.print(F("Read "));
  D Serial.print(bytes_read);
  D Serial.println(F(" bytes from serial"));

  if (bytes_read >= sizeof(stats_t)) {
#ifdef DEBUG
    char printbuf[100];
    sprintf(printbuf, "Received %d bytes, wanted %d == sizeof(stats_t)", bytes_read, sizeof(stats_t));
    Serial.println(printbuf);
    Serial.print(F("buffer: "));
    for (byte *ptr = buffer; ptr < buffer + sizeof(stats_t); ptr++) {
      sprintf(printbuf, "%02x ", *ptr);
      Serial.print(printbuf);
    }
    Serial.println();
    
    Serial.println(F("Check magic start"));
#endif
    if (stats.magic_start != MAGIC_START) goto invalidate;
    D Serial.println(F("Check magic end"));
    D Serial.println(stats.magic_end, HEX);
    if (stats.magic_end != MAGIC_END) goto invalidate;

    uint8_t checkxor = 0;
    for (byte *b = (byte *) &stats; b < &(stats.checkxor); b++) {
      checkxor ^= *b;
    }
    D Serial.println(F("Checking check xor"));
    D Serial.print(F("Computed: "));
    D Serial.print(checkxor, HEX);
    D Serial.print(F(" at offset "));
    D Serial.println((uint32_t) ((uint32_t) &(stats.checkxor) - ((uint32_t) &stats)));
    D Serial.print(F("Got: "));
    D Serial.println(stats.checkxor, HEX);
    
    if (stats.checkxor != checkxor) goto invalidate;

    D Serial.println(F("Checks passed, data accepted"));

    bytes_read = 0;
    return true;
  } else {
    return false;
  }
  
invalidate:
  D Serial.println(F("Invalid data, ignoring"));
  bytes_read = 0;
  return false;
}


void drawWaitingForData(U8G2 *u8g2, u8g2_uint_t *offset) {
  u8g2->clearBuffer();

  u8g2_uint_t x = *offset;
  u8g2->setFont(u8g2_font_helvR18_tf);
  do {
    u8g2->drawStr(x, 26, waiting_string);
    x += waiting_width;
  } while( x < u8g2->getDisplayWidth() );

  yield();
  u8g2->sendBuffer();

  (*offset) -= SCROLL_SPEED;
  if ( (u8g2_uint_t)(*offset) < (u8g2_uint_t)-waiting_width )  
    (*offset) = 0;
}

void drawWaitingForDataAllDisplays() {
  drawWaitingForData(&u8g2_d1, &waiting_offset);
  drawWaitingForData(&u8g2_d2, &waiting_offset1);
}

void format_freq(char *dest, uint16_t value) {
  if (value >= 1000) {
    sprintf(dest, "%4dM", value);
  } else {
    sprintf(dest, "%1d.%03dG", value / 1000, value % 1000);
  }
}

void format_bar(
  char *buffer, uint8_t total_tiles, uint16_t full_perc, uint16_t gray_perc, uint16_t perc_max) {
      
  memset(buffer, 0, total_tiles + 1);
  int32_t gray_tiles = (((int32_t) gray_perc) * 1024) / ((int32_t) perc_max) * total_tiles / 1024;
  int32_t full_tiles = (((int32_t) full_perc) * 1024) / ((int32_t) perc_max) * total_tiles / 1024;

  // Pick left butt tile
  if (full_tiles == 1 && gray_tiles == 0)
    strcat(buffer, GLYPH_BUTT_LEFT_FULL_END); 
  else if (full_tiles == 0 && gray_tiles == 1)
    strcat(buffer, GLYPH_BUTT_LEFT_GRAY_END);
  else if (full_tiles > 0)
    strcat(buffer, GLYPH_BUTT_LEFT_FULL); 
  else if (gray_tiles > 0)
    strcat(buffer, GLYPH_BUTT_LEFT_GRAY);
  else
    strcat(buffer, GLYPH_BUTT_LEFT_EMPTY);

  // Draw full tiles
  for (char i = 0; i < full_tiles - 2; i++)
    strcat(buffer, GLYPH_MID_FULL);
  
  if (full_tiles < total_tiles && gray_tiles == 0)
    strcat(buffer, GLYPH_MID_FULL_END);
  else if (gray_tiles > 0)
    strcat(buffer, GLYPH_MID_FULL);

  // Draw gray tiles
  for (char i = 0; i < full_tiles - 1 - (full_tiles > 0 ? 0 : 1); i++)
    strcat(buffer, GLYPH_MID_GRAY);
    
  if (gray_tiles < total_tiles)
    strcat(buffer, GLYPH_MID_GRAY_END);

  // Draw empty tiles
  for (char i = 0; i < total_tiles - 1 - gray_tiles - full_tiles; i++)
    strcat(buffer, GLYPH_MID_EMPTY);

  // Pick right butt tile
  if (full_tiles == total_tiles)
    strcat(buffer, GLYPH_BUTT_RIGHT_FULL); 
  else if (gray_tiles == total_tiles)
    strcat(buffer, GLYPH_BUTT_RIGHT_GRAY);
  else
    strcat(buffer, GLYPH_BUTT_RIGHT_EMPTY);
}


void format_size(char *buffer, uint32_t megabytes) {
  if (megabytes < 2000) {
    sprintf(buffer, "%4dMB", megabytes);
  } else {
    sprintf(buffer, "%4.2fGB", (float) megabytes / 1024);
  }
}


void drawStatsPage(
  U8G2 *u8g2, struct line_buffers *lines, char *minibanner,
  u8g2_uint_t mb_w, u8g2_uint_t mb_h, u8g2_uint_t *offset) {

  u8g2->setFont(stats_font2x2);
  u8g2_uint_t line0_width = u8g2->getStrWidth(lines->line0);
    
  u8g2->clearBuffer();

  // Line 0
  u8g2_uint_t x = *offset;
  u8g2->setFont(stats_font2x2);
  do {
    u8g2->drawStr(x, f2x2h, lines->line0);
    x += line0_width;
  } while( x < u8g2->getDisplayWidth() );

  // Minibanner
  u8g2->setDrawColor(0);
  u8g2->drawBox(0, 0, mb_w, mb_h);
  u8g2->setDrawColor(1);
  u8g2->drawStr(0, f2x2h, minibanner);

  // Line 1
  u8g2->setFont(stats_font1x1);
  u8g2->drawStr(0, f2x2h + f1x1h + 1, lines->line1);

  // Line 2
  u8g2->drawStr(0, f2x2h + 2*f1x1h, lines->line2);

  yield();

  u8g2->sendBuffer();
  
  (*offset) -= SCROLL_SPEED;
  if ( (u8g2_uint_t) (*offset) < (u8g2_uint_t) -line0_width )  
    (*offset) = 0;
}


void sprintfStrings() {
  uint8_t total_8x8_tiles = u8g2_d1.getDisplayWidth() / 8 - 3;
  char tmp_buf[20] = {0};

  // Temps page
  sprintf(tempLines_next->line0, tempLine0Format,
    stats.cpu_temp,
    stats.vr_mos_temp,
    stats.gpu_temp,
    stats.chipset_temp,
    stats.system_temp,
    stats.ssd_temp);

  format_freq(tmp_buf, stats.cpu_freq);
  sprintf(tempLines_next->line1, tempLine1Format,
    tmp_buf, stats.cpu_load_avg / 100, stats.cpu_load_avg % 100, stats.cpu_perc
  );

  format_bar(
    tempLines_next->line2, total_8x8_tiles,
    stats.cpu_perc - stats.cpu_perc_kernel, stats.cpu_perc_kernel, stats.cpu_perc_max);


  // Fans page
  sprintf(fansLines_next->line0, fansLine0Format,
    stats.pump,
    stats.gpu_fan,
    stats.chipset_fan,
    stats.top_fan,
    stats.bottom_fan,
    stats.back_fan);

  format_size(tmp_buf, stats.ram_used);
  sprintf(fansLines_next->line1, fansLine1Format, stats.ram_perc, tmp_buf, 3200);

  format_bar(
    fansLines_next->line2, total_8x8_tiles, stats.ram_perc, stats.ram_perc_buffers, 100);

  struct line_buffers *temp_ptr = tempLines;
  struct line_buffers *fans_ptr = fansLines;

  xSemaphoreTake(stringsMutex, portMAX_DELAY);
  tempLines = tempLines_next;
  fansLines = fansLines_next;
  xSemaphoreGive(stringsMutex);

  tempLines_next = temp_ptr;
  fansLines_next = fans_ptr;
}



void displaysTask(void *pvParameters) {
  Serial.print("Handling displays on core ");
  Serial.println(xPortGetCoreID());

  while (true) {    
    if (!stats_ever_received) {
      drawWaitingForDataAllDisplays();
      yield();
      continue;
    }

    xSemaphoreTake(stringsMutex, portMAX_DELAY);
    struct line_buffers *templines_ptr = tempLines;
    struct line_buffers *fanslines_ptr = fansLines;
    xSemaphoreGive(stringsMutex);

    drawStatsPage(&u8g2_d1, templines_ptr, GLYPH_TEMP, 10, 32, &temp_page_offset);
    drawStatsPage(&u8g2_d2, fanslines_ptr, GLYPH_FAN, 16, 32, &fans_page_offset);

    yield();
  }
}

void receiverTask(void *pvParameters) {
  Serial.print("Handling incoming data on core ");
  Serial.println(xPortGetCoreID());

  while (true) {
    yield();

    if (receiveStats()) {
      sprintfStrings();
      last_received = millis();
      stats_ever_received = true;
    }

    delay(10);
  }
}


void setup() {
  // Fuck this shit and fuck your "wifi" and "important stuff"
  disableCore0WDT();

  Serial.begin(115200);
  Serial.setTimeout(0);
  stringsMutex = xSemaphoreCreateMutex();

#ifdef USE_ESP32_I2C_HAL
  u8g2_d1.setI2CAddress(0x78);
  u8g2_d2.setI2CAddress(0x78);
#endif
  
  u8g2_d1.begin();
  u8g2_d1.setPowerSave(0);
  u8g2_d1.setFontMode(0);
  u8g2_d1.setAutoPageClear(0);

  u8g2_d1.setFont(u8g2_font_helvR18_tf);
  waiting_width = u8g2_d1.getStrWidth(waiting_string);
  waiting_offset = 0;

  u8g2_d2.begin();
  u8g2_d2.setPowerSave(0);
  u8g2_d2.setFontMode(0);
  u8g2_d2.setAutoPageClear(0);

  u8g2_d2.setFont(u8g2_font_helvR18_tf);
  waiting_offset1 = (u8g2_uint_t) -u8g2_d2.getDisplayWidth();

  delay(500);

  xTaskCreatePinnedToCore(displaysTask, "DisplaysTask", 10000, NULL, 4, NULL, 0);
  xTaskCreatePinnedToCore(receiverTask, "ReceiverTask", 10000, NULL, 4, NULL, 1);
}

void loop() {
  // Avoid task watchdog firing
  vTaskDelete(NULL);
}