/*  =========================================================================================
 *   
 *    CS408 Environmental Monitoring Independent of Existing Infrastructure
 *    Copyright (C) 2021 Callum Inglis
 *
 *    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 2 of the License, or
 *    (at your option) 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, write to the Free Software Foundation, Inc.,
 *    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *    
 *    Contact: Callum.Inglis.2018(at)uni.strath.ac.uk
 *    
 *  =========================================================================================
 *    
 *  Libraries Used
 *    ArduinoJson v6 Library - https://arduinojson.org/v6/doc/ (https://github.com/sandeepmistry/arduino-LoRa/blob/master/LICENSE)
 *    LoRa Library - https://github.com/sandeepmistry/arduino-LoRa/blob/master/API.md (https://github.com/bblanchon/ArduinoJson/blob/6.x/LICENSE.md)
 *    SHTSensor - https://github.com/Sensirion/arduino-sht (https://github.com/Sensirion/arduino-sht/blob/master/LICENSE)
 *
 *  =========================================================================================
 */

#include <TimeLib.h>
#include <SPI.h>
#include <Wire.h>
#include <LoRa.h>
#include <SoftwareSerial.h>
#include <ArduinoJson.h>
#include <base64.h>
#include "SHTSensor.h"

// Lora Config
#define ss 16 // Physical Pin 16 = D0 (Default is Physical Pin 5)
#define rst 0
#define dio0 15 // Physical Pin 15 = D8 (Default is Physical Pin 4)
static const int loraSpreadingFactor = 7;
static const int loraSignalBandwidth = 125E3;
static const int loraFrequency = 433E6;

// Serial PMS Config
static const int RXPin = 4, TXPin = 3;

// Temp / Humidity Sensor
SHTSensor sht;

// Particle Sensor Serial
SoftwareSerial pmsSerial(2, 3);

// LoRa Message Tracking
String sensorID = "";
byte localAddress = 0xBB;
byte destination = 0xFF;
uint32_t msgCount = 0;

// Sensor Values - Temp / Humidity
double temperature;
double humidity;
double avgTemperature = 0;
double avgHumidity = 0;

// Sensor Values - Co2
byte co2;
double avgCo2 = 0;

// Sensor Values - Particles
uint32_t ppm10;
uint32_t ppm25;
uint32_t ppm100;
uint32_t avgPpm10 = 0;
uint32_t avgPpm25 = 0;
uint32_t avgPpm100 = 0;

// Partial Sensor Data
struct pms5003data {
  uint16_t framelen;
  uint16_t pm10_standard, pm25_standard, pm100_standard;
  uint16_t pm10_env, pm25_env, pm100_env;
  uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
  uint16_t unused;
  uint16_t checksum;
};

struct pms5003data data;

// Polling / Transmitting Config
int pollingFrequency = 2500; // Polling Frequency, ms
int pollEventCount = 0; // Number of times data has been sampled in this recording period
int sendAfterPolls = 10; // Sample Period, after which Transmit Reading 
int reservationTime = 1; // How long do we require use of TX / RX airways for sending packets?

/**
 * Setup Humidity / Temperature Sensor
 */
bool setupSHT() {
  Wire.begin();

  if (!sht.init()) {
    Serial.println("[-] SHT Init Failed");
    return false;
  }
  
  sht.setAccuracy(SHTSensor::SHT_ACCURACY_MEDIUM);
  return true;
}

double getTemperature() {
  double t = -1;

  if (sht.readSample()) {
    t = sht.getTemperature();
    temperature += t;
  }

  return t; // -1 on error TODO Make this better!
}

double getHumidity() {
  double h = -1;

  if (sht.readSample()) {
    h = sht.getHumidity();
    humidity += h;
  }

  return h; // -1 on error
}

bool setupLoRa() {
  LoRa.setPins(ss, rst, dio0);  
  if (!LoRa.begin(loraFrequency)) {
    Serial.println("[-] Fatal. Starting LoRa failed!");
    return false;
  }

  LoRa.setSpreadingFactor(loraSpreadingFactor);
  LoRa.setSignalBandwidth(loraSignalBandwidth);

  Serial.println("[+] LoRa Initialized OK!");
  return true;
}

/* 
 * Return ESP Module ID, based on MAC address
 * Source: https://arduino.stackexchange.com/a/58678
 * 
 * 2021-11-07 - This method proved unreliable, switched to casting ESP.getChipID() as a string instead
 */
