/*
 * WetterStation
 * Version: 0.0
 * Author: Hendrik Langer <hendrik+dev@xd0.de>
 */

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
extern "C" {
#include "user_interface.h"
}

#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include <MQTTClient.h>

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP085_U.h>
#include <DHT.h>

#include <RunningAverage.h>

#include <SdsDustSensor.h>

#include <XD0OTA.h>
#include "main.h"
#include "passwords.h"

const char* server PROGMEM = "ingress.opensensemap.org";

#define MQTT_MAX_PACKET_SIZE 512
const char* mqttserver PROGMEM = "home.xd0.de";
const char* mqttusername PROGMEM = "esp-weatherstation";
const char* mqttpassword PROGMEM = PWD_MQTT;

constexpr unsigned int postingInterval = 60000; //Uploadintervall in Millisekunden
constexpr unsigned int dhcp_interval = 60*60*1000;

uint32_t loop_count = 0;

#define EXTERNAL_POWER 1

//senseBox ID
#define SENSEBOX_ID "5a9e9e38f55bff001a494877"

//Sensor IDs
// Temperature
#define SENSOR1_ID "5a9e9e38f55bff001a49487e"
// Humidity
#define SENSOR2_ID "5a9e9e38f55bff001a49487d"
// Pressure (sea-level)
#define SENSOR3_ID "5a9e9e38f55bff001a49487c"
// Pressure RAW
#define SENSOR9_ID "5d12233630bde6001adf6092"
// PM10
#define SENSOR4_ID "5a9e9e38f55bff001a49487b"
// PM2.5
#define SENSOR5_ID "5a9e9e38f55bff001a49487a"
// Radioactivity
#define SENSOR6_ID "5a9e9e38f55bff001a494879"
// Voltage
#define SENSOR7_ID "5a9e9e38f55bff001a494878"
// RSSI
#define SENSOR8_ID "5a9eddb1f55bff001a51de52"


static constexpr uint8_t BMP_SCL = D4;
static constexpr uint8_t BMP_SDA = D3;

static constexpr uint8_t DHT22_PIN = D7;
static constexpr uint8_t DHTTYPE = DHT22; // DHT 22 (AM2302)

static constexpr uint8_t SDS_TX = D1;
static constexpr uint8_t SDS_RX = D2;

static constexpr uint8_t GEIGER_PIN = D6;
static constexpr float CONV_FACTOR = 0.008120 / 2.5;
static constexpr float OWN_BACKGROUND_CPS = 0;  // documentation says 0.2 (make sure value doesn't get negative if subtracting!)

#ifndef EXTERNAL_POWER
ADC_MODE(ADC_VCC);
#endif

ESP8266WiFiMulti wifiMulti;

os_timer_t Timer1;
RunningAverage geigeraverage(10);

Adafruit_BMP085_Unified bmp = Adafruit_BMP085_Unified(10085);
DHT dht(DHT22_PIN, DHTTYPE);
SdsDustSensor sds(SDS_TX, SDS_RX);
volatile unsigned long geiger_counts = 0;
unsigned long geiger_previousMillis;
unsigned long last_dhcp = 0;
unsigned long previousMillis = 0;
IPAddress ip, dns, gateway, subnet;
char ssid[64];
char password[64];

struct __attribute__((packed)) SENSOR_DATA {
  float temperature;
  float humidity;
  float pressure;
  float temp2;
  float p10;
  float p25;
  float cpm;
  float radioactivity;
  float voltage;
  float rssi;
  bool sds_updated;
} sd;

static uint32_t cal = system_rtc_clock_cali_proc(); // WARNING: UPDATING THIS WILL MAKE THE rtcMillis() RETURN A LOWER VALUE THEN BEFORE, EVEN IF TIME PASSED, BREAKING INTERVAL CHECKS!
unsigned long rtcMillis() {
  uint64_t rtc_t = system_get_rtc_time();
//  uint32_t timemicrosec = rtc_t*((uint64) ((cal * 1000) >> 12));
//  Serial.printf("cal: %d.%d \r\n", ((cal*1000)>>12)/1000, ((cal*1000)>>12)%1000 );
//  Serial.printf("cal: %u\n", cal);
  return ((uint64_t)(rtc_t*(uint64_t)cal) >> 12)/1000;
}

void ICACHE_RAM_ATTR fpm_wakup_cb_func1(void) {
  //gpio_pin_wakeup_disable();
  //ESP.wdtFeed();
  //wifi_fpm_do_wakeup();
  wifi_fpm_close();
  geiger_counts++;
}

void inline lightsleep() {
  ESP.wdtFeed();
  wifi_station_disconnect();
  wifi_set_opmode(NULL_MODE);
  wifi_fpm_set_sleep_type(LIGHT_SLEEP_T);
  wifi_fpm_open(); // Enables force sleep
  GPIO_DIS_OUTPUT(GPIO_ID_PIN(GEIGER_PIN));
  PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTCK_U,FUNC_GPIO13); // PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U,FUNC_GPIO13);
  gpio_pin_wakeup_enable(GPIO_ID_PIN(GEIGER_PIN), GPIO_PIN_INTR_LOLEVEL);
  wifi_fpm_set_wakeup_cb(fpm_wakup_cb_func1);
  wifi_fpm_do_sleep(0xFFFFFFF); // Sleep for longest possible time
  //delay(1);
}

void ICACHE_FLASH_ATTR getValuesJSON(char* buffer, const size_t buf_len, int format) {
  StaticJsonDocument<MQTT_MAX_PACKET_SIZE> jsonDoc;  // ToDo: buf_len

  if (format == 1) {
    JsonArray array = jsonDoc.to<JsonArray>();

    JsonObject temperatureObject = array.createNestedObject();
    temperatureObject["sensor"] = SENSOR1_ID;
    temperatureObject["value"] = sd.temperature;
    JsonObject humidityObject = array.createNestedObject();
    humidityObject["sensor"] = SENSOR2_ID;
    humidityObject["value"] = sd.humidity;
    JsonObject pressureObject = array.createNestedObject();
    pressureObject["sensor"] = SENSOR9_ID;
    pressureObject["value"] = sd.pressure;
    JsonObject pressureObject2 = array.createNestedObject();
    pressureObject2["sensor"] = SENSOR3_ID;
    pressureObject2["value"] = sd.pressure+16;
    if (sd.sds_updated) {
      JsonObject pm10Object = array.createNestedObject();
      pm10Object["sensor"] = SENSOR4_ID;
      pm10Object["value"] = sd.p10;
      JsonObject pm25Object = array.createNestedObject();
      pm25Object["sensor"] = SENSOR5_ID;
      pm25Object["value"] = sd.p25;
    }
    if (sd.cpm > 0) {
      JsonObject cpmObject = array.createNestedObject();
      cpmObject["sensor"] = SENSOR6_ID;
      cpmObject["value"] = sd.radioactivity;
    }
    if (sd.voltage > 2.5 ) {
      JsonObject voltageObject = array.createNestedObject();
      voltageObject["sensor"] = SENSOR7_ID;
      voltageObject["value"] = sd.voltage;
    }
    if (sd.rssi != 0) {
      JsonObject rssiObject = array.createNestedObject();
      rssiObject["sensor"] = SENSOR8_ID;
      rssiObject["value"] = sd.rssi;
    }

    serializeJson(jsonDoc, buffer, buf_len);
  } else if (format == 2) {

    jsonDoc["temperature"] = sd.temperature;
    jsonDoc["humidity"] = sd.humidity;
    jsonDoc["pressure"] = sd.pressure;
    if (sd.sds_updated) {
      jsonDoc["pm10"] = sd.p10;
      jsonDoc["pm2.5"] = sd.p25;
    }
    if (sd.cpm > 0) {
      jsonDoc["cpm"] = sd.cpm;
      jsonDoc["radioactivity"] = sd.radioactivity;
    }
    if (sd.voltage > 2.5 ) {
      jsonDoc["voltage"] = sd.voltage;
    }
    if (sd.rssi != 0) {
      jsonDoc["rssi"] = sd.rssi;
    }
//    jsonDoc["millis"] = millis();
//    jsonDoc["heap"] = ESP.getFreeHeap();

    serializeJson(jsonDoc, buffer, buf_len);
  }
}

