#include "EventType.h"
#include "UDPIO.h"
#include "UrsHelpers.h"
#include <UrsUdpSerial.h>
#include <UrsTimeSource.h>
#include <UrsTimeElements.h>
#include <UrsTime.h>
#include <UrsSystemTimeServer.h>
#include <UrsNTPTimeSource.h>
#include <ESP8266WiFi.h>
//#include <ESPAsyncWebServer.h>
#include <UrsAsyncWebServer.h>
#include <UrsWiFi.h>
#include <UrsOTA.h>
#include "AppVersion.h"
#include "Globals.h"
#include "EMSettings.h"
#include "EMPersistence.h"
#include "SignalLed.h"
#include "EnergyWebServer.h"
#include "EnergySensor.h"
#include "Relais.h"
#include <sstream>

// https://github.com/me-no-dev/ESPAsyncWebServer
// https://github.com/olehs/PZEM004T
// https://wifi-iot.com/p/wiki/15/en/
// https://github.com/plerup/espsoftwareserial
// https://wiki.cuvoodoo.info/doku.php?id=spark_abacus

// ToDo: Defaults für UDP
// ToDo: Das UDP-Inferface auf Datagramme umstellen und an den Absender zurücksenden ? Events? Liste mit Clients für events?

UrsNTPTimeSource NTPTimeSource;
UrsSystemTimeServer SystemTimeServer(NTPTimeSource);

Print* EnergyMeterLog = &Serial;
UrsUdpSerial UdpLog(2048); // Log-Ausgaben

wl_status_t oldStatus; // Zur Ekennung eines Status-Wechsel
bool DataLog = true;   // Schalter für Daten-Log

// ToDo: Nach Verbindungsverlust diese neu aufbauen
// ToDo: Zeit für Messwerte, wenn keine Zeitquelle
// ToDo: Start-Knopf, Reset, Stopp etc.



void ConfigChangedHandler() {
  SignalLed.config(EMSettings.Brightness, EMSettings.Flash, EMSettings.FlashDuration, EMSettings.FlashInterval * 1000);
}

void EnergySensorReset(float startValue, time_t startTime) {
  EMPersistence.startValue = startValue;
  EMPersistence.startTime = startTime;
  EMPersistence.save();

  UDPIO.raiseEvent(EventType::Reset);
}

void setLedColor() {
  bool isApEnabled = ((WiFi.getMode() & WIFI_AP) != 0);

  EnergyMeterLog->printf("Status\xe4nderung:\n  Mode:   %s\n  Status: %s\n  Relais: %s\n",
                         UrsWiFi.getModeString().c_str(), UrsWiFi.getStatusString().c_str(), Relais.isOn() ? "On" : "Off");

  // unverbunden, weder STA noch AP
  if (!WiFi.isConnected() && !isApEnabled) {
    EnergyMeterLog->println("unverbunden, weder STA noch AP");
    SignalLed.setColor(Relais.isOn()? SignalLedColors::connectingOn:SignalLedColors::connectingOff);
    return;
  }

  // Nur Access Point
  if (!WiFi.isConnected() && isApEnabled ) {
    EnergyMeterLog->println("Nur Access Point");
    SignalLed.setColor(Relais.isOn() ? SignalLedColors::apOn : SignalLedColors::apOff);
    return;
  }

  // WiFi
  if (WiFi.isConnected() && !isApEnabled) {
    SignalLed.setColor(Relais.isOn() ? SignalLedColors::wifiOn : SignalLedColors::wifiOff);
    return;
  }

  EnergyMeterLog->println("Fehler");

  // Fehler
  SignalLed.signalError();
}

void RelaisStateChangedHandler(RelaisState state) {
  if (state == RelaisState::on) {
    UDPIO.raiseEvent(EventType::On);
  } else {
    UDPIO.raiseEvent(EventType::Off);
  }

  if (state != EMPersistence.relaisState) {
    EMPersistence.relaisState = state;
    EMPersistence.save();
  }

  setLedColor();
}