String getSensorUID() {
//  uint64_t chipid = ESP.getChipId();
//  uint16_t chip = (uint16_t)(chipid >> 32);
//  
//  char sensorID[23];
//  snprintf(sensorID, 23, "EMIEI-%04X%08X", chip, (uint32_t)chipid);

  String sensorID = "EMIEI-";
  sensorID.concat(String(ESP.getChipId()));
  
  return sensorID;
}

/**
 * Get data from PMS Partical Sensor
 * @param Stream s PMS Serial Connection
 * @modifies struct data PMS Sensor Data
 * @returns boolean Data Read Success status
 */
boolean readPMSdata(Stream *s) {
  if (!s->available()) {
    return false;
  }
  
  // Read a byte at a time until we get to the special '0x42' start-byte
  if (s->peek() != 0x42) {
    s->read();
    return false;
  }
 
  // Now read all 32 bytes
  if (s->available() < 32) {
    return false;
  }
    
  uint8_t buffer[32];    
  uint32_t sum = 0;
  s->readBytes(buffer, 32);
 
  // get checksum ready
  for (uint8_t i=0; i<30; i++) {
    sum += buffer[i];
  }
  
  // The data comes in endian'd, this solves it so it works on all platforms
  uint16_t buffer_u16[15];
  for (uint8_t i=0; i<15; i++) {
    buffer_u16[i] = buffer[2 + i*2 + 1];
    buffer_u16[i] += (buffer[2 + i*2] << 8);
  }

  // Struct it
  memcpy((void *)&data, (void *)buffer_u16, 30);
 
  if (sum != data.checksum) {
    Serial.print("\n Checksum failure... ");
    Serial.print("Expected: "); Serial.print(sum);
    Serial.print(", Got: "); Serial.print(data.checksum);
    return false;
  }

  return true;
}

/* Turn on Reciever, 
 * listen for messages for [listenDuration] seconds,
 * Return true if no messages recieved
 */
boolean clearToSend(int listenDuration) {  
  int listenUntil = now() + listenDuration;

  // Listen until timeout expires
  while (listenUntil >= now()) {
    int packetSize = LoRa.parsePacket();
    
    if (packetSize) {
      return false; // Other message heard on Rx, we can not transmit just now. 
    }

    delay(5);
  }

  return true; // We didn't hear anything, so continue
}

// Listen of listenDuration seconds
// If we see a response to our txHello packet, and txHello.okToTransmit is True, we can send our packet
boolean listenTxHelloAccept(int listenDuration, int messageID) {
  int time_now = now() + listenDuration;

  // Listen until timeout expires
  Serial.println("[+] Transmit - \"Hello\" - Listening for ack & clear to send");
  while (time_now >= now()) {
    int packetSize = LoRa.parsePacket();
    
    if (packetSize) {
      String incoming = "";
      char temp;
      
      while (LoRa.available()) {
        // TODO - Tidy this  up, ensure we only read in valid JSON
        temp = (char)LoRa.read();

        // Opening {
        if (incoming.length() == 0 && temp == '{') {
          incoming = "{";
          
        // Closing }  
        } else if (temp == '}') {
          incoming.concat("}");
          break;

        // Anything else that's valid
        } else if (incoming.length() > 0) {
          incoming.concat(temp);
        }
      }

      // DEBUG
//      Serial.print("in listenTxHelloAccept() Recieved: \n"); 
//      Serial.println(incoming);

      // Verify its OK to transmit
      StaticJsonDocument<200> doc;
      deserializeJson(doc, incoming);

      const bool okToTransmit = doc["okTransmit"];
      const String authIsForUid = doc["uid"];
      const int authIsForMessageID = doc["messageID"];
      const String gatewayUid = doc["gatewayUid"];

      // Verify txHello.okToTransmit is True & UID Match & Message IDs Match
      if (authIsForUid == getSensorUID()) { Serial.println("[+] Transmit - \"Hello\" - Sensor UID Match!"); } else { Serial.println("[-] Transmit - \"Hello\" - Sensor UID Mis-Match! " + String(authIsForUid) + " vs " + String(getSensorUID())); }
      if (authIsForMessageID == messageID) { Serial.println("[+] Transmit - \"Hello\" - Message ID Match!"); } else { Serial.println("[-] Transmit - \"Hello\" - MessageID Mis-Match!"); }
      
      return (okToTransmit 
        && authIsForUid == getSensorUID() 
        && authIsForMessageID == messageID);
    }

    delay(3);
  }

  Serial.println("[-] Transmit - \"Hello\" - Timeout while waiting for Clear to Send\n\n");
  return false; // We didn't hear anything, conside this as meaning "Can't Send"
}

