/**
 * Author : Anatole SCHRAMM-HENRY
 * Created the : 11/07/2021
 * This is a quick and dirty firmewire to receive and save the data in the database
 */

#include <SPI.h>
#include <ESP8266WiFi.h>
#include <PCF8574.h>
#include <HttpClient.h>
#include "RF24.h"
#include <LoRa.h>
#include "definition.h"
#include "credentials.h"

/* NRF pinout definition */
#define NRF_1_CE 15 //chip enable
#define NRF_2_CE 16 //chip enable

#define NRF_1_CS 5 //chip select
#define NRF_2_CS 4 //chip select

#define CHAN 108   //0-125
#define RECV_CHECK 5000 //We test every 5s if we received something
#define WIFI_CHECK_TIMEOUT 20000
#define PREVENTIVE_RESET_DELAY 18000000 //Every 5 hours

/* LoRa module pinout definition (using the IO expander) */
#define LORA_CS_PIN  (PCF8574::P5)
#define LORA_RST_PIN (PCF8574::P6)
#define LORA_DI0_PIN (PCF8574::P7)

#define LORA_BAND (868E6)

/* PCF8574 IO expander pinout */
#define PCF_INT_PIN (D9)

uint8_t payload[32] = {0};
DataPacket dp;
WeatherStationDataPacket wsdp;
MailboxDataPacket mdp;

RF24 NRF_1(NRF_1_CE, NRF_1_CS);
RF24 NRF_2(NRF_2_CE, NRF_2_CS);
PCF8574 PCF(0x20);

//Wifi event handlers
WiFiEventHandler gotIp, lostConnection;
HttpClient DBHost(DB_SERVER, PATH);

Dictionary<DictionaryHelper::StringEntity> weatherStationPostData, mailboxPostData;

const uint8_t ADDR[] = "WEST1", ADDR2[] = "ABCDE";
uint32_t timeStamp(0), irqSaver(0), resetTimeStamp(0);
uint32_t freeMem(0);
uint16_t biggestContigMemBlock(0);
uint8_t frag(0);
int insertIntoDBError(0);
volatile boolean IRQFlag(false);
boolean waitingForResetSignal(false), resetNow(false);

void PCFPinMode(uint8_t pin, uint8_t mode)
{
    PCF.pinMode(static_cast<PCF8574::Pin>(pin), static_cast<boolean>(mode));
}

void PCFDigitalWrite(uint8_t pin, uint8_t val)
{
    PCF.digitalWrite(static_cast<PCF8574::Pin>(pin), static_cast<boolean>(val));
}

void onLoRaReceive(int payload_size)
{
    PCF.digitalWrite(PCF8574::P2, LOW);
    Serial.printf("LoRa packet size : %d\n", payload_size);

    for(int i = 0; i < payload_size; i++)
    {
        Serial.printf("%c", LoRa.read());
    }
    Serial.printf("\nPacket RSSI : %d dBm\n", LoRa.packetRssi());
    PCF.digitalWrite(PCF8574::P2, HIGH);
}

IRAM_ATTR void PCFIRQHandler(void *p)
{
  Serial.println("PCF IRQs detected !");
  bool pinStates[8];
  PCF.digitalReadAll(pinStates);
  *(boolean *)p = true;

  /* If the IRQ pin is the DI0 one, manually call the LoRa IRQ handler */
  if(pinStates[7] == HIGH)
  {
    LoRa.handleDio0Rise();
  }
}

void setup() 
{
  //We do not need to read on the serial bus
  Serial.begin(115200, SERIAL_8N1, SERIAL_TX_ONLY);
  delay(1000);
  Serial.println("\nSetup begin");
  //We set the WiFi part up :
  gotIp = WiFi.onStationModeGotIP(&(gotIpFunc));
  lostConnection = WiFi.onStationModeDisconnected(&(lostConnectionFunc));
  //To prevent wear and tear on the flash mem
  WiFi.persistent(false);
  WiFi.disconnect(true);
  WiFi.softAPdisconnect(true);
  Serial.println("Connecting to the access point");
  WiFi.begin(SSID, PWD);
  //We initialize the dictionary 
  //These keys are used to post data for the weather station : 
  weatherStationPostData.add("accessCode", DictionaryHelper::StringEntity(ACCESS_CODE));
  weatherStationPostData.add("deviceType", DictionaryHelper::StringEntity(WEATHER_STATION_DEV_TYPE));
  weatherStationPostData.add("packetUID", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("battery", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("light", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("bmp_tmp", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("pressure", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("humidity", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("compensated_humidity", DictionaryHelper::StringEntity(NULL));
  weatherStationPostData.add("htu_tmp", DictionaryHelper::StringEntity(NULL));
  //These keys are used to post data for the mailbox
  mailboxPostData.add("accessCode", DictionaryHelper::StringEntity(ACCESS_CODE));
  mailboxPostData.add("deviceType", DictionaryHelper::StringEntity(MAILBOX_DEV_TYPE));
  mailboxPostData.add("packetUID", DictionaryHelper::StringEntity(NULL));
  mailboxPostData.add("battery", DictionaryHelper::StringEntity(NULL));
  mailboxPostData.add("event", DictionaryHelper::StringEntity(NULL));
  
  
  Serial.printf("\nNRF 1 %s\n",NRF_1.begin() ? "started" : "error");
  if(!NRF_1.isChipConnected())
    Serial.println("NRF 1 is missing");
  else
    Serial.println("NRF 1 is detected");
    
  NRF_1.setChannel(CHAN); 
  NRF_1.setPALevel(RF24_PA_MIN);
  NRF_1.setDataRate(RF24_250KBPS);
  NRF_1.setRetries(8,15);
  NRF_1.openReadingPipe(1, ADDR);
  NRF_1.startListening();

  Serial.printf("NRF 2 %s\n",NRF_2.begin() ? "started" : "error");
  if(!NRF_2.isChipConnected())
    Serial.println("NRF 2 is missing");
  else
    Serial.println("NRF 2 is detected");
    
  NRF_2.setChannel(CHAN); 
  NRF_2.setPALevel(RF24_PA_MIN);
  NRF_2.setDataRate(RF24_250KBPS);
  NRF_2.setRetries(8,15);
  NRF_2.openReadingPipe(1, ADDR);
  NRF_2.startListening();

  //Setting the I2C pins and the PCF8574
  Wire.begin(0,2);
  Serial.printf("PCF %s\n", PCF.begin() ? "found" : "not found");
  PCF.pinMode(PCF8574::P2, OUTPUT);
  PCF.pinMode(PCF8574::P3, OUTPUT);
  PCF.pinMode(PCF8574::P4, OUTPUT);

  /* Configuring LoRa module and RF settings */
  LoRa.setPins(LORA_CS_PIN, LORA_RST_PIN, LORA_DI0_PIN);
  LoRa.setCustomGPIOFn(&(PCFPinMode), &(PCFDigitalWrite));
  Serial.printf("LoRa module %s\n", LoRa.begin(LORA_BAND) ? "found" : "not found");
  Serial.println("######### LoRa regs : #########");
  LoRa.dumpRegisters(Serial);
  Serial.println("###############################");
  LoRa.setSpreadingFactor(10);
  LoRa.enableCrc();
  LoRa.onReceive(&(onLoRaReceive));
  /* Let's start listening to LoRa packets */
  LoRa.receive();

  //We set the RXD0 as a GPIO
  pinMode(PCF_INT_PIN, INPUT_PULLUP);
  attachInterruptArg(PCF_INT_PIN, &(PCFIRQHandler), (void *)&IRQFlag, ONLOW /*FALLING*/);

  Serial.println("End setup");
}

void loop() 
{
  //We check if we got and IRQ from one, both NRFs
  if(IRQFlag || (millis() - irqSaver > RECV_CHECK))
  {
    bool tx_ok, tx_fail, rx_ready;
    //We read the PCFs IO to check which NRF raised the IRQ :
    if(!PCF.digitalRead(PCF8574::P1)) //IRQs are active low
    {
      if(!IRQFlag)
        Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!IRQ MISSED FOR NRF 1");
      Serial.printf("NRF 1 triggered the IRQs\n Checking why :\n");
      NRF_1.whatHappened(tx_ok, tx_fail, rx_ready);
      if(tx_ok)
      {
        Serial.println("NRF 1 TX_OK");
      }
      if(tx_fail)
      {
        Serial.println("NRF 1 TX_FAIL");
      }
      if(rx_ready)
      {
        Serial.printf("NRF 1 Received %u bytes with sig(%s) : \n",NRF_1.getPayloadSize(), NRF_1.testRPD()?"good":"bad");
        NRF_1.read(payload, sizeof(payload));

        memcpy(&dp, payload, sizeof(dp));
        
        switch(dp.header)
        {
          case WEATHER_STATION:
          {
            memcpy(&wsdp, payload, sizeof(wsdp));
            debugStruct(&wsdp);
            insertIntoDBError = insertIntoDB(&wsdp);
          }
            break;
          case CONNECTED_MAILBOX:
          {
            memcpy(&mdp, payload, sizeof(mdp));
            debugStruct(&mdp);
            insertIntoDBError = insertIntoDB(&mdp);
          }
            break;
          default:
            break;
        }

        if(waitingForResetSignal)resetNow = true;
      }
    }
    
    if(!PCF.digitalRead(PCF8574::P0)) //IRQs are active low
    {
      if(!IRQFlag)
        Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!IRQ MISSED FOR NRF 2");
      Serial.printf("NRF 2 triggered the IRQs\n Checking why :\n");
      NRF_2.whatHappened(tx_ok, tx_fail, rx_ready);
      if(tx_ok)
      {
        Serial.println("NRF 2 TX_OK");
      }
      if(tx_fail)
      {
        Serial.println("NRF 2 TX_FAIL");
      }
      if(rx_ready)
      {
        Serial.printf("NRF 2 Received %u bytes with sig(%s) : \n",NRF_2.getPayloadSize(), NRF_2.testRPD()?"good":"bad");
        NRF_2.read(payload, sizeof(payload));
       
        memcpy(&dp, payload, sizeof(dp));
        
        switch(dp.header)
        {
          case WEATHER_STATION:
          {
            memcpy(&wsdp, payload, sizeof(wsdp));
            debugStruct(&wsdp);
            insertIntoDBError = insertIntoDB(&wsdp);
          }
            break;
          case CONNECTED_MAILBOX:
          {
            memcpy(&mdp, payload, sizeof(mdp));
            debugStruct(&mdp);
            insertIntoDBError = insertIntoDB(&mdp);
          }
            break;
          default:
            break;
        }

        if(waitingForResetSignal)resetNow = true;
      }
    }

    //We check if the RX fifo the NRFs are full
    //If yes, it means we have a big problem since packets can be thrown away without notice
    //Could be due to missed IRQs (very bad)
    if(NRF_1.rxFifoFull())
    {
      Serial.println("NRF 1 FIFO FULL !!!!!!!!!!!!");
      NRF_1.flush_rx();
    }
    if(NRF_2.rxFifoFull())
    {
      Serial.println("NRF 2 FIFO FULL !!!!!!!!!!!!");
      NRF_2.flush_rx();
    }
    IRQFlag = false;
    irqSaver = millis();
  }

  //Here we check if we are still connected
  if(millis() - timeStamp > WIFI_CHECK_TIMEOUT)
  {
    //PCF.togglePin(PCF8574::P2);
    //PCF.togglePin(PCF8574::P3);
    //PCF.togglePin(PCF8574::P4);
    ESP.getHeapStats(&freeMem, &biggestContigMemBlock, &frag);
    printf("Memory Info :\n - Free Mem > %u\n - Heap frag > %u\n - Max block > %u\nSign strength : %d\n", freeMem, frag, biggestContigMemBlock,WiFi.RSSI());
    Serial.println("Checking wifi link : ");
    if(WiFi.isConnected())
    {
      Serial.println("Link is established, nothing to be done.");
    }
    else
    {
      Serial.println("We try to reconnect...");
      WiFi.begin(SSID,PWD);
    }
    timeStamp = millis();
  }

  //Here we handle the reset
  if(millis() - resetTimeStamp > PREVENTIVE_RESET_DELAY)
  {
    Serial.println("Waiting next payload before resetting MCU :( !");
    waitingForResetSignal = true;
    resetTimeStamp = millis();
  }

  if((waitingForResetSignal && resetNow) || insertIntoDBError == -125)
  {
    ESP.reset();
  }
}

/**
 * Part where we declare our event handlers
 */

void gotIpFunc(const WiFiEventStationModeGotIP &event)
{
  Serial.printf("Got connected to station : ip address : %u.%u.%u.%u \n", event.ip[0], event.ip[1], event.ip[2], event.ip[3]);
}

void lostConnectionFunc(const WiFiEventStationModeDisconnected &event)
{
  (void)event;
  Serial.println("Lost connection, will try to reconnect ...");
}

int insertIntoDB(WeatherStationDataPacket *p)
{
  char buffer[100] = "";
  int result(0);
  //We get the values and put it in our dictionary
   sprintf(buffer ,"%u", p->id);
   weatherStationPostData("packetUID")->setString(buffer);

   sprintf(buffer, "%.5f", p->battery);
   weatherStationPostData("battery")->setString(buffer);

   sprintf(buffer, "%u", p->ldr);
   weatherStationPostData("light")->setString(buffer);

   sprintf(buffer, "%f", p->bmpTemp);
   weatherStationPostData("bmp_tmp")->setString(buffer);

   sprintf(buffer, "%f", p->bmpPress);
   weatherStationPostData("pressure")->setString(buffer);

   sprintf(buffer, "%f", p->humidity);
   weatherStationPostData("humidity")->setString(buffer);

   sprintf(buffer, "%f", p->compensatedHumidity);
   weatherStationPostData("compensated_humidity")->setString(buffer);

   sprintf(buffer, "%f", p->htuTemp);
   weatherStationPostData("htu_tmp")->setString(buffer);

   if((result = DBHost.sendHttpQuery(HttpClient::HttpRequestMethod::POST, NULL, &weatherStationPostData)) == 0)
   {
    Serial.println("Data posted successfully");
    HttpClient::HTTP_CODE response = DBHost.isReplyAvailable(2000);
    Serial.printf("Got response code : %u\n", response);
   }
   else
   {
    Serial.printf("Failed to post data : error(%d)\n", result);
   }
   DBHost.stop();

   return result;
}

void debugStruct(WeatherStationDataPacket *p)
{
  Serial.println("##############WEATHER STATION DATA##############");
  Serial.print("ID : ");
  Serial.println(p->id);

  Serial.print("HEADER : ");
  Serial.println(p->header);
  
  Serial.printf("BATT :  %.5f V\n", p->battery);
  
  Serial.print("LDR : ");
  Serial.println(p->ldr);

  Serial.print("BMP TEMP : ");
  Serial.print(p->bmpTemp);
  Serial.println(" *C");
  
  Serial.print("BMP PRESS : ");
  Serial.print(p->bmpPress);
  Serial.println(" Pa");

  Serial.print("HUM : ");
  Serial.print(p->humidity);
  Serial.println(" %");
  
  Serial.print("COM HUM : ");
  Serial.print(p->compensatedHumidity);
  Serial.println(" %");
  
  Serial.print("HTU TEMP : ");
  Serial.print(p->htuTemp);
  Serial.println(" *C");
}

int insertIntoDB(MailboxDataPacket *p)
{
  char buffer[100] = "";
  int result(0);
  //We get the values and put it in our dictionary
   sprintf(buffer ,"%u", p->id);
   mailboxPostData("packetUID")->setString(buffer);

   sprintf(buffer, "%.5f", p->battery);
   mailboxPostData("battery")->setString(buffer);

  switch(p->mailbox_event)
  {
    case MAILBOX_LETTER:
      mailboxPostData("event")->setString("LETTER");
      break;
    case MAILBOX_PACKAGE:
      mailboxPostData("event")->setString("PACKAGE");
      break;
    case MAILBOX_COLLECTED:
      mailboxPostData("event")->setString("COLLECTED");
      break;
    default:
      break;
  }

   if((result = DBHost.sendHttpQuery(HttpClient::HttpRequestMethod::POST, NULL, &mailboxPostData)) == 0)
   {
    Serial.println("Data posted successfully");
    HttpClient::HTTP_CODE response = DBHost.isReplyAvailable(5000); //We increase the timeout because sending a mail on the server side takes some time.
    Serial.printf("Got response code : %u\n", response);
   }
   else
   {
    Serial.printf("Failed to post data : error(%d)\n", result);
   }
   DBHost.stop();

   return result;
}

void debugStruct(MailboxDataPacket *p)
{
  Serial.println("##############MAILBOX DATA##############");
  Serial.print("ID : ");
  Serial.println(p->id);

  Serial.print("HEADER : ");
  Serial.println(p->header);
  
  Serial.printf("BATT :  %.5f V\n", p->battery);
  
  Serial.print("EVENT : ");
  switch(p->mailbox_event)
  {
    case MAILBOX_LETTER:
      Serial.println("LETTER");
      break;
    case MAILBOX_PACKAGE:
      Serial.println("PACKAGE");
      break;
    case MAILBOX_COLLECTED:
      Serial.println("COLLECTED");
      break;
    default:
      break;
  }
}