// -------------------------------------------------------------
//  --- setup
// -------------------------------------------------------------
void setup() {
  ESP.eraseConfig();
  WiFi.mode(WIFI_OFF);
  WiFi.setAutoConnect(false);
  WiFi.setAutoReconnect(false);
  delay(500);

  // Serielle Schnittstelle initialisieren
  // -------------------------------------------------------------------------
  Serial.begin(115200);
  Serial.println(F("\n----------------------------"));


  // Dateisystem mounten und Einstellungen laden
  // -------------------------------------------------------------------------
  if (!SPIFFS.begin()) {
    Serial.println(F("SPIFFS nicht initialisiert!\nDefault-Konfiguration wird genutzt."));
  }
  else {
    EMSettings.loadFromFile("/config.txt");
    Serial.println(F("SPIFFS ok\nKonfigurationsdaten aus 'config.txt' geladen."));
  }

  // Signallicht initialisieren
  // -------------------------------------------------------------------------
  SignalLed.begin(EMSettings.Brightness, EMSettings.Flash, EMSettings.FlashDuration, EMSettings.FlashInterval * 1000);
  setLedColor();


  // Relais initialisieren
  // -------------------------------------------------------------------------
  Relais.onRelaisStateChanged(RelaisStateChangedHandler);
  Relais.begin(EMPersistence.relaisState);


  // Applikationsinfo auf Serial ausgeben
  // -------------------------------------------------------------------------
  Serial.println(F("\n----------------------------"));
  PrintAppInfo(Serial);


  // mit WLAN verbinden
  // -------------------------------------------------------------------------
  UrsWiFi.connectStation(EMSettings.SSID, EMSettings.Password, EMSettings.Name);
  setLedColor();
  //ToDo: Was soll passieren, wenn die Verbindung nicht zustande kommt?
  // mit WLAN verbinden
  // -------------------------------------------------------------------------
  auto conStatus = UrsWiFi.connectStation(EMSettings.SSID, EMSettings.Password, EMSettings.Name, EMSettings.LocalIP);
  if (!(conStatus & UrsCodes::ConnectCode::notConnected)) { // Verbunden
    if (conStatus != UrsCodes::ConnectCode::noError) {// aber Probleme: 2. Versuch
      Serial.println(F("=== 2. Versuch"));
      UrsWiFi.connectStation(EMSettings.SSID, EMSettings.Password, EMSettings.Name, EMSettings.LocalIP);
    }
  }

  // UdpLog initialisieren
  // -------------------------------------------------------------------------
  IPAddress LogUdpIP;
  if (EMSettings.LogUdpIP == IPAddress()) //IP-Adresse ist 0.0.0.0 -> Broadcast
    LogUdpIP = UrsWiFi.getDirectedBroadcastAddress();
  else
    LogUdpIP = EMSettings.LogUdpIP;

  if (UdpLog.begin(LogUdpIP, EMSettings.LogUdpPort)) {
    EnergyMeterLog = &UdpLog;
    Serial.print(F("UdpLog gestartet mit IP: ")); Serial.println(LogUdpIP);
    EnergyMeterLog->print(F("UdpLog gestartet  mit IP: ")); EnergyMeterLog->println(LogUdpIP);
  }
  else
    Serial.println(F("UdpLog Fehler"));

// ToDo: Access-Point
/*
  // Access-Point einrichten
  // -------------------------------------------------------------------------
  bool res = WiFi.softAP((const char*)EMSettings.APName, (const char*)EMSettings.APPassword);
  if (res) {
    EnergyMeterLog->print(F("Access-Point gestartet. IP: ")); EnergyMeterLog->println(WiFi.softAPIP());
  }
  else
    EnergyMeterLog->println(F("**** Fehler: Access-Point nicht gestartet"));
*/

  // OTA initialisieren
  // -------------------------------------------------------------------------
  UrsOTA.onStart([] {SignalLed.setBrightness(255); SignalLed.setColor(SignalLedColors::OtaStart); });
  UrsOTA.onEnd([] {UDPIO.raiseEvent(EventType::Reboot); SignalLed.setBrightness(255); SignalLed.setColor(SignalLedColors::OtaEnd); });
  UrsOTA.onError([](ota_error_t error) {SignalLed.setBrightness(255); SignalLed.setColor(SignalLedColors::OtaError); });
  UrsOTA.onProgress([](uint8_t p) {SignalLed.setBrightness(200 - 2 * p); });
  UrsOTA.begin(EMSettings.Name, EnergyMeterLog);

  //für den Fall, dass später eine Exception auftritt: 1 Sek. OTA
  uint32_t start = millis();
  while (millis() < start + 1000) { 
    UrsOTA.handle();
    delay(1);
  }

  // UDPIO initialisieren
  // -------------------------------------------------------------------------
  IPAddress UdpIP;
  if (EMSettings.UdpIP == IPAddress())
    UdpIP = UrsWiFi.getDirectedBroadcastAddress();
  else
    UdpIP = EMSettings.UdpIP;

  if (UDPIO.begin(UdpIP, EMSettings.UdpRemotePort, EMSettings.UdpLocalPort)) {
    EnergyMeterLog = &UdpLog; // ToDo: gehört nicht hierhin?
    EnergyMeterLog->print(F("UDPIO gestartet  mit IP: ")); EnergyMeterLog->println(UdpIP);
  }
  else
    EnergyMeterLog->println(F("UDPIO Fehler"));


  // Ereignis "Restart" auslösen
  // -------------------------------------------------------------------------
  UDPIO.raiseEvent(EventType::Restart);
  if (Relais.getState() == RelaisState::on) {
    UDPIO.raiseEvent(EventType::On);
  }
  else {
    UDPIO.raiseEvent(EventType::Off);
  }


  // Zustandsinformationen auf WLAN-Log ausgeben
  // -------------------------------------------------------------------------
  EnergyMeterLog->printf("\nVerbunden mit %s als '%s', IP-Adresse %s\n", WiFi.SSID().c_str(), WiFi.hostname().c_str(), WiFi.localIP().toString().c_str());
  EnergyMeterLog->printf("Reset reason: %s\n", ESP.getResetReason().c_str());
  EnergyMeterLog->printf("Reset info: %s\n", ESP.getResetInfo().c_str());
  EnergyMeterLog->println(F("---------------------------------"));

  if (EMSettings.isLoaded()) 
    EnergyMeterLog->println(F("Konfigurationsdaten aus 'config.txt' geladen."));
  else 
    EnergyMeterLog->println(F("SPIFFS nicht initialisiert!\nDefault-Konfiguration wird genutzt."));

  PrintAppInfo(*EnergyMeterLog);

  // Zeitserver intitialisieren
  // -------------------------------------------------------------------------
  NTPTimeSource.addNTPServer(EMSettings.NTP1);
  NTPTimeSource.addNTPServer(EMSettings.NTP2);
  NTPTimeSource.addNTPServer(EMSettings.NTP3);

  if (!SystemTimeServer.synchronize()) {
    delay(500);
    SystemTimeServer.synchronize(); // 2.Versuch
  }

  UrsTimeElements t = SystemTimeServer.localCurrentTime();
  EnergyMeterLog->println(t.toString("%A, der %d. %B %Y %H:%M:%S %Z"));

  if (EMPersistence.startTime > SystemTimeServer.now())
    EMPersistence.startTime = SystemTimeServer.now();

  // Sensor initialisieren
  // -------------------------------------------------------------------------
  EnergySensor.setInterval(EMSettings.SampleInterval);
  EnergySensor.onEnergySensorReset(EnergySensorReset);
  EnergySensor.begin(EMPersistence.startValue, EMPersistence.startTime, EnergyMeterLog); // Kommunikation mit dem PZEM-004T initialisieren

  // Webserver intitialisieren
  // -------------------------------------------------------------------------
  EnergyWebServer.onConfigChanged(ConfigChangedHandler);
  EnergyWebServer.begin(EMSettings, EMSettings.AdminUser, EMSettings.AdminPassword, EMSettings.Name);

  oldStatus = WiFi.status();
}

