#include "NoiseSensor.h"

#include <SPI.h>    // For WiFi101
#include <WiFi101.h>
#include <senseBoxIO.h>
#include <TimeLib.h>

// ---> Fill in your WiFi name (SSID) and password here. <---
#define SECRET_SSID "CHANGEME"
#define SECRET_PASSWORD "CHANGEME"

// Keep these values like this unless you know what you're doing.
const char server[] = "api.opensensemap.org";
const uint8_t tick_count = 60;
const bool verbose = 1;

// ---> Fill this in yourself. <---
// SenseBox IDs from the web interface in order 0, 1, 2, 3, 4. Master is ID 0!
const char *sensebox_ids[] = {"CHANGEME", "", "CHANGEME", "CHANGEME", ""};
// Sensor IDs from the web interface in the same order as above.
const char *sensor_ids[] = {"CHANGEME", "", "CHANGEME", "CHANGEME", ""};

// =================

WiFiClient client;
uint32_t last_reading;

WiFiUDP udp;

static const char ntpServerName[] = "de.pool.ntp.org";
const int timeZone = 0;     // Central European Time

void setup() {
  sleep(2000);
  Serial.begin(115200);
  Serial.println("\nStarting wifi connection...");


  // Retry WiFi connection until it succeeds.
  do {
    WiFi.begin(SECRET_SSID, SECRET_PASSWORD);
    uint32_t wifi_start_time = millis();
    while (WiFi.status() != WL_CONNECTED) {
      senseBoxIO.statusRed();
      delay(500);
      Serial.print(".");
      senseBoxIO.statusGreen();
      if (millis() - wifi_start_time >= 5000) {
        Serial.println("Couldn't connect to Wifi. Try again.");
        WiFi.end();
        break;
      }
    } 
  } 
  while (WiFi.status() != WL_CONNECTED);


  Serial.print("Connected to AP ");
  Serial.println(SECRET_SSID);

  udp.begin(8888);
  setSyncProvider(getNtpTime);
  setSyncInterval(300);

  // Let the sensors know we're ready and start measuring.
  if (!NoiseSensor.begin(tick_count)) {
    Serial.println("Something didn't work. Please restart the system.");
    while (1) {
      senseBoxIO.statusRed();
      delay(100);
      senseBoxIO.statusNone();
      delay(100);
    }
  }
  NoiseSensor.setIds(sensebox_ids, sensor_ids);

  sleep(1000);
}

void loop() {
  // Shows the openSenseMap's response.
  if (!verbose) {
    while (client.available()) {
        char c = client.read();
        Serial.write(c);
    }
  }

  // This check is clumsy but will be replaced by a proper timer interrupt in the future.
  if (NoiseSensor.beaconReady()) {
    senseBoxIO.statusRed();
    NoiseSensor.sendSyncBeacon();
    senseBoxIO.statusGreen();
  }

  // This check is clumsy but will be replaced by a proper timer interrupt in the future.
  if (millis() - last_reading >= 2000 && NoiseSensor.requestReady()) {
    senseBoxIO.statusRed();

    Serial.println();
    NoiseSensor.read();

    // ---> Choose which sensor's data you want to upload by calling upload(device_id).
    upload(0, server);
    upload(2, server);
    upload(3, server);
    senseBoxIO.statusGreen();

    // Record the last upload time to make sure we don't read and upload multiple times per second.
    last_reading = millis();
  }
}

void upload(uint8_t device_id, const char* server) {
  Serial.println("Starting upload");

  if (client.connected()) {
    client.stop();
    sleep(500);
  }
  
  // If there's a successful connection:
  if (client.connectSSL(server, 443)) {
    String uploadbuf;
    String json = NoiseSensor.buildSenseBoxJSON(device_id);
    uint16_t content_length = json.length();

    uploadbuf = NoiseSensor.buildHTTPHeader(device_id, server, content_length);

    client.println(uploadbuf);   
    if (verbose) { Serial.println(uploadbuf); }

    client.println();
    if (verbose) { Serial.println(); }

    // For some reason that nobody knows, client.print has an arbitrary input string limit. 
    // Therefore, we need to split it at some arbitrary length and provide chunks.
    for (uint16_t idx = 0; idx < json.length(); idx += 1000) {
      client.print(json.substring(idx, idx + 1000));
      if (verbose) { Serial.print(json.substring(idx, idx + 1000)); }
    }
    
    client.println();
    if (verbose) { Serial.println(); }
    
    Serial.println("done!");
        
  } else {
    // If we couldn't make a connection:
    Serial.println("connection failed. Restart System.");
    sleep(1000);
  }

  Serial.println("Upload done");
}

void sleep(unsigned long ms) {     // ms: duration
  unsigned long start = millis();  // start: timestamp
  for (;;) {
    unsigned long now = millis();         // now: timestamp
    unsigned long elapsed = now - start;  // elapsed: duration
    if (elapsed >= ms)                    // comparing durations: OK
      return;
  }
}


const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
  IPAddress ntpServerIP;// = IPAddress(176, 9 , 42, 91); // NTP server's ip address

  while (udp.parsePacket() > 0) ; // discard any previously received packets
  WiFi.hostByName(ntpServerName, ntpServerIP);

  if (verbose) {
    Serial.println("Transmit NTP Request");
    Serial.print(ntpServerName);
    Serial.print(": ");
    Serial.println(ntpServerIP);
  }
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      if (verbose) { Serial.println("Receive NTP Response"); }
      udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  udp.beginPacket(address, 123); //NTP requests are to port 123
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
}