void sendJsonPayloadWithLoRa(DynamicJsonDocument payload) {
  LoRa.beginPacket();
  serializeJson(payload, LoRa);
  LoRa.endPacket();
}

// TODO - Finish Acks & Naks
boolean transmitData(DynamicJsonDocument payload) {

  // Listen for other communication
  //    Rx - Listen for other messages, if no messages heard then continue
  if (!clearToSend(0.5)) {
    Serial.println("[-] Airways busy, could not send");
    delay(random(500, 1250)); // Introduce random delay to avoid another collision

    // TODO Refactor
    while (!clearToSend(0.5)) {
      Serial.println("[-] Airways busy, could not send");
      delay(random(500, 1250)); // Introduce random delay to avoid another collision
    }
    //return false;
  }

  Serial.println("[+] Transmit - \"Hello\"");
  // Send short "clear to send?" packet
  //    Tx - "I've got data to send!" + UID
  //    RX - Continue upon recieving "OK" + UID + TX_Auth_ID
  DynamicJsonDocument txHello(2048);
  txHello["uid"] = sensorID;
  txHello["reservationTime"] = reservationTime; // How long do we require reservation of radio?
  txHello["messageID"] = msgCount;
  sendJsonPayloadWithLoRa(txHello);

  if (!listenTxHelloAccept(reservationTime * 1.5, msgCount)) { // Can't transmit just now
    Serial.println("[-] Transmit - \"Hello\" - Can Not Transmit At This Time\n");
    return false; 
  }

  Serial.println("[+] Recieved - Clear to Transmit Payload"); // Else we have clear to send

  // Transmit Payload
  //    Tx - Send payload + UID + TX_Auth_ID
  //    Rx - Listen for Ack/Nack
  Serial.println("[+] Transmit - Payload");
  sendJsonPayloadWithLoRa(payload);


  // TODO Await Response Ack/Nak
  int ackTimeout = 2; // Seconds
  int time_now = now() + ackTimeout;

  // Listen until timeout expires
  Serial.println("[.] Transmit - Payload - Listening for Ack");
  while (time_now >= now()) {
    int packetSize = LoRa.parsePacket();
    
    if (packetSize) {
      String incoming = "";
      char temp;
      
      while (LoRa.available()) {
        // TODO - Tidy this  up, ensure we only read in valid JSON
        temp = (char)LoRa.read();

        // Opening {
        if (incoming.length() == 0 && temp == '{') {
          incoming = "{";
          
        // Closing }  
        } else if (temp == '}') {
          incoming.concat("}");
          break;

        // Anything else that's valid
        } else if (incoming.length() > 0) {
          incoming.concat(temp);
        }
      }

      // DEBUG
//      Serial.print("\nin listedForAck() Recieved: \n"); 
//      Serial.println(incoming);

      StaticJsonDocument<200> doc;
      deserializeJson(doc, incoming);

      const bool ackStatus = doc["ackStatus"];
      const String authIsForUid = doc["uid"];
      const String gatewayUid = doc["gatewayUid"];

      // Verify txHello.okToTransmit is True & UID Match
      if (authIsForUid == getSensorUID()) {
        Serial.println("[+] Transmit - Payload - Ack Recieved: " + String(ackStatus) + "\n");
        
        if (ackStatus) { return true; } // It all worked :)

        // TODO Retransmit, recover, etc
        return false;
      }

      // Else UID Mis-Match so we wait for next message
    }

    delay(5);
  }

  // TODO After Timeout we need to deal with!
  Serial.println("[-] Transmit - Payload - Ack Timeout Reached - Assuming Message Was Not Delivered");

  // TODO Listen for ack/nak
 
  // T+2  Rx OFF
  //      Handle Ack + UID + Next_Send_Interval
  //      Handle Nak + UID: GoTo T-3
  //      Handle No Response: GoTo T-3
          
  // T+3  Tx/Rx Window Expires

    
    //LoRa.write(localAddress);
//      LoRa.write(msgCount);
    //LoRa.write(outgoing.length());
    //LoRa.print(outgoing);
    
    return true;
}