// -------------------------------------------------------------
//  --- loop
// -------------------------------------------------------------
void loop() {
  static uint32_t lastLogging;

  if (oldStatus != WiFi.status()) {
    setLedColor();
    oldStatus = WiFi.status();
  }

  // Synchrone Handler-Methoden aufrufen
  // -------------------------------------------------------------------------
  UrsWiFi.handleReconnectStation(*EnergyMeterLog);
  UrsOTA.handle();
  SystemTimeServer.handle();
  SignalLed.handle();
  EnergySensor.handle();
  UDPIO.handle();


  // Synchrone Aktionen des Webservers ausführen
  // -------------------------------------------------------------------------
  if (EnergyWebServer.energySensorShouldReset) {
    EnergyWebServer.energySensorShouldReset = false;
    EnergySensor.reset();
  }

  if (EnergyWebServer.shouldReboot) {
    EnergyMeterLog->println("Rebooting...");
    UDPIO.raiseEvent(EventType::Reboot);
    delay(100);
    ESP.restart();
  }


  // System-Zustand auf WLAN-Log ausgeben (alle 10 sec)
  // -------------------------------------------------------------------------
  if (lastLogging + 10 * 1000 < millis() && DataLog) {
    lastLogging = millis();

    EnergyData e = EnergySensor.getLastReading();

    EnergyMeterLog->print((int)(millis() / 1000)); EnergyMeterLog->print(" "); // Sekunden

    if (e.isValid) {
      EnergyMeterLog->print(e.power, 1);  EnergyMeterLog->print(" W; ");
      EnergyMeterLog->print(ftoa(e.energy, 1));
      EnergyMeterLog->print(" Wh (");
      EnergyMeterLog->print(e.energyRaw); EnergyMeterLog->print(")");
    }
    else {
      EnergyMeterLog->print(F("Keine g\xFCltigen Daten"));
    }
    EnergyMeterLog->println(Relais.isOn() ? ";on" : ";off");
  }
}