// Standard ESP8266 libs from project folder
#include <ESP8266mDNS.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiType.h>

#include <esp.h>
#include <user_interface.h>
#include <WiFiClient.h>  // WiFiClient

// Project includes
#include "constants.h"
#include "config.h"
#include "config_user.h"

#ifndef DISABLE_WIFIMANAGER
#include <WiFiManager.h>  // WiFiManager from bib manager
#endif

#ifdef HTTP_CALL_ON_WINDSPEED_EXCEED
#include <ESP8266HTTPClient.h>
#endif

#ifdef HTTP_CALL_SEND_JSON_DATA
#include <ESP8266HTTPClient.h>
#include <UrlEncode.h> // from bib manager UrlEncode by Masayuki
#endif

//*************************************************************************//
// check if some settings are correct

#ifdef HTTP_CALL_ON_WINDSPEED_EXCEED
#if (HTTP_CALL_ON_WINDSPEED_INTERVAL_S < WIND_SENSOR_MEAS_TIME_S)
#error "HTTP_CALL_ON_WINDSPEED_INTERVAL_S < WIND_SENSOR_MEAS_TIME_S"
#endif
#endif

//*************************************************************************//
// constant variables

const uint8_t VALUES = 8;  // see constants.h file - count of number of SENSOR_ defines
float currentSensorData[VALUES] = { nanf("no value"), nanf("no value"), nanf("no value"),
                                    nanf("no value"), nanf("no value"), nanf("no value"),
                                    nanf("no value"), nanf("no value") };

uint32_t update_sensor_cnt = 0;
uint32_t update_webserver_cnt = 0;
uint32_t update_windspeed_exceed_cnt = 0;
uint32_t wifi_check_interval_counter = 0;
uint32_t http_call_send_json_data_cnt = 0;

#ifndef DISABLE_WIFIMANAGER
const String wifiName = "oko-weather-" + DEVICE_NAME;

WiFiManager wifiManager;
#endif

uint8_t fsm_state = FSM_STATE_1;

uint8_t sensor_cnt = 0;

boolean validData = false;

boolean do_not_read_windsensor = false;

uint32_t wifi_reconnect_cnt = 0;

//*************************************************************************//

void debug(String x) {
#ifdef DEBUG
  Serial.println(x);
#endif

#ifdef USE_LOGGER
  logdata(String(millis()) + ":" + x);
#endif
}

//*************************************************************************//

void setup() {

#ifdef ENABLE_WATCHDOG
  //wdt_disable();
#endif

#if defined(DEBUG) || defined(SERIAL_FEATURE) || defined(DEBUG_RESET_REASON)
  Serial.begin(SERIAL_BAUD_RATE);
#endif

#ifdef DEBUG_RESET_REASON
  debugResetReason();
#endif

  // Pin settings
  pinMode(BAT_CHARGED_PIN, INPUT);
  pinMode(BAT_CHARGING_PIN, INPUT);
  pinMode(STATUS_LED_PIN, OUTPUT);
  pinMode(ANEMOMETER_PIN, INPUT_PULLUP);
  pinMode(A0, INPUT);

  digitalWrite(STATUS_LED_PIN, HIGH);

#ifndef BAT_PINS_D34
  debug("D5 D6 used as battery pins");
#else
  debug("D3 D4 used as battery pins");
#endif

#ifdef BATTERY_POWERED
  criticalBatCheck();
#endif

  wifiConnect();
  debug("Connected!");

  initWifiBasedSW();

  initSensors();

  //It's magic! leave in
  delay(100);

#ifdef BATTERY_POWERED
  debug("battery powered");

  _battery_mode_main();

  digitalWrite(STATUS_LED_PIN, HIGH);

  criticalBatCheck();

  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();

  debug("deep sleep");

  // the ESP.deepSleep requires microseconds as input, after the
  // sleep the system will run into the setup routine
  ESP.deepSleep(POWERSAVING_SLEEP_S * 1000000, WAKE_RF_DEFAULT);
  delay(100);

#else  // not in battery mode

#ifdef ENABLE_WATCHDOG
  wdt_disable();
  wdt_reset();
  // Enable the internal watchdog
  wdt_enable(WATCHDOG_TIMEOUT_MS);
  debug("Watchdog enabled");
#endif
#endif
}

//*************************************************************************//

void initWifiBasedSW() {

#ifdef INFLUXDB_FEATURE
  influxdb_begin();
#endif

#ifdef WEBUPDATER_FEATURE
#ifndef BATTERY_POWERED
  setupWebUpdater(DEVICE_NAME, WiFi.localIP().toString());
#endif
#endif
}

//*************************************************************************//

void initSensors() {
  // Initialize and configure the sensors
#ifdef SENSOR_APDS9930
  if (sensor_apds9930_begin();
#endif

#ifdef SENSOR_APDS9960
  sensor_apds9960_begin();
#endif

#ifdef SENSOR_BME280
  //Temperature + pressure + humidity
  sensor_bme280_begin(BME_ADDRESS);
#endif

#ifdef SENSOR_BMP280
  //Temperature + pressure + humidity
  sensor_bmp280_begin(BMP_ADDRESS);
#endif
}

//*************************************************************************//

float readSensors(uint8_t s) {
  float ret = nan("no value");

  switch (s) {
    case SENSOR_LIGHT:  // Initialize and configure the sensors
#ifdef SENSOR_APDS9930
      ret = apds9930_light();
#endif
#ifdef SENSOR_APDS9960
      ret = apds9960_light();
#endif
      break;

    case SENSOR_TEMPERATURE:
#ifdef SENSOR_BME280
      ret = bme280_temperature();
#endif
#ifdef SENSOR_BMP280
      ret = bmp280_temperature();
#endif
      break;

    case SENSOR_HUMIDITY:
#ifdef SENSOR_BME280
      ret = bme280_humidity();
#endif
      break;

    case SENSOR_PRESSURE:
#ifdef SENSOR_BME280
      ret = bme280_pressure();
#endif
#ifdef SENSOR_BMP280
      ret = bmp280_pressure();
#endif
      break;

    case SENSOR_WINDSPEED:
#ifdef SENSOR_WIND
      if (do_not_read_windsensor == false) {
        ret = wind_speed();
      }
#endif
      break;

    case SENSOR_BAT_VOLTAGE:
#ifdef SENSOR_BATTERY
      ret = battery_voltage();
#endif
      break;

    case SENSOR_ESAVEMODE:
#ifdef SENSOR_BATTERY
      ret = battery_charging();
#endif
      break;

    case SENSOR_BATCHARGESTATE:
#ifdef SENSOR_BATTERY
      ret = isEnergySavingMode();
#endif
      break;

    default:
      break;
  }

  return ret;
}

//*************************************************************************//

void wifiConnectionCheck() {

  if ((wifi_check_interval_counter + WIFI_CHECK_INTERVAL_MS) > millis()) {
    // if check interval is not exceeded abort check
    return;
  }

  wifi_check_interval_counter = millis();

  if (WiFi.status() == WL_CONNECTED) {
    // if we are connected
    return;
  }

  // TODO Was muss hier getan werden?

}

#ifdef DISABLE_WIFIMANAGER
void wifi_disconnected(WiFiEvent_t event) {

  if (wifi_reconnect_cnt >= 5) {

    debug("\nReboot, to much reconnects to wifi done before");
    ESP.restart();

  } else {

    debug("no wifi connection, try to reconnect " + String(wifi_reconnect_cnt));

    wifi_reconnect_cnt++;
    wifiConnect();

#ifdef WEBUPDATER_FEATURE
    setWifiReconnectCnt(wifi_reconnect_cnt);
#endif
  }

}
#endif

//*************************************************************************//

void wifiConnect() {

  // Establish WiFi connection if not already applied

#ifndef DISABLE_WIFIMANAGER

  wifiManager.setMinimumSignalQuality(WIFI_MINIMUM_SIGNAL_QUALITY);
  // the time in seconds to wait for the known wifi connection
  wifiManager.setConnectTimeout(WIFI_AUTOCONNECT_TIMEOUT_S);
  // the time in seconds to wait for the user to configure the device
  wifiManager.setTimeout(WIFI_CONFIG_PORTAL_TIMEOUT_S);

  while (!wifiManager.autoConnect(wifiName.c_str(), "DEADBEEF")) {

#ifdef SLEEP_IF_NO_WLAN_CONNECTION
    // If autoconnect to WLAN failed and no client connected, go to deep sleep
    ESP.deepSleep(POWERSAVING_SLEEP_S * 1000000, WAKE_RF_DEFAULT);
    delay(100);
#endif

#ifndef SLEEP_IF_NO_WLAN_CONNECTION
    // sleep a few seconds and go on trying to connect
    debug("WiFi connection failed, try again in 5 seconds...");
    delay(5000);
#endif
  }

#else  // DISABLE_WIFIMANAGER is defined

  if (!WiFi.config(local_IP, gateway, subnet)) {
    debug("Failed to set IP configuration");
  } else {
    debug("Successful set IP configuration");
  }

  // bind the check function to the disconnected wifi event
  WiFi.onEvent(wifi_disconnected, WIFI_EVENT_STAMODE_DISCONNECTED);

  WiFi.begin(WIFI_SSID, WIFI_PASSWD);
  WiFi.setAutoReconnect(true);

  debug("Connecting to WLAN");

  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }

#endif  // DISABLE_WIFIMANAGER
}

//*************************************************************************//

#ifdef BATTERY_POWERED
void criticalBatCheck() {
  float volt = battery_voltage();
  if (volt <= BAT_EMERGENCY_DEEPSLEEP_VOLTAGE) {
    debug("Bat Voltage: " + String(volt) + " V");
    debug("Low battery, going into deep sleep.");
    // Casting to an unsigned int, so it fits into the integer range
    ESP.deepSleep(1U * EMERGENCY_SLEEP_S * 1000000u);  // battery low, shutting down
    delay(100);
  }
}
#endif

//*************************************************************************//

void loop() {

#ifdef BATTERY_POWERED
  delay(50);
  return;

#else // not in BATTERY_POWERED mode

#ifdef ENABLE_WATCHDOG
  WDT_FEED();
#endif

  // call fsm loop function
  _fsm_loop();

  // Needed to give WIFI time to function properly
  delay(DELAY_LOOP_MS);

#endif
}

//*************************************************************************//

#ifndef BATTERY_POWERED
void _fsm_loop()
{

#ifdef WEBUPDATER_FEATURE
  if ((update_webserver_cnt + (UPDATE_WEBSERVER_INTVERVAL_MS)) <= millis())
  {
    //debug("web updater call");
    update_webserver_cnt = millis();
    doWebUpdater();
  }
#ifdef ENABLE_WATCHDOG
  WDT_FEED();
#endif
#endif

#ifdef HTTP_CALL_SEND_JSON_DATA
  if ((http_call_send_json_data_cnt + (HTTP_CALL_SEND_JSON_DATA_INTERVAL_S * 1000)) <= millis() and validData == true)
  {
    // send the data to the server
    debug("Sending weather json data to http webserver");
    //debug(String(0) + "=" + String(currentSensorData[0]));
    //debug(String(SENSOR_TEMPERATURE) + "=" + String(currentSensorData[SENSOR_TEMPERATURE]));
    http_call_send_json_data_cnt = millis();
    http_call_send_json_data();
  }
#ifdef ENABLE_WATCHDOG
  WDT_FEED();
#endif
#endif

  switch (fsm_state)
  {

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_1:
      //debug("wind speed exceeded check if required");
#ifdef HTTP_CALL_ON_WINDSPEED_EXCEED
      if ((update_windspeed_exceed_cnt + (HTTP_CALL_ON_WINDSPEED_INTERVAL_S * 1000)) <= millis()) {
        debug("reading wind sensor exceed");

        // reset the wait timer to get a value every HTTP_CALL_ON_WINDSPEED_INTERVAL_S independently to the runtime of the measurement
        update_windspeed_exceed_cnt = millis();

        // start measurement of wind speed
        start_measure_wind();

        fsm_state = FSM_STATE_11;  // wait untile the wind meas time exceeded
        break;                     // abort case here to prevent read of next sensor in list
      }
#endif
      fsm_state = FSM_STATE_2;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_2:
      //debug("reset time check if required");
#ifdef RESET_ESP_TIME_INTERVAL
      // if millis() reached interval restart ESP
      if (RESET_ESP_TIME_INTERVAL_MS <= millis()) {
        debug("resetting firmware intentionally");
        // Push reset button after flashing once or do a manual power cycle to get the functionality working.
        ESP.restart();
      }
#endif
      fsm_state = FSM_STATE_3;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_3:
      wifiConnectionCheck();
      fsm_state = FSM_STATE_4;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_4:
      //debug("disable measure of wind speed if required");
#ifdef defined(BATTERY_POWERED) && defined(SENSOR_WIND)
      if (energySavingMode() == 1) {
        // Disable expensive tasks
        //sensors[SENSOR_WINDSPEED] = 0;
        //debug("read of wind sensor because of low battery disabled");
        do_not_read_windsensor = true;

      } else {
        //sensors[SENSOR_WINDSPEED] = &wind_speed;
        //debug("read of wind sensor because of high battery enabled");
        do_not_read_windsensor = false;
      }
#endif
      sensor_cnt = 0;
      fsm_state = FSM_STATE_5;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_5:
      //debug("read sensor data check");
      if ((update_sensor_cnt + (UPDATE_SENSOR_INTERVAL_S * 1000)) <= millis() or validData == false) {
        debug("read sensor data " + String(sensor_cnt));

        if (sensor_cnt != SENSOR_WINDSPEED) {
          // read data from sensor
          currentSensorData[sensor_cnt] = readSensors(sensor_cnt);
          //debug(String(sensor_cnt) + "=" + String(currentSensorData[sensor_cnt]));

        } else {

          start_measure_wind();     // start measurement of wind speed
          fsm_state = FSM_STATE_9;  // wait untile the wind meas time exceeded
          break;                    // abort case here to prevent read of next sensor in list
        }

        if (sensor_cnt < VALUES - 1) {
          sensor_cnt++;
          fsm_state = FSM_STATE_5;  // jump to same state again, more sensors to read

        } else {

          update_sensor_cnt = millis();  // reset the update interval counter
          sensor_cnt = 0;
          fsm_state = FSM_STATE_6;  // next state
        }

      } else {

        //debug("skip read sensor data");
        fsm_state = FSM_STATE_1;  // no new data, reset FSM
      }
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_6:
      //debug("log to serial if required");
#ifdef SERIAL_FEATURE
      logToSerial(currentSensorData);
#endif
      fsm_state = FSM_STATE_7;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_7:
      //debug("send data to influxdb if required");
#ifdef INFLUXDB_FEATURE
      for (uint8_t i = 0; i < 5 and validData == false; i++) {  // only check sensor data 0 to 4 -> SENSOR_TEMPERATURE, SENSOR_HUMIDITY, SENSOR_LIGHT, SENSOR_WINDSPEED, SENSOR_PRESSURE
        if (currentSensorData[i] != 0 and currentSensorData[i] != nanf("no value") and (not isnan(currentSensorData[i]))) {
          validData = true;
        }
      }

      if (validData == true) {
        // send data only if valid data is available
        pushToInfluxDB(DEVICE_NAME, currentSensorData);
      }
#endif
      fsm_state = FSM_STATE_8;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_8:
      //debug("set sensor data in webupdater if required");
#ifdef WEBUPDATER_FEATURE
#ifdef SHOW_SENSOR_DATA_ON_WEBUPDATER_MAIN_PAGE
      setSensorData(currentSensorData);
#endif
#endif

      fsm_state = FSM_STATE_1;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_9:
#ifdef SENSOR_WIND
      if (check_measure_wind_done() == false) {
        //debug("wait for wind sensor finish");
        fsm_state = FSM_STATE_9;  // stay here until the wind measurement is done
      } else {
        //debug("wind sensor read finish");
        fsm_state = FSM_STATE_10;
      }
#else
      // in case that the wind sensor is not used skip this step
      sensor_cnt++;
      fsm_state = FSM_STATE_5;
#endif
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_10:
#ifdef SENSOR_WIND
      currentSensorData[SENSOR_WINDSPEED] = measure_wind_result();
      debug("wind sensor " + String(currentSensorData[SENSOR_WINDSPEED]));
#endif
      // step into read of next sensor read
      sensor_cnt++;
      fsm_state = FSM_STATE_5;
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_11:
#ifdef SENSOR_WIND
      if (check_measure_wind_done() == false) {
        //debug("wait for wind sensor finish");
        fsm_state = FSM_STATE_11;  // stay here until the wind measurement is done
      } else {
        //debug("wind sensor read finish");
        fsm_state = FSM_STATE_12;
      }
#else
      // in case that the wind sensor is not used skip this step
      sensor_cnt++;
      fsm_state = FSM_STATE_5;
#endif
      break;

    /* -------------------------------------------------------------------------------- */
    case FSM_STATE_12:
#ifdef HTTP_CALL_ON_WINDSPEED_EXCEED
      currentSensorData[SENSOR_WINDSPEED] = measure_wind_result();
      debug("wind sensor value " + String(currentSensorData[SENSOR_WINDSPEED]));

      if (currentSensorData[SENSOR_WINDSPEED] >= HTTP_CALL_ON_WINDSPEED_EXCEED_MPS) {
        // windspeed exceeded send http call
        digitalWrite(STATUS_LED_PIN, LOW);

        // call the url HTTP_CALL_ON_WINDSPEED_URL
        WiFiClient client;
        HTTPClient http;

        http.begin(client, String(HTTP_CALL_ON_WINDSPEED_URL).c_str());
        // Send HTTP GET request
        int httpResponseCode = http.GET();

#ifdef ENABLE_WATCHDOG
        WDT_FEED();
#endif

        if (httpResponseCode > 0) {
          String response = http.getString();
          debug("http response code: " + String(httpResponseCode) + " = " + response);
          // TODO handle response
        }

        http.end();
        debug("Called windspeed exceed callout");
        digitalWrite(STATUS_LED_PIN, HIGH);
      }

#ifdef WEBUPDATER_FEATURE
#ifdef SHOW_SENSOR_DATA_ON_WEBUPDATER_MAIN_PAGE
      sentWindspeed(currentSensorData[SENSOR_WINDSPEED]);
#endif // SHOW_SENSOR_DATA_ON_WEBUPDATER_MAIN_PAGE
#endif // WEBUPDATER_FEATURE
#endif // HTTP_CALL_ON_WINDSPEED_EXCEED

      // step into read of next fsm state
      fsm_state = FSM_STATE_2;
      break;

    /* -------------------------------------------------------------------------------- */
    default:
      fsm_state = FSM_STATE_1;
      break;

  }  // close of switch

  //debug("FSM state = " + String(fsm_state));
  /*if (fsm_state == FSM_STATE_1)
  {
    debug("----------");
  }*/
}
#endif

//*************************************************************************//

void _battery_mode_main() {

  if (energySavingMode() == 1) {
    // Disable expensive tasks
    //debug("read of wind sensor because of low battery disabled");
    do_not_read_windsensor = true;

  } else {
    //debug("read of wind sensor because of high battery enabled");
    do_not_read_windsensor = false;
  }

  for (uint8_t i = 0; i < VALUES; i++) {
    currentSensorData[i] = readSensors(i);
  }

#ifdef SERIAL_FEATURE
  logToSerial(currentSensorData);
#endif

#ifdef INFLUXDB_FEATURE
  pushToInfluxDB(DEVICE_NAME, currentSensorData);
#endif

#ifdef WEBUPDATER_FEATURE
#ifdef SHOW_SENSOR_DATA_ON_WEBUPDATER_MAIN_PAGE
  setSensorData(currentSensorData);
#endif
#endif
}

//*************************************************************************//

#ifdef SERIAL_FEATURE
void logToSerial(float sensorValues[]) {
  Serial.println("");
  Serial.println("Current readings:");
  Serial.println("Temperature: " + String(sensorValues[SENSOR_TEMPERATURE]) + " °C");
  Serial.println("Humidity: " + String(sensorValues[SENSOR_HUMIDITY]) + " %");
  Serial.println("Light: " + String(sensorValues[SENSOR_LIGHT]) + " Lux");
  Serial.println("Windspeed: " + String(sensorValues[SENSOR_WINDSPEED]) + " km/h");
  Serial.println("Pressure: " + String(sensorValues[SENSOR_PRESSURE]) + " hPa");
  Serial.println("Bat Voltage: " + String(sensorValues[SENSOR_BAT_VOLTAGE]) + " V");
  Serial.println("Bat charge state: " + String(sensorValues[SENSOR_BATCHARGESTATE]));
  Serial.println("Energy saving: " + String(sensorValues[SENSOR_ESAVEMODE]));
}
#endif

//*************************************************************************//

#ifdef HTTP_CALL_SEND_JSON_DATA

String getJsonData()
{
  debug(String(SENSOR_TEMPERATURE) + "=" + String(currentSensorData[SENSOR_TEMPERATURE]));
  String msg = hb_ws_msg_start +
              hb_ws_msg_temp +
              String(currentSensorData[SENSOR_TEMPERATURE], 2) +
              ", " +
              hb_ws_msg_humi +
              String((isnan(currentSensorData[SENSOR_HUMIDITY]) ? 0.0 : currentSensorData[SENSOR_HUMIDITY]), 2) +
              ", " +
              hb_ws_msg_light +
              String(currentSensorData[SENSOR_LIGHT], 0) + // The light level for the homebridge-http-lux2 plugin is only able to parse integer values
              ", " +
              hb_ws_msg_windspeed +
              String(currentSensorData[SENSOR_WINDSPEED], 2) +
              ", " +
              hb_ws_msg_pressure +
              String(currentSensorData[SENSOR_PRESSURE], 2) +
              ", " +
              hb_ws_msg_timestamp +
              String(millis()) +
              ", " +
              hb_ws_msg_valid +
              String(validData) +
              hb_ws_msg_end;

  return msg;
}

void http_call_send_json_data()
{
  //debug("http call to  " + String(HTTP_CALL_SEND_JSON_DATA_URL));
  //debug(String(SENSOR_TEMPERATURE) + "=" + String(currentSensorData[SENSOR_TEMPERATURE]));
  // windspeed exceeded send http call
  digitalWrite(STATUS_LED_PIN, LOW);

  // call the url HTTP_CALL_SEND_JSON_DATA_URL
  WiFiClient client;
  HTTPClient http;

  String tmp_str = HTTP_CALL_SEND_JSON_DATA_URL + urlEncode(getJsonData());

  debug("send: " + tmp_str);

  http.begin(client, String(tmp_str).c_str());
  // Send HTTP GET request
  int httpResponseCode = http.GET();

#ifdef ENABLE_WATCHDOG
  WDT_FEED();
#endif

  if (httpResponseCode > 0) {
    String response = http.getString();
    debug("http response code: " + String(httpResponseCode) + " = " + response);
    // TODO handle response
  }

  http.end();
  digitalWrite(STATUS_LED_PIN, HIGH);
}

#endif

#ifdef DEBUG_RESET_REASON
void debugResetReason() {

  rst_info *resetInfo;

  // 1. read eeprom reset reason
  //int eep_reset_reason = eeprom_read(0);
  //debug("EEPROM reset reason " + String(eep_reset_reason));

  // 2. read real reset reason
  int reset_reason = resetInfo->reason;

  debug("New reset reason " + String(reset_reason));
  //eeprom_write(0, reset_reason);
}
#endif