void setup() {
  delay(1000);
  
  Serial.begin(115200); // Console Debug
  Serial.println("\n\n[+] Transmitter Node");

  sensorID = getSensorUID();

  pmsSerial.begin(9600); // Partical Sensor

  // Setup Sensors
  if (!setupSHT()) { while(1); } // Temp/Humidity - Die on Error

  // Setup LoRa
  if (!setupLoRa()) { while(1); } // Die on error
}

void loop() {
  // TODO Gather Sensor Data

  // Error reading PMS Data, ignore on this round
  if (!readPMSdata(&pmsSerial)) {
    data.pm10_standard = 0;
    data.pm25_standard = 0;
    data.pm100_standard = 0; 
  }
  
  pollEventCount++;
  
  Serial.println();
  Serial.print(String(pollEventCount) + ") ");
  Serial.print("PM 1.0: "); Serial.print(data.pm10_standard);
  Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_standard);
  Serial.print("\t\tPM 10: "); Serial.print(data.pm100_standard);
  Serial.print("\t\tTemp: "); Serial.print(getTemperature());
  Serial.print("\t\tHumidity: "); Serial.print(getHumidity());

  // Add to Average
  // Standard (_standard) or Environmental (_env)
  ppm10 = ppm10 + data.pm10_standard;
  ppm25 = ppm25 + data.pm25_standard;
  ppm100 = ppm100 + data.pm100_standard;

  if (pollEventCount == sendAfterPolls) {
    // Average Values over recording period
    avgPpm10 = ppm10 / sendAfterPolls;
    avgPpm25 = ppm25 / sendAfterPolls;
    avgPpm100 = ppm100 / sendAfterPolls;

    avgTemperature = temperature / sendAfterPolls;
    avgHumidity = humidity / sendAfterPolls;

    // TODO Transmit Sensor Data
    Serial.println("");
    Serial.print("Avg ppm10: "); Serial.print(avgPpm10);
    Serial.print("\t\tAvg ppm25: "); Serial.print(avgPpm25);
    Serial.print("\t\tAvg ppm100: "); Serial.print(avgPpm100);
    Serial.print("\t\tAvg Temp: "); Serial.print(avgTemperature);
    Serial.print("\t\tAvg Humidity: "); Serial.print(avgHumidity);
    Serial.print("\t\tChip ID: "); Serial.println(sensorID);
    Serial.println("");

    // LORA SEND
    DynamicJsonDocument doc(2048);

    JsonObject metadata = doc.createNestedObject("sensorMetadata");
    metadata["uid"] = sensorID;
    metadata["messageID"] = msgCount;
    metadata["samplePeriod"] = sendAfterPolls; // TODO: Multiply by poll duration!

    JsonObject data = doc.createNestedObject("data");

    JsonObject ppm = data.createNestedObject("ppm"); // Particulates
    ppm["p10"] = avgPpm10;
    ppm["p25"] = avgPpm25;
    ppm["p100"] = avgPpm100;

    JsonObject sht = data.createNestedObject("sht");  // Temp, Humidity
    sht["temperature"] = avgTemperature;
    sht["humidity"] = avgHumidity;

    JsonObject co2 = data.createNestedObject("co2"); // Co2
    co2["tmp"] = 0;

    if (transmitData(doc)) {
      Serial.println("Packet Sent\n");
      
    } else {
      int maxRetries = 10; // TODO Move to Config
      int numRetries = 1;
      
      while (!transmitData(doc) && numRetries < maxRetries){
        numRetries++;
        Serial.println("[-] Failed to send packet, retrying. Attempt " + String(numRetries) + " of " + String(maxRetries) + "\n");
        delay(reservationTime + random(1250, 5250)); // Introduce random delay to avoid another collision
      }

      if (numRetries >= maxRetries) {
        Serial.println("[-] Failed to send packet, max retries reached. Aborting");

        // TODO Don't Clear Counters, record more values then try to retransmit
      }
    }
    
    // Reset Loop Values
    ppm10 = 0;
    ppm25 = 0;
    ppm100 = 0;
    temperature = 0;
    humidity = 0;
    
    pollEventCount = 0;
    Serial.println("---------------+++++-------------------");

    msgCount++;
  }

  delay(pollingFrequency);
}