#include <Arduino.h>
#include <Adafruit_SCD30.h>
#include <Adafruit_BME280.h>
#include "RTClib.h"
#include <Wire.h>
#include <U8g2lib.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <SD.h>
#include <ESP32Time.h>

#include <WiFi.h>
#include <time.h>

#define INTERNAL_RTC_OFFSET 3600
#define X_OFFSET 3
#define BAUDRATE 57600
#define DATAPOINTS_ON_SCREEN 288
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_SIZE 2
#define FONT_SIZE_LEGEND 1
#define CS_SD 5
#define CO2_COLOR TFT_BLUE
#define TEMP_COLOR TFT_RED
#define HUM_COLOR TFT_GREEN
#define CENTER_X SCREEN_WIDTH / 2 + 20
#define MODE_SWITCH 35 // 8
#define FORCE_REDRAW 34 // 11
#define SET_RTC 32 // 10

RTC_DS3231 rtc;
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
Adafruit_SCD30 scd30;  //I2C
Adafruit_BME280 bme;   // I2C
TFT_eSPI tft = TFT_eSPI();
File g_myFile;
ESP32Time rtc_internal(INTERNAL_RTC_OFFSET);

struct datapoint {
  String time;
  float temperature;
  float humidity;
  int co2;
};
String g_fileName = "";

const char* ssid = "Obi W(L)an Kenobi";
const char* password = "YTI2YWJjMjk3ZTUx";
const char* ntpServer = "de.pool.ntp.org";
const long gmtOffset_sec = 3600;
const int dayLightOffset_sec = 0;
datapoint ga_graphed_data[DATAPOINTS_ON_SCREEN];
int g_graphedIndex = 0;
String g_timeYear, g_timeMonth, g_timeDay, g_timeHour, g_timeMinute, g_timeSecond, g_timeStamp, g_date, g_time_hms, g_co2Data, g_humidData, g_tempData, g_timeDow, g_path, g_DataPoint;
datapoint g_currentData = { "", 0, 0, 0 };

int g_screenSelect = 0;
bool g_screenChangeFlag, g_modeSwitchActive, g_triggeredLogging, g_debounceSerialOut, g_debounceForceReDraw, g_debounceRTCSet, g_rtcError, g_debounceRtcError, g_IndexMisMatchReDraw, g_WiFiConnected;

int theoreticalIndex(datapoint point) {
  return point.time.substring(11, 13).toInt() * 12 + floor(point.time.substring(14, 16).toInt() / 5);
}
void setRTC() {
  bool gotString = false;
  char inChar;
  u8g2.clearDisplay();
  u8g2.clearBuffer();
  u8g2.drawStr(X_OFFSET, 10, "Setting Time via Serial");
  u8g2.sendBuffer();
  int year, month, day, hour, minute, second;
  int j = 0;
  int data[20];
  while (!gotString) {
    if (Serial.available()) {
      inChar = Serial.read();
      data[j] = char(inChar) - '0';
      j += 1;
      if (inChar == 'x') {
        gotString = true;
      }
    }
  }
  Serial.println();
  for (int i = 0; i < 20; i++) Serial.print(data[i]);
  year = data[0] * 10 + data[1] + 2000;
  // now month
  month = data[2] * 10 + data[3];
  // now date
  day = data[4] * 10 + data[5];
  // now hour
  hour = data[6] * 10 + data[7];
  // now minute
  minute = data[8] * 10 + data[9];
  // now second
  second = data[10] * 10 + data[11];
  rtc.adjust(DateTime(year, month, day, hour, minute, second));
  Serial.println("Current Time: " + String(rtc.now().year()) + "-" + String(rtc.now().month()) + "-" + String(rtc.now().day()) + "--" + String(rtc.now().hour()) + ":" + String(rtc.now().minute()) + ":" + String(rtc.now().second()));
  g_IndexMisMatchReDraw = true;
}
datapoint StrToDataPoint(String text) {
  int StringCount = 0;
  String strs[5];
  while (text.length() > 0) {
    int index = text.indexOf(';');
    if (index == -1) {
      strs[StringCount++] = text;
      break;
    } else {
      strs[StringCount++] = text.substring(0, index);
      text = text.substring(index + 1);
    }
  }
  datapoint data = { strs[0], strs[1].toFloat(), strs[2].toFloat(), strs[3].toInt() };
  return data;
}
void drawGraph(float data[288], int points, int type) {
  float offset = 0;
  int color = 0;
  float scale = 0;
  int max_y = 220;
  int min_y = 20;
  int last_y;
  float raw_y;
  int y;
  points = min(points, 288);
  switch (type) {
    case 0:  //co2
      color = CO2_COLOR;
      offset = -300;
      scale = 0.22;
      break;
    case 1:  //temp
      color = TEMP_COLOR;
      offset = -15;
      scale = 10;
      break;
    case 2:  //humidity
      color = HUM_COLOR;
      offset = 0;
      scale = 2;
      break;
  }

  for (int i = 0; i < points; i++) {

    raw_y = floor((float(data[i]) + offset) * scale);
    y = min(float(max_y), max_y - max(float(min_y), raw_y));
    if (i) {
      last_y = min(float(max_y), max_y - max(float(min_y), floor((float(data[i - 1]) + offset) * scale)));
    } else {
      last_y = y;
    }
    int x = i + 30;
    if (i % 5 == 0) {
      //Serial.println("Printed Pixel to X: "+String(x)+"; Y: "+String(y));
      //Serial.println("Debug Data:  Data Value: "+String(data[i]+offset)+"; Y-Raw: "+String(raw_y)+"; Scale: "+String(scale));
    }
    last_y = max(last_y, 20);
    if (y < 20) {
      tft.drawLine(x, 20, x, last_y, TFT_RED);

    } else {
      tft.drawLine(x, y, x, last_y, color);
    }
  }
}
void checkforData() {
  int data;
  g_graphedIndex = 0;
  String dataString = "";
  int StringCount = 0;
  String strs[288];
  datapoint point;
  int new_index;
  getTime();
  g_myFile = SD.open(g_fileName.c_str());
  if (!g_myFile) {
    Serial.println("File read of " + g_fileName + " failed. returning to normal program routine");
    return;
  }
  while (g_myFile.available()) {
    data = g_myFile.read();
    dataString += char(data);
    //Serial.print(char(data));
    if (dataString.endsWith("\n")) {
      strs[StringCount++] = dataString;
      //Serial.println(dataString);
      dataString = "";
    }
    //ga_graphed_data[g_graphedIndex] = StrToDataPoint(data);
    //g_graphedIndex++;
  }
  for (int i = 0; i < StringCount; i++) {
    point = StrToDataPoint(strs[i]);
    new_index = theoreticalIndex(point);
    if (new_index > 288) {
      Serial.println("Clock error ig");
      new_index = 288;
    }
    if (new_index != g_graphedIndex) {
      Serial.println("New Index: " + String(new_index) + " Graphed Index was: " + String(g_graphedIndex));
      Serial.println("Indexing Error: Correcting Index");
      g_graphedIndex = new_index;
    }
    //Serial.println(g_graphedIndex);
    ga_graphed_data[g_graphedIndex] = point;
    //Serial.println(strs[i]);
    g_graphedIndex++;
  }
  g_myFile.close();
  new_index = theoreticalIndex(g_currentData);
  if (new_index > g_graphedIndex) {
    g_graphedIndex = new_index;
    Serial.println("Corrected Index");
  }
  Serial.println("Imported " + String(max(StringCount - 1, 0)) + " of " + String(new_index) + " Datapoints");
}
void synchTime(bool WiFiConnected) {
  DateTime now = rtc.now();
  if (WiFiConnected) {
    configTime(gmtOffset_sec, dayLightOffset_sec, ntpServer);
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) {
      Serial.println("failed to get current time! Updating internal rtc from DS3231");
      synchTime(false);
      return;
    }
    Serial.println("Current time:");
    Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
    if (getLocalTime(&timeinfo)) {
      rtc_internal.setTimeStruct(timeinfo);
      int now_hour;
    if(rtc_internal.getHour(true)>0){
      now_hour = rtc_internal.getHour(true)-1;
    }
    else{
      now_hour = 23;
    }
      rtc.adjust(DateTime(rtc_internal.getYear(), rtc_internal.getMonth()+1, rtc_internal.getDay()-1, now_hour, rtc_internal.getMinute(), rtc_internal.getSecond()));
    }
  } else {
    Serial.println("Updating internal rtc from DS3231");
    int now_hour;
    if(now.hour()>0){
      now_hour = now.hour()-1;
    }
    else{
      now_hour = 23;
    }
    rtc_internal.setTime(now.second(), now.minute(), now_hour, now.day(), now.month(), now.year());  //potential issue here
    Serial.println(rtc_internal.getDateTime());
  }
  Serial.println("Synchronised Clocks");
}
void setup() {
  // put your setup code here, to run once:

  Serial.begin(BAUDRATE);
  delay(5000);
  while (!Serial) { ; };
  Serial.flush();
  if (!SD.begin(CS_SD)) {
    Serial.println("Init of SD failed");
  }
  else{
    Serial.println("SD Card initialised");
  }
  pinMode(MODE_SWITCH, INPUT);
  pinMode(FORCE_REDRAW, INPUT);
  pinMode(SET_RTC, INPUT);
  Wire.begin();
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  u8g2.begin();
  u8g2.clearDisplay();
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_courR08_tf);
  g_screenChangeFlag = true;
  if (!rtc.begin()) {
    Serial.println("Could not find RTC! Check circuit.");
  }
  Wire.beginTransmission(0x68);  // address DS3231
  Wire.write(0x0E);              // select CONTROL register
  Wire.write(0b000011100);       // write register (bit 7 is EOSC)
  Wire.endTransmission();
  Wire.beginTransmission(0x68);
  Wire.write(0x0F);        // select STATUS register
  Wire.write(0b00001000);  // write register (bit 7 is OSF)
  Wire.endTransmission();
  //rtc.adjust(DateTime(__DATE__, __TIME__));
  scd30.begin();
  bme.begin(0x76);  //bme Sensor

  if (!scd30.setMeasurementInterval(2)) {
    Serial.println("Failed to Set Interval");
  }
  Serial.print("Measurement Interval: ");
  Serial.print(scd30.getMeasurementInterval());
  Serial.println(" seconds");
  int wificheck = 10;
  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED && wificheck) {
    delay(1000);
    wificheck--;
    Serial.println("Connecting to WiFi...");
  }
  if (WiFi.status() == WL_CONNECTED) {
    g_WiFiConnected = true;
    Serial.println("Connected to WiFi");
  } else {
    g_WiFiConnected = false;
    Serial.println("Not Connected to WiFi, synchronising with RTC");
  }
  delay(2000);
  synchTime(g_WiFiConnected);
  checkforData();
}

void fixRTCError() {
  int t = 0;
  Serial.println("RTC Error detected, Please reset over Serial or continue");
  while (1) {
    if (digitalRead(SET_RTC) == HIGH) {
      setRTC();
      break;
    }
    if (digitalRead(MODE_SWITCH) == HIGH || t == 60) break;
    sleep(1);
    t++;
  }

  Serial.println();
}
void getTime() {
  DateTime now = rtc.now();
  g_timeYear = String(now.year(), DEC);
  g_timeMonth = String(now.month(), DEC);
  g_timeDay = String(now.day(), DEC);
  g_timeHour = String(now.hour(), DEC);
  g_timeMinute = String(now.minute(), DEC);
  g_timeSecond = String(now.second(), DEC);
  if ((g_timeMonth.length()) == 1) { g_timeMonth = "0" + g_timeMonth; }  // eine null voranstellen
  if ((g_timeDay.length()) == 1) { g_timeDay = "0" + g_timeDay; }
  if ((g_timeHour.length()) == 1) { g_timeHour = "0" + g_timeHour; }
  if ((g_timeMinute.length()) == 1) { g_timeMinute = "0" + g_timeMinute; }
  if ((g_timeSecond.length()) == 1) { g_timeSecond = "0" + g_timeSecond; }
  if (g_date != g_timeDay + "." + g_timeMonth + "." + g_timeYear) {
    g_date = g_timeDay + "." + g_timeMonth + "." + g_timeYear;
    g_graphedIndex = 0;
    g_screenChangeFlag = true;
  }
  g_time_hms = g_timeHour + ":" + g_timeMinute + ":" + g_timeSecond;
  g_timeStamp = g_date + "-" + g_time_hms;
  g_fileName = "/data_" + g_timeDay + "_" + g_timeMonth + "_" + g_timeYear + ".csv";
  g_currentData.time = g_timeStamp;
}
void getSensorData() {

  if (scd30.dataReady()) {
    scd30.read();
    g_currentData.co2 = scd30.CO2;

    g_currentData.temperature = bme.readTemperature();
    //pressure = bme.readPressure() / 100.0F;
    g_currentData.humidity = bme.readHumidity();

    //Serial.println(g_timeStamp+","+String(g_currentData.temperature)+","+String(g_currentData.humidity)+","+String(g_currentData.co2));
  }
}
void drawOnScreen() {
  if (g_screenChangeFlag) {
    tft.fillScreen(TFT_BLACK);
    tft.drawLine(29, 20, 29, 220, TFT_WHITE);
    tft.drawLine(29, 220, 320, 220, TFT_WHITE);
    for (int i = 0; i < 13; i++) {
      if (i != 12) tft.drawCentreString(String(i * 2), 30 + i * 24, 227, FONT_SIZE_LEGEND);
      tft.drawLine(30 + i * 24, 220, 30 + i * 24, 225, TFT_WHITE);
    }
  }
  float data[288];
  //small screen
  u8g2.clearBuffer();
  u8g2.drawStr(X_OFFSET, 10, (g_date + "-" + g_time_hms).c_str());
  g_tempData = "Temp: " + String(g_currentData.temperature) + "C";
  g_humidData = "Humid: " + String(g_currentData.humidity) + "%";
  g_co2Data = "CO2: " + String(g_currentData.co2) + "ppm";
  u8g2.drawStr(X_OFFSET, 19, g_tempData.c_str());
  u8g2.drawStr(X_OFFSET, 28, g_humidData.c_str());
  u8g2.drawStr(X_OFFSET, 37, g_co2Data.c_str());
  u8g2.sendBuffer();
  //big screen
  switch (g_screenSelect) {
    case 0:  //co2
      if (g_screenChangeFlag) {
        Serial.println("showing CO2 Graph");
        tft.drawCentreString("CO2 Concentration Statistics for today", CENTER_X, 5, FONT_SIZE);
        tft.drawCentreString("ppm", 29, 10, FONT_SIZE_LEGEND);
        g_screenChangeFlag = false;
        tft.drawCentreString("500", 16, 197, FONT_SIZE_LEGEND);
        tft.drawLine(24, 197, 29, 197, TFT_WHITE);  //500
        tft.drawCentreString("750", 16, 142, FONT_SIZE_LEGEND);
        tft.drawLine(24, 142, 29, 142, TFT_WHITE);  //1000
        tft.drawCentreString("1000", 16, 86, FONT_SIZE_LEGEND);
        tft.drawLine(29, 86, 320, 86, TFT_YELLOW);  //Ab hier lüften
        tft.drawLine(24, 86, 29, 86, TFT_WHITE);    //1500
      }
      for (int i = 0; i < 288; i++) {
        data[i] = ga_graphed_data[i].co2;
      }
      break;
    case 1:  //temp
      if (g_screenChangeFlag) {
        Serial.println("showing Temperature Graph");
        tft.drawCentreString("Temperature Statistics for today", CENTER_X, 5, FONT_SIZE);
        tft.drawCentreString("°C", 29, 10, FONT_SIZE_LEGEND);
        g_screenChangeFlag = false;
        tft.drawCentreString("20", 16, 170, FONT_SIZE_LEGEND);
        tft.drawLine(24, 170, 29, 170, TFT_WHITE);  //20
        tft.drawCentreString("25", 16, 120, FONT_SIZE_LEGEND);
        tft.drawLine(24, 120, 29, 120, TFT_WHITE);  //25
        tft.drawCentreString("30", 16, 70, FONT_SIZE_LEGEND);
        tft.drawLine(24, 70, 29, 70, TFT_WHITE);  //30
      }
      for (int i = 0; i < 288; i++) {
        data[i] = ga_graphed_data[i].temperature;
      }
      break;
    case 2:  //humidity
      if (g_screenChangeFlag) {
        Serial.println("Showing Humidity Graph");
        tft.drawCentreString("Humidity Statistics for today", CENTER_X, 5, FONT_SIZE);
        tft.drawCentreString("%", 29, 10, FONT_SIZE_LEGEND);
        g_screenChangeFlag = false;
      }
      for (int i = 0; i < 288; i++) {
        data[i] = ga_graphed_data[i].humidity;
      }
      break;
      //
    default:
      break;
  }
  drawGraph(data, g_graphedIndex, g_screenSelect);
}
void combineData() {
  g_DataPoint = g_timeStamp + ";" + String(g_currentData.temperature) + ";" + String(g_currentData.humidity) + ";" + String(g_currentData.co2) + "\n";
  Serial.print(g_DataPoint);
  g_path = g_fileName;
  int data;
  String dataString = "";
  g_myFile = SD.open(g_path.c_str());
  while (g_myFile.available()) {
    data = g_myFile.read();
    dataString += char(data);
  }
  g_myFile.close();
  g_myFile = SD.open(g_path.c_str(), FILE_WRITE);
  if (!g_myFile) {
    Serial.println("opening Log File for write failed - - - " + g_timeStamp);
  }
  g_myFile.print(dataString + g_DataPoint);
  g_myFile.close();
  Serial.println("Logging complete");
  Serial.println("Current graphed index: " + String(g_graphedIndex));
}
void loop() {
  if (rtc.lostPower()) {
    Serial.println("RTC lost power at some point...");
  }
  if (digitalRead(MODE_SWITCH) == HIGH) { //LOW
    if (!g_modeSwitchActive) {
      g_screenSelect++;
      g_screenChangeFlag = true;
      if (g_screenSelect > 1) {
        g_screenSelect = 0;
      }
      g_modeSwitchActive = true;
    }
  } else {
    g_modeSwitchActive = false;
  }
  if (digitalRead(FORCE_REDRAW) == HIGH || g_IndexMisMatchReDraw) {
    g_IndexMisMatchReDraw = false;
    if (!g_debounceForceReDraw) {
      checkforData();
      g_screenChangeFlag = true;
      drawOnScreen();
      g_debounceForceReDraw = true;
    }
  } else {
    g_debounceForceReDraw = false;
  }
  if (digitalRead(SET_RTC) == HIGH) {
    if (!g_debounceRTCSet) {
      setRTC();
      g_debounceRTCSet = true;
    }
  } else {
    g_debounceRTCSet = false;
  }
  getTime();
  getSensorData();
  g_rtcError = g_timeHour.toInt() > 23 || g_timeMinute.toInt() > 60 || g_timeSecond.toInt() > 60 || g_timeDay.toInt() > 31 || g_timeMonth.toInt() > 12;
  if (g_rtcError) {
    if (!g_debounceRtcError) {
      g_debounceRtcError = true;
      fixRTCError();
    }
  } else {
    g_debounceRtcError = false;
  }
  if ((g_timeMinute.toInt() % 5 == 0) && g_timeSecond.toInt() == 0) {

    if (!g_triggeredLogging) {
      Serial.println("Logging current Data...");
      combineData();

      ga_graphed_data[g_graphedIndex] = { g_timeStamp, g_currentData.temperature, g_currentData.humidity, g_currentData.co2 };
      if (g_graphedIndex + 2 < g_timeStamp.substring(11, 13).toInt() * 12 + floor(g_timeStamp.substring(14, 16).toInt() / 5)) {
        g_IndexMisMatchReDraw = true;
        Serial.println("Detected Mismatch in Indeces");
      }
      g_graphedIndex++;
      if (g_graphedIndex > 287) {
        for (int i = 1; i < 288; i++) {
          ga_graphed_data[i - 1] = ga_graphed_data[i];
        }
        g_graphedIndex = 286;
        g_screenChangeFlag = true;
      }
      g_triggeredLogging = true;
    }
  } else {
    g_triggeredLogging = false;
  }
  if (g_timeSecond.toInt() % 2 == 0) {
    if (!g_debounceSerialOut) {
      drawOnScreen();
      g_debounceSerialOut = true;
    }
  } else {
    g_debounceSerialOut = false;
  }
  delay(20);
}
