#include <Wire.h>
// display mapping for Arduino MEGA
// BUSY -> 7, RST -> 9, DC -> 8, CS-> 53, CLK -> 52, DIN -> 51
#include <GxEPD2_BW.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include "FreeSansBold11pt7b.h"
#include "FreeSans22pt7b.h"
#include <Fonts/TomThumb.h>
#include <MD_DS3231.h>
#include <BME280I2C.h>
#include <bc_co2_module_arduino.h>

/**************************** BME280 setup **********************************/
BME280I2C::Settings settings(
   BME280::OSR_X1,
   BME280::OSR_X1,
   BME280::OSR_X1,
   BME280::Mode_Forced,
   BME280::StandbyTime_1000ms,
   BME280::Filter_4,
   BME280::SpiEnable_False,
   0x76 // I2C address. I2C specific.
);

BME280I2C bme(settings);

/**************************** Serial setup **********************************/
#define BAUDRATE 115200

/**************************** Screen setup **********************************/

#define MAX_DISPAY_BUFFER_SIZE 32
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPAY_BUFFER_SIZE / (EPD::WIDTH / 8))
GxEPD2_BW<GxEPD2_290, MAX_HEIGHT(GxEPD2_290)> display(GxEPD2_290(/*CS=10*/ SS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));

/*************************** Global variables *******************************/

struct data {
    float temp;
    float hum;
    int rhum;
    float pres;
    unsigned int rpres;
    int16_t co2;
};

volatile static struct data dataset;
static struct lp8_t sensor;
char incoming[40];
char ssid[16];
char ip[16];
static byte bmeerr = 0;

/******************************* Setup routine ******************************/
void setup() {
    
    ADCSRA = 0; //turn off adc to save power as it is not needed
    
    pinMode(13, OUTPUT);
    digitalWrite(13, LOW); //turn off led

    Serial.begin(BAUDRATE);
    Serial2.begin(BAUDRATE);
    Wire.begin();

    Serial.print(F("\r\n***Enviro data monitor***\r\n"));
   //start co2 module
    init_co2_module();

    display.init(); //init display
    delay(100);
    layoutScreen(); //print static parts of screen

    //make sure we're always in 24hr mode, invert to change to 12h
    switch (RTC.status(DS3231_12H)) {
        case DS3231_OFF: 
            Serial.println(F("RTC in 24h mode"));
            break;
        case DS3231_ON: 
            Serial.println(F("RTC in 12h mode, fixing")); //should not occur
            RTC.control(DS3231_12H, DS3231_OFF);
            break;
        default:
            break;
    }
    
    Serial.println(F("Waiting for ESP to boot"));
    bool n;
    static byte tries = 0;
    Serial.print("[");
    while(1) {
            delay(1000);
            Serial.print("=");
            n = readNetInfo();
            if(n == true) {
                break;
            }
            else {
                tries++;
                if(tries >= 33) {
                    Serial.print("X");
                    Serial.println();
                    Serial.println(F("Could not read connection info, likely no network connection"));
                    display.setFont(&TomThumb);
                    display.setTextColor(GxEPD_BLACK);
                    uint16_t x = 10;
                    uint16_t y = 277;
                
                    display.setPartialWindow(0, 270, display.width(), 20);
                
                    display.firstPage();
                    do {
                        display.fillScreen(GxEPD_WHITE);
                        display.setCursor(x,y);
                        display.print("Network error");
                    }
                    while (display.nextPage());   
                    tries = 0;
                    break;
                }
            }
    }

    static byte retries = 0;
    bool t;
    while(1){     //sync time with network
        delay(5000); 
        t = syncTime();
        if(t == true) {
            break;
        }
        else {
            retries++;
            if(retries > 4){ //break after 5 tries
                Serial.println(F("Could not sync time\n"));
                break;  
            }
        }
    }

    retries = 0;
    printRTCTimeToScreen();   //print time to screen
    printRTCDateToScreen();   //print date to screen

    bool tsens;
    while(1) {
        tsens = bme.begin();
        if(tsens == true) {
            switch(bme.chipModel()) {
                case BME280::ChipModel_BME280:
                    Serial.println(F("Found BME280 sensor!"));
                    break;
                case BME280::ChipModel_BMP280:
                    Serial.println(F("Found BMP280 sensor! No Humidity available."));
                    break;
                default:
                    Serial.println(F("Error, found unknown sensor."));
            }
            break;
        }
        else {
            delay(2000);
            retries++;
            if(retries > 4) {
                Serial.println(F("Could not find BME280 sensor."));
                //display error message on screen
                display.setFont(&FreeSansBold11pt7b);
                display.setTextColor(GxEPD_BLACK);
                int16_t tbx, tby; uint16_t tbw, tbh;
                display.getTextBounds("SENS ERR!", 0, 0, &tbx, &tby, &tbw, &tbh);
                uint16_t y = (92 - tbh) - tby + 20;
            
                display.setPartialWindow(0, 70 + 20, display.width(), 33);
                display.firstPage();
                do {
                    display.fillScreen(GxEPD_WHITE);
                    display.setCursor(6, y);
                    display.print(F("SENS ERR!"));
                }
                while (display.nextPage());
                bmeerr = 1;
                break;
            }
        }
    }
  
   Serial.println(F("Setup complete"));
};

/********************************* MAINLOOP ***************************************/
void loop() {
    static byte oldmins;
        
    RTC.readTime();
    if(RTC.m != oldmins){  //run this every minute
        printRTCTimeToScreen(); //update time on screen
        if(bmeerr == 0){ //only attempt to measure anything if the sensor is present
            doStuff(); //acquire data
            sendDataToServer(); //send over to esp for publishing
            printDataToScreen(); //update values on screen
        }
        oldmins = RTC.m;  //save old value
    }
    else if((RTC.m == 0 || RTC.m == 30) && RTC.s == 55) {
        delay(1100); //delay to prevent firing multiple times
        display.refresh();  //perform a full display refresh every half hour, removes quick refresh artifacts
    }
    else if(RTC.m == 0 && RTC.h == 0 && RTC.s == 40){
        delay(1100); //delay to prevent firing multiple times
        printRTCDateToScreen(); //refresh date on midnight
    }
    else if(RTC.m == 0 && RTC.h == 2 && RTC.s == 33) { //resync time at 2am 
        static byte retries = 0;
        bool t;
        while(1){ 
            delay(2000); 
            t = syncTime();
            if(t == true) {
                break;
            }
            else {
                retries++;
                if(retries > 4){ //break after 5 tries
                    Serial.println(F("Could not sync time\n"));
                    break;  
                }
            }
        }
    }
    delay(500);
};
/************************************************************************************/

void doStuff() {
    dataset.temp = measureTemp();
    dataset.hum = measureHum();
    dataset.rhum = round(dataset.hum);
    dataset.pres = measurePres();
    dataset.rpres = round(dataset.pres);
    dataset.co2 = measureCO2();
};

bool readNetInfo() {
    char buf;
    bool commRecvd = false;
    static byte i=0;
    static bool recv = false;
    
    while(Serial2.available() > 0) {
        buf = Serial2.read();
        if(recv == true) {
            if(buf != '\n'){
                incoming[i] = buf;
                i++;
            }
            else {
                incoming[i] = '\0';
                recv = false;
                i = 0;
                commRecvd = true;
            }
        }
        else if (buf == '!'){
            recv = true;
        }
    }

    i = 0;
    
    if(commRecvd == true){
        char* token = strtok(incoming, ",");
        while(token != NULL){
            if(i == 0){
                sprintf(ssid,"%s", token);
            };
            if(i == 1) {
                sprintf(ip,"%s", token);
            };
            token = strtok(NULL, ",");
            i++;
        }
        i = 0;
        commRecvd = false;
        Serial.print("]");
        Serial.println();
        Serial.print(F("Got conn info:\n"));
        Serial.println(ssid);
        Serial.println(ip);
        printConnInfo();
        return true;
    }
    return false;
}


bool syncTime() {
    char tbuf;
    static bool trecvd = false;
    static bool trecvn = false;
    char tarry[21];
    static byte j = 0;
    byte dst;

    dst = checkDST();

    Serial.print(F("Syncing time...\n"));
    delay(10);
    Serial2.print("?ntp");
    Serial2.print("\n");
    delay(300);
    while(Serial2.available() > 0 && trecvd == false){
        tbuf = Serial2.read();
         if(trecvn == true) {
            if(tbuf != '\n'){
                tarry[j] = tbuf;
                j++;
            }
            else {
                tarry[j] = '\0';
                trecvn = false;
                j = 0;
                trecvd = true;
            }
        }
        else if (tbuf == '>'){
            trecvn = true;
        }
    }
    j = 0;
    if(trecvd == true){
        Serial.println(tarry);
        char* token = strtok(tarry, "/");
        while(token != NULL){
            if(j == 0){
                RTC.yyyy = atoi(token);
            };
            if(j == 1) {
                RTC.mm = atoi(token);
            };
            if(j == 2) {
                RTC.dd = atoi(token);
            };
            if(j == 3) {
                if(dst == 1) {
                    if(atoi(token) == 23) {
                        RTC.h = 0;
                    }
                    else {
                        RTC.h = atoi(token) + 1;
                    }
                }
                if(dst == 0) {
                    RTC.h = atoi(token);
                }
            };
            if(j == 4){
                RTC.m = atoi(token);
            };
            if(j == 5){
                RTC.s = atoi(token);
            };
            token = strtok(NULL, "/");
            j++;
        }
        j = 0;
        RTC.writeTime();
        Serial.println(F("NTP sync complete"));
        trecvd = false;
        return true;
    }
    return false;
}

void layoutScreen() {
    display.setRotation(0); //portrait, connector at the top
    display.setFullWindow();

    display.setFont(&TomThumb);
    display.setTextColor(GxEPD_BLACK);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(8, 69+20);
        display.print(F("Temperature:"));
        display.setCursor(8, (69 + 40 + 20));
        display.print(F("Humidity:"));
        display.setCursor(8, (69 + 80) + 20);
        display.print(F("Pressure:"));
        display.setCursor(8, (69 + 120) + 20);
        display.print(F("CO2 concentration:"));
        display.setCursor(8, 269);
        display.print(F("Network info:"));
    }
    while (display.nextPage());
}

void printConnInfo() {
    display.setFont(&TomThumb);
    display.setTextColor(GxEPD_BLACK);
    int16_t tbx, tby; uint16_t tbw, tbh;
    display.getTextBounds(ssid, 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x = 10;
    uint16_t y = 277;
    display.getTextBounds(ip, 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x2 = 10;
    uint16_t y2 = 284;

    display.setPartialWindow(0, 270, display.width(), 20);

    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x,y);
        display.print("SSID: ");
        display.print(ssid);
        display.setCursor(x2,y2);
        display.print("IP: ");
        display.print(ip);
    }
    while (display.nextPage());
}

void printRTCTimeToScreen() {
    char delim[] = ":";
    RTC.readTime();
    display.setFont(&FreeSans22pt7b);
    display.setTextColor(GxEPD_BLACK);
    int16_t tbx, tby; uint16_t tbw, tbh;
    display.getTextBounds(delim, 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x = ((display.width() / 2) - tbw);
    uint16_t y = ((32 - tbh) / 2) - (tby - 4) + 10;
    display.getTextBounds("00", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x2 = (display.width() / 2) - 8 - tbw ;
    uint16_t y2 = ((32 - tbh) / 2) - ( tby - 4) + 10;
    display.getTextBounds("00", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x3 = (display.width() / 2) + 8;
    display.setPartialWindow(0, 0, display.width(), (display.height() - (display.height() - 32)) + 20);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x, y);
        display.print(delim);
        display.setCursor(x2, y2);
        if(RTC.h < 10) {
            display.print("0");
            display.print(RTC.h);
        }
        else {
            display.print(RTC.h);
        }
        display.setCursor(x3, y2);
        if(RTC.m < 10) {
            display.print("0");
            display.print(RTC.m);
        }
        else {
            display.print(RTC.m);
        }
    }
    while(display.nextPage());
}

void printRTCDateToScreen() {
    RTC.readTime();
            
    display.setFont(&FreeSansBold9pt7b);
    display.setTextColor(GxEPD_BLACK);
    int16_t tbx, tby; uint16_t tbw, tbh;
    display.getTextBounds("00 - 00 - 0000", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x = (display.width() / 2) - (tbw / 2) ;
    uint16_t y = 48 + (tbh / 2) + 20;

    display.setPartialWindow(0, 33 + 20, display.width(), 31);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x, y);
        if(RTC.dd < 10) {
            display.print("0");
            display.print(RTC.dd);
        }
        else {
            display.print(RTC.dd);
        }
        display.print(" - ");
        if(RTC.mm < 10){
            display.print("0");
            display.print(RTC.mm);
        }
        else {
            display.print(RTC.mm);
        }
        display.print(" - ");
        display.print(RTC.yyyy);
    }
    while(display.nextPage());
}

void printDataToScreen() {
    struct data *dtsp = &dataset;
    display.setFont(&FreeSansBold11pt7b);
    display.setTextColor(GxEPD_BLACK);
    int16_t tbx, tby; uint16_t tbw, tbh;
    display.getTextBounds("00.0 C", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x = ((display.width() - tbw) / 2) - tbx;
    uint16_t y = (92 - tbh) - tby +20;
    display.getTextBounds("00 %", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x2 = ((display.width() - tbw) / 2) - tbx;
    uint16_t y2 = (132 - tbh) - tby + 20;
    display.getTextBounds("1488 hpa", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t x3 = ((display.width() - tbw) / 2) - tbx;
    uint16_t y3 = ( 176 - tbh)  - tby + 20;
    display.getTextBounds("1488 ppm", 0, 0, &tbx, &tby, &tbw, &tbh);
    uint16_t y4 = ( 216 - tbh) - tby + 20;

    display.setPartialWindow(0, 70 + 20, display.width(), 33);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x, y);
        display.print(dtsp -> temp, 1);
        display.drawCircle(x+48,80 + 20,3,GxEPD_BLACK);
        display.drawCircle(x+48,80 + 20,2,GxEPD_BLACK); //quick and dirty way to get a "degrees symbol"
        display.print(F("  C"));
    }
    while (display.nextPage());

    display.setPartialWindow(0, 110 + 20, display.width(), 33);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x2, y2);
        display.print(dtsp -> rhum);
        display.print(F(" %"));
    }
    while(display.nextPage());

    display.setPartialWindow(0, 150 + 20,display.width(), 33);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x3, y3);
        display.print(dtsp -> rpres);
        display.print(F(" hPa"));
    }
    while(display.nextPage());

    display.setPartialWindow(0, 190 +20 ,display.width(), 33);
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
        display.setCursor(x3, y4);
        display.print(dtsp -> co2);
        display.print(F(" ppm"));
    }
    while(display.nextPage());
}

float measureTemp() {
    BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
    int temps[5];

    //light led while measuring
    digitalWrite(13, HIGH);

    Serial.print(F("Measuring temperature"));

    for(char i = 0; i < 5; i++){
        temps[i] = bme.temp() * 100;
        Serial.print(F("."));
        delay(1000);
    }

    float temp;

    temp = (temps[0] + temps[1] + temps[2] + temps[3] + temps[4]) / 5;

    Serial.print(F("done: "));
    Serial.println(temp / 100);
    digitalWrite(13, LOW);
    
    return temp/100;
}

 float measureHum() {
    int hums[5];

    //light led while measuring
    digitalWrite(13, HIGH);

    Serial.print(F("Measuring humidity"));

    for(char i = 0; i < 5; i++){
        hums[i] = bme.hum() * 100;
        Serial.print(F("."));
        delay(1000);
    }

    float hum;

    hum = (hums[0] + hums[1] + hums[2] + hums[3] + hums[4]) / 5;

    Serial.print(F("done: "));
    Serial.println(hum / 100);

    digitalWrite(13, LOW);
    return hum / 100;
}

float measurePres() {
    BME280::PresUnit presUnit(BME280::PresUnit_hPa);
    float presrs[5];

    //light led while measuring
    digitalWrite(13, HIGH);

    Serial.print(F("Measuring pressure"));

    for(char i = 0; i < 5; i++){
        presrs[i] = bme.pres(BME280::PresUnit_hPa);
        Serial.print(F("."));
        delay(1000);
    }

    float pres;

    pres = (presrs[0] + presrs[1] + presrs[2] + presrs[3] + presrs[4]) / 5;

    Serial.print(F("done: "));
    Serial.println(pres);

    digitalWrite(13, LOW);
    return pres;
}

int16_t measureCO2(){
    digitalWrite(13, HIGH);
    Serial.print(F("Measuring CO2....."));

    int16_t co2conc = get_co2_concentration(uint16_t(dataset.pres * 10));

    Serial.print(F("done:"));
    Serial.println(co2conc);

    digitalWrite(13, LOW);
    return(co2conc);
}

bool sendDataToServer(){
    char payload[22];
    char stemp[8];
    char shum[8];
    dtostrf(dataset.temp, 5, 2, stemp);
    dtostrf(dataset.hum, 5, 2, shum);   //convert floats to strings
    sprintf(payload, "%s,%s,%d,%d", stemp, shum, dataset.rpres, dataset.co2); //convert everything to a string
    Serial2.print(payload);     //send over serial 2 to esp
    Serial2.print('\n');        //terminate transmission
    delay(1000); //give time to process comm
    Serial.print(F("Data sent\r\n"));
    return(true);
}

byte checkDST() {
    byte DST;
    RTC.readTime();
// ********************* Calculate offset for Sunday *********************
    int y = RTC.yyyy - 2000;        //take only last two digits of year (while not universal, this will work for the next 80 years so good enough for me)
    int x = (y + y/4 + 2) % 7;      // remainder will identify which day of month
                                                 // is Sunday by subtracting x from the one
                                                // or two week window.  First two weeks for March
                                               // and first week for November
// *********** BEGINS on 2nd Sunday of March @ 2:00 AM *********
    if(RTC.mm == 3 && RTC.dd == (14 - x) && RTC.h >= 2) {
        DST = 1;
    }
    if((RTC.mm == 3 && RTC.dd > (14 - x)) || RTC.mm > 3) {
        DST = 1;
    }
// ************* ENDS on 1st Sunday of Nov @ 2:00 AM ************
    if(RTC.mm == 11 && RTC.dd == (7 - x) && RTC.h >= 2) {
        DST = 0;
    }
    if((RTC.mm == 11 && RTC.dd > (7 - x)) || RTC.mm > 11 || RTC.mm < 3) {
        DST = 0;
    }
    return(DST);
}