void ICACHE_FLASH_ATTR sendValues() {
  sd.temperature = dht.readTemperature();
  sd.humidity = dht.readHumidity();

  bmp.getPressure(&(sd.pressure));
  sd.pressure /= 100;
  bmp.getTemperature(&(sd.temp2));

#ifndef EXTERNAL_POWER
  sd.voltage = ESP.getVcc()/1024.0;
#else
  sd.voltage = analogRead(A0)*0.04285078 -0.05942125; // by linear regression for my(!) voltage divider. else: sd.voltage = analogRead(A0)*3.3*(R1+R2)/(R2*1024)
#endif

//  sds.wakeup(); delay(30000); // working 30 seconds
  PmResult pm = sds.queryPm();
  sd.sds_updated = false;
  if(pm.isOk() && (pm.pm25 != sd.p25 || pm.pm10 != sd.p10)) {
    sd.p25 = pm.pm25;
    sd.p10 = pm.pm10;
    sd.sds_updated = true;
  } else {
    sd.sds_updated = false;
  }
//  sds.sleep();

  if (rtcMillis() - geiger_previousMillis > 10000) {
    sd.cpm = geiger_counts * 60000 / (rtcMillis() - geiger_previousMillis);
    geiger_previousMillis = rtcMillis();
    geiger_counts = 0;
    geigeraverage.addValue(sd.cpm);
  }
  //geiger_runningaverage = geigeraverage.getAverage()*10;
  float constexpr own_cpm = OWN_BACKGROUND_CPS * 60;
  sd.radioactivity = (sd.cpm - own_cpm) * CONV_FACTOR;

                  DEBUG_MSG("Temperature  : %6.2f°C    (DHT22)\n", sd.temperature);
                  DEBUG_MSG("Humidity     : %6.2f%%     (DHT22)\n", sd.humidity);
                  DEBUG_MSG("Temperature  : %6.2f°C    (BMP180)\n", sd.temp2);
                  DEBUG_MSG("Pressure     : %6.2fhPa   (BMP180)\n", sd.pressure);
  if (sd.sds_updated) DEBUG_MSG("Particles 10 : %6.2fµg/m³ (SDS011)\n", sd.p10);
  if (sd.sds_updated) DEBUG_MSG("Particles 2.5: %6.2fµg/m³ (SDS011)\n", sd.p25);
  if (sd.cpm > 0)    DEBUG_MSG("Radiation    : %6.2fµSv/h (J305)\n", sd.radioactivity);
                  DEBUG_MSG("Voltage      : %6.2fV     (ESP8266)\n", sd.voltage);

  char buffer[MQTT_MAX_PACKET_SIZE];
  getValuesJSON(buffer, MQTT_MAX_PACKET_SIZE, 1);

  WiFi.forceSleepWake();
  delay(1);  // yield();
  WiFi.mode(WIFI_STA);

  if ( ip != INADDR_NONE && dns != INADDR_NONE && gateway != INADDR_NONE && subnet != INADDR_NONE
       && ((ip[0] == 192 && ip[1] == 168) || (ip[0] == 172 && ip[1] == 16))
       && strlen(ssid) > 0 && strlen(password) > 0
       && (rtcMillis() - last_dhcp < dhcp_interval)
     ) {

    DEBUG_MSG("static ip\n");
    WiFi.config(ip, dns, gateway, subnet);
    WiFi.begin(ssid, password);
    int tries = 0;
    constexpr unsigned int retry_delay = 500;
    constexpr unsigned int max_retry_delay = 10000;
    while (WiFi.status() != WL_CONNECTED) {
      tries++;
      DEBUG_MSG(".");
      if (tries*retry_delay >= max_retry_delay) {
        DEBUG_MSG(" [ERROR]\n");
        DEBUG_MSG("Rebooting..\n");
        ESP.restart();
      }
      delay(retry_delay);
    }
    DEBUG_MSG(" [CONNECTED, static]\n");
    DEBUG_MSG("IP address: ");
    DEBUG_MSG("%s\n", String(WiFi.localIP()).c_str());

  } else {
    DEBUG_MSG("dhcp\n");
    int tries = 0;
    constexpr unsigned int retry_delay = 500;
    constexpr unsigned int max_retry_delay = 12000;
    while (wifiMulti.run() != WL_CONNECTED) {
      tries++;
      DEBUG_MSG(".");
      if (tries*retry_delay >= max_retry_delay) {
        DEBUG_MSG(" [ERROR]\n");
        DEBUG_MSG("Rebooting..\n");
        ESP.restart();
      }
      delay(retry_delay);
    }
    DEBUG_MSG(" [CONNECTED, dhcp]\n");
    DEBUG_MSG("IP address: ");
    DEBUG_MSG("%s\n", String(WiFi.localIP()).c_str());

    ip = WiFi.localIP();
    dns = WiFi.dnsIP();
    gateway = WiFi.gatewayIP();
    subnet = WiFi.subnetMask();
    strncpy(ssid, WiFi.SSID().c_str(), 64);
    strncpy(password, WiFi.psk().c_str(), 64);
    last_dhcp = rtcMillis();
  }

  sd.rssi = WiFi.RSSI();

  int httpCode = 0;
  for (int tries=0; tries<3 && httpCode != HTTP_CODE_CREATED; tries++) {

    HTTPClient httpclient;
    char url[100];
    sprintf(url, "http://%s/boxes/%s/data", server, SENSEBOX_ID);

    httpclient.begin(url);
    httpclient.addHeader("Content-Type", "application/json");

    httpCode = httpclient.POST(buffer);

    if (httpCode > 0) {
      if (httpCode == HTTP_CODE_CREATED) {
        #ifdef USERDEBUG
        httpclient.writeToStream(&Serial);
        DEBUG_MSG("\n");
        #endif
      } else {
        DEBUG_MSG("[HTTP] POST... failed, error: %s\n", httpclient.errorToString(httpCode).c_str());
      }
    }

    httpclient.end();

  }

  WiFiClientSecure net;
  MQTTClient mqttclient(MQTT_MAX_PACKET_SIZE);

  mqttclient.begin(mqttserver, 8883, net);
  int tries = 0;
  constexpr unsigned int retry_delay = 500;
  constexpr unsigned int max_retry_delay = 5000;
  while (!mqttclient.connect(mqttusername, mqttusername, mqttpassword)) {
    tries++;
    DEBUG_MSG(".");
    if (tries*retry_delay >= max_retry_delay) {
      DEBUG_MSG(" [ERROR]\n");
      DEBUG_MSG("Rebooting..\n");
      ESP.restart();
    }
    delay(retry_delay);
  }

  mqttclient.loop();
  getValuesJSON(buffer, MQTT_MAX_PACKET_SIZE, 2);
  if (mqttclient.publish("sensor/esp-weatherstation/01/json", buffer, strlen(buffer))) {
    DEBUG_MSG("mqtt done\n");
  } else {
    DEBUG_MSG("mqtt failed\n");
  }
  mqttclient.loop();
  mqttclient.disconnect();

  if (sd.rssi == 0) {  // re-read rssi if zero
    sd.rssi = WiFi.RSSI();
  }

  if (loop_count == 1) {
    XD0OTA ota;
    ota.update();
  } else if (loop_count > 720) { // do an update every 12h
    loop_count = 0;
  }
  loop_count++;

  WiFi.disconnect();
  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();
  delay(1); // yield();

}

// geiger event when gpio pulled low (called vin on board)
/*void ICACHE_RAM_ATTR ISR_geiger_impulse() {
  geiger_counts++;
  DEBUG_MSG("X");
}*/


void setup() {
  #ifdef USERDEBUG
  Serial.begin(115200);
  #endif

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);  // turn OFF board led

  //wifi_status_led_uninstall();

  sds.begin();
  sds.setCustomWorkingPeriod(5);  // sensor sends data every 5 minutes
  sds.setQueryReportingMode();  // ensures sensor is in 'query' reporting mode

  Wire.begin(BMP_SDA, BMP_SCL);
  if (!bmp.begin(BMP085_MODE_STANDARD)) {
    DEBUG_MSG("No valid BMP085 sensor!\n");
  }

  dht.begin();

  pinMode(GEIGER_PIN, INPUT);
  //attachInterrupt(digitalPinToInterrupt(GEIGER_PIN), ISR_geiger_impulse, FALLING);

  geigeraverage.clear();

  //WiFi.persistent(false); // don't load and save credentials to flash
  WiFi.mode(WIFI_STA);
  wifiMulti.addAP("nether.net", PWD_NETHERNET);
  wifiMulti.addAP("LNet", PWD_LNET);
  wifiMulti.addAP("hw1_gast", PWD_HW1);
  wifiMulti.addAP("Freifunk", "");

  previousMillis = rtcMillis();
  DEBUG_MSG("ready.\n"); //Serial.flush();
}

void loop() {
  unsigned long currentMillis = rtcMillis();
  if ((currentMillis - previousMillis) >= postingInterval) {
    previousMillis = currentMillis;
    DEBUG_MSG("sending values... previousMillis:%u, millis():%u, difference:%d\n", previousMillis, currentMillis, currentMillis-previousMillis);
    sendValues();
    DEBUG_MSG("resetting geiger counts. was: %d\n", geiger_counts);
    geiger_previousMillis = rtcMillis();
    geiger_counts = 0;
  }
  DEBUG_MSG("sleeping now... (%u)\n", currentMillis);
  DEBUG_MSG("heap: %d\n", ESP.getFreeHeap());

  delay(10);
  lightsleep();
  delay(10); // debounce?
}