#include "MQTT.h"
#define MDNS_SERVICENAME "gardenwater"

char MQTT::myIP[16] = "0.0.0.0";
char MQTT::hostname[40] = "Gartenwasser_vorn";
int MQTT::mqtt_port = 1883;
char MQTT::mqtt_server[40] = "nas.local";
char MQTT::mqtt_topic[40] = "Gartenwasser/vorn";
char MQTT::ntpServer[40] = "nas.local";
int MQTT::defTime_min = 60;
int MQTT::numValves = 1;
WiFiUDP MQTT::udp;
const char* MQTT::months[]={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};

AsyncWebServer* MQTT::server = new AsyncWebServer(80);
AsyncWebSocket* MQTT::ws = new AsyncWebSocket("/ws");
bool MQTT::saveConfigFlag=false;
WiFiManagerParameter custom_hostname("hostname", "Hostname", "", 40);
WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "", 40);
WiFiManagerParameter custom_mqtt_port("port", "mqtt port", "",6);
WiFiManagerParameter custom_mqtt_topic("topic", "topic", "", 40);
WiFiManagerParameter custom_deftime("deftime", "Default ON Time (min)", "", 6);
WiFiManagerParameter custom_numValves("numValves", "Available Valves", "", 6);
WiFiManagerParameter custom_ntpServer("ntpServer", "NTP Server", "", 40);
MQTT::MQTT(){
}

void MQTT::handleWebSocketMessage(void *arg, uint8_t *data, size_t len){
    data[len] = 0; //ensure termination!
    Serial.printf("WebSocket Msg '%s'\n", data);
}

void MQTT::handleWebSocket(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
     switch (type) {
       case WS_EVT_CONNECT:
         Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
         client->text(makeStatusJSON());
         client->text(makeTimerJSON());
         break;
       case WS_EVT_DISCONNECT:
         Serial.printf("WebSocket client #%u disconnected\n", client->id());
         break;
       case WS_EVT_DATA:
         handleWebSocketMessage(arg, data, len);
         break;
       case WS_EVT_PONG:
       case WS_EVT_ERROR:
         break;
     }
   }

   String MQTT::makeStatusJSON(void){
    JsonDocument doc;
    JsonArray jArray;
    jArray = doc["valveStates"].to<JsonArray>();
    for (int i = 0; i < NUM_VALVES; i++) {
        jArray.add(valveStateStrings[glblData.valveState[i]]);
    }
    jArray = doc["desiredValveStates"].to<JsonArray>();
    for (int i = 0; i < NUM_VALVES; i++) {
        jArray.add(valveStateStrings[glblData.setValveState[i]]);
    }
    doc["hostname"] = hostname;
    doc["IP"] = myIP;
    doc["MQTT-Server"] = mqtt_server;
    doc["MQTT-Port"] = mqtt_port;
    doc["MQTT-topic"] = mqtt_topic;
    doc["numValves"] = numValves;
    doc["defaultTimerMins"] = defTime_min;
    doc["NTPserver"] = ntpServer;
    String jsonStr;
    serializeJsonPretty(doc, jsonStr);
    return jsonStr;
   }

   String MQTT::makeTimerJSON(void){
    JsonDocument doc;
    JsonArray jArray;
    jArray = doc["timers"].to<JsonArray>();
    for (int i = 0; i < NUM_VALVES; i++) {
        jArray.add(glblData.timers[i]);
    }
    String jsonStr;
    serializeJsonPretty(doc, jsonStr);
    return jsonStr;
   }

   void MQTT::sendMQTTstatus(void){
        JsonDocument doc;
        JsonArray jArray;
        jArray = doc["valveStates"].to<JsonArray>();
        for (int i = 0; i < NUM_VALVES; i++) {
            jArray.add(valveStateStrings[glblData.valveState[i]]);
        }
        jArray = doc["desiredValveStates"].to<JsonArray>();
        for (int i = 0; i < NUM_VALVES; i++) {
            jArray.add(valveStateStrings[glblData.setValveState[i]]);
        }
        String jsonStr;
        serializeJson(doc, jsonStr);
        publish_sub("status", jsonStr.c_str());
        publish_sub("hostname", hostname);
        publish_sub("IP", myIP);
        publish_sub("numValves", String(numValves).c_str());
        publish_sub("defaultTimerMins", String(defTime_min).c_str());
        publish_sub("NTP-Server", ntpServer);
   }


void MQTT::serveData(AsyncWebServerRequest *request){
    request->send(200, FPSTR(HTTP_HEAD_JSON), makeStatusJSON());
}

String MQTT::processor(const String& var){
    if(var == "HOSTNAME"){
      return String(hostname);
    }else if(var == "NAME_VALVE1"){
        if(numValves == 1)
            return String("Hochbeete");
        else
            return String("Garten Vorn");
    }else if(var == "NAME_VALVE2"){
        if(numValves == 1)
            return String("");
        else
            return String("Tröge");
    }
    return String();
  }

void MQTT::serveIndex(AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", String(), false, processor);
}

void MQTT::serveCss(AsyncWebServerRequest *request){
    request->send(SPIFFS, "/simple.css", String(), false);
}

void MQTT::serveSetValve(AsyncWebServerRequest *request){
    JsonDocument doc;
    doc["status"] = "Fail";

    String sending;
    sending = "{";
    if(request->hasParam("valve") && request->hasParam("set")){
        int valvenum = request->getParam("valve")->value().toInt();
        if(valvenum > 0 && valvenum < NUM_VALVES){
            if(request->getParam("set")->value().equals("open")){
                glblData.setValveState[valvenum] = OPEN;
                doc["status"] = "Success opening valve";
                if(request->hasParam("timer")){
                    glblData.timers[valvenum] = request->getParam("timer")->value().toInt()*60;
                }
                remoteLogf("Valve %d opened for %d mins",valvenum,glblData.timers[valvenum]/60);
            }else{
                glblData.setValveState[valvenum] = CLOSE;
                doc["status"] = "Success closing valve";  
            }
             
        }
    }
    String jsonStr;
    serializeJsonPretty(doc, jsonStr);
    request->send(200, FPSTR(HTTP_HEAD_JSON), jsonStr);
}




void MQTT::begin(){
    saveConfigFlag = false;
    prefs.begin("settings");
    prefs.getString("HOST",hostname,40);
    prefs.getString("MQTT_SERVER",mqtt_server,40);
    mqtt_port = prefs.getInt("MQTT_PORT",mqtt_port);
    prefs.getString("MQTT_TOPIC",mqtt_topic,40);
    defTime_min = prefs.getInt("DEF_TIME_MIN",defTime_min);
    numValves = prefs.getInt("NUM_VALVES",numValves);
    prefs.getString("NTP_SERVER",ntpServer,40);
    //glblData.enBuff = enBuff;
    //ui_settemp(defTemp);
    prefs.end();
    if(!SPIFFS.begin(true)){
        Serial.println("An Error has occurred while mounting SPIFFS");
        return;
    }
    Serial.println("Settings loaded:");
    Serial.print("HOST: ");
    Serial.println(hostname);
    Serial.print("MQTT_SERVER: ");
    Serial.println(mqtt_server);
    Serial.print("MQTT_PORT: ");
    Serial.println(mqtt_port);
    Serial.print("MQTT_TOPIC: ");
    Serial.println(mqtt_topic);
    Serial.print("DEF_TIME_MIN: ");
    Serial.println(defTime_min);
    Serial.print("NUM_VALVES: ");
    Serial.println(numValves);
    Serial.print("NTP_SERVER: ");
    Serial.println(ntpServer);


    wm.setSaveConfigCallback(MQTT::saveConfigCallback);
    custom_hostname.setValue(hostname,40);
    custom_mqtt_server.setValue(mqtt_server,40);
    custom_mqtt_port.setValue(String(mqtt_port).c_str(),6);
    custom_mqtt_topic.setValue(mqtt_topic,40);
    custom_deftime.setValue(String(defTime_min).c_str(),6);
    custom_numValves.setValue(String(numValves).c_str(),6);
    custom_ntpServer.setValue(ntpServer,40);
    wm.addParameter(&custom_hostname);
    wm.addParameter(&custom_mqtt_server);
    wm.addParameter(&custom_mqtt_port);
    wm.addParameter(&custom_mqtt_topic);
    wm.addParameter(&custom_deftime);
    wm.addParameter(&custom_numValves);
    wm.addParameter(&custom_ntpServer);
    psclient.setClient(client);
    psclient.setServer(mqtt_server,mqtt_port);
    WiFi.setHostname(custom_hostname.getValue()); //define hostname
    wm.setConfigPortalBlocking(false);


    if(wm.autoConnect("AutoConnectAP")){
        //if you get here you have connected to the WiFi
        Serial.println("connected...yeey :)");
        strcpy(myIP,WiFi.localIP().toString().c_str());
        wm.setHttpPort(8080);
        wm.startWebPortal();
        wifiConnected = true;
    }else{
        delay(5000);
        if(wm.autoConnect("AutoConnectAP")){
            //if you get here you have connected to the WiFi
            Serial.println("connected...yeey :)");
            strcpy(myIP,WiFi.localIP().toString().c_str());
            wm.setHttpPort(8080);
            wm.startWebPortal();
            wifiConnected = true;
        }else{
            wm.startWebPortal();
            Serial.println("non blocking config portal running on Port 80");
        }
    }
    if(wifiConnected){
        setenv("TZ", TZ_INFO, 1);             // Zeitzone  muss nach dem reset neu eingestellt werden
        tzset();
        configTzTime(TZ_INFO, ntpServer); // ESP32 Systemzeit mit NTP Synchronisieren
        struct tm time;
        if(!getLocalTime(&time)){
          Serial.println("no time from NTP");
        }else{
            Serial.printf("Localtime: %02d.%02d.%d %02d:%02d:%02d",time.tm_mday,time.tm_mon+1,time.tm_year+1900,time.tm_hour,time.tm_min,time.tm_sec);
        }
        ArduinoOTA.setHostname(hostname);
        ArduinoOTA
        .onStart([]() {
        String type;
        if (ArduinoOTA.getCommand() == U_FLASH)
            type = "sketch";
        else // U_SPIFFS
            type = "filesystem";

        // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
        Serial.println("Start updating " + type);
        })
        .onEnd([]() {
            Serial.println("\nEnd");
        })
        .onProgress([](unsigned int progress, unsigned int total) {
            Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
        })
        .onError([](ota_error_t error) {
            Serial.printf("Error[%u]: ", error);
            if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
            else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
            else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
            else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
            else if (error == OTA_END_ERROR) Serial.println("End Failed");
        });
        ArduinoOTA.begin();
        
        // This is not the safest way to reset the webserver, it can cause crashes on callbacks initilized before this and since its a shared pointer...
        // @todo add a new callback maybe, after webserver started, callback cannot override handlers, but can grab them first
        
        /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */

        // G macro workaround for Uri() bug https://github.com/esp8266/Arduino/issues/7102
        server->on("/", MQTT::serveIndex);
        server->on("/simple.css", MQTT::serveCss);
        server->on("/data", MQTT::serveData);
        server->on("/setValve", MQTT::serveSetValve);
        server->begin();
        if (MDNS.begin(hostname)) {
            Serial.println("mDNS responder started");
            // Add service to MDNS-SD
            MDNS.addService(MDNS_SERVICENAME, "tcp", 80);
            MDNS.addService("http", "tcp", 80);
        }
        
        ws->onEvent(handleWebSocket);
        server->addHandler(ws);
        remoteLogf("Started and connected as %s (IP: %s) Discoverable service: %s",hostname,myIP,MDNS_SERVICENAME);

        reconnect();
    }else{
        lastReconnectTry = millis();   //try a wifi connection again in 300secs
    }
}
void MQTT::saveConfigCallback(void){
    Serial.println("SAVE");
    saveConfigFlag = true;
}

void MQTT::saveConfigToFlash(void){
    strcpy(mqtt_server,custom_mqtt_server.getValue());
    mqtt_port = atoi(custom_mqtt_port.getValue());
    strcpy(hostname,custom_hostname.getValue());
    strcpy(mqtt_topic,custom_mqtt_topic.getValue());
    defTime_min = atoi(custom_deftime.getValue());
    numValves = atoi(custom_numValves.getValue());
    strcpy(ntpServer ,custom_ntpServer.getValue());
    //glblData.enBuff = enBuff;
    prefs.begin("settings");
    prefs.putString("HOST",hostname);
    prefs.putString("MQTT_SERVER",mqtt_server);
    prefs.putInt("MQTT_PORT",mqtt_port);
    prefs.putString("MQTT_TOPIC",mqtt_topic);
    prefs.putInt("DEF_TIME_MIN",defTime_min);
    prefs.putInt("NUM_VALVES",numValves);
    prefs.putString("NTP_SERVER",ntpServer);
    prefs.end();
    Serial.println("Settings SAVED:");
    Serial.print("HOST: ");
    Serial.println(hostname);
    Serial.print("MQTT_SERVER: ");
    Serial.println(mqtt_server);
    Serial.print("MQTT_PORT: ");
    Serial.println(mqtt_port);
    Serial.print("MQTT_TOPIC: ");
    Serial.println(mqtt_topic);
    Serial.print("DEF_TIME_MIN: ");
    Serial.println(defTime_min);
    Serial.print("NUM_VALVES: ");
    Serial.println(numValves);
    Serial.print("NTP_SERVER: ");
    Serial.println(ntpServer);

}

void MQTT::reconnect(void) {
    // Loop until we're reconnected
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (psclient.connect(hostname)) {
        Serial.println("connected");
        // Subscribe
        glblData.statusChange = true;
    } else {
        Serial.print("failed, rc=");
        Serial.print(psclient.state());
    }
}

void MQTT::loop(void){
    unsigned long now = millis();
    ws->cleanupClients();
    if(now-lastStatusUpdate > 15000){   //update wifi status every 15secs.
        lastStatusUpdate = now;
        if(WiFi.isConnected()){
            int32_t rssi = WiFi.RSSI();
            if(rssi > -55){
               // update_wifi_strength(WIFI_STRENGTH_HIGH);
            }else if(rssi > -75){
                //update_wifi_strength(WIFI_STRENGTH_MED);
            }else if(rssi > -90){
                //update_wifi_strength(WIFI_STRENGTH_LOW);
            }else{
                //update_wifi_strength(WIFI_STRENGTH_OFF);
            }
            if(!psclient.connected()){
                //update_wifi_strength(WIFI_STRENGTH_ERROR);
                if(now-lastReconnectTry > 60000){
                    lastReconnectTry = now;
                    reconnect();
                }
            }else{
                if(now-lastReconnectTry > 300000){
                    lastReconnectTry = now;
                    wm.autoConnect();
                }
            }
        }else{
            wm.autoConnect();
            //update_wifi_strength(WIFI_STRENGTH_OFF);
        }
    }
    if(now-lastWSupdate > 1000){
        lastWSupdate = now;
        bool onlyTimers = true;
        if(glblData.statusChange){
            glblData.statusChange = false;
            ws->textAll(makeStatusJSON());
            sendMQTTstatus();
        }
        
        if(glblData.timerChange){
            String data = makeTimerJSON();
            glblData.timerChange = 0;
            publish_sub("Timers",data.c_str());
            ws->textAll(data);
        }
    }
    psclient.loop();
    ArduinoOTA.handle();
    wm.process();
    if(saveConfigFlag){
        Serial.println("Save Settings");
        saveConfigFlag = false;
        saveConfigToFlash();
        Serial.println("disconnect mqtt to activate new settings");
        psclient.disconnect();
    }
}

char* MQTT::getMqttServer(void){
    return mqtt_server;
}
uint16_t MQTT::getMqttPort(void){
    return mqtt_port;
}
char* MQTT::getHostname(void){
    return hostname;
}
char* MQTT::getMqttTopic(void){
    return mqtt_topic;
}
char* MQTT::getMyIP(void){
    return myIP;
}

void MQTT::publish(const char* msg){
    psclient.publish(mqtt_topic,msg);
}
void MQTT::publish(String msg){
    psclient.publish(mqtt_topic,msg.c_str());
}
void MQTT::publish(const char* msg, unsigned int len){
    psclient.publish(mqtt_topic,msg,len);
}
void MQTT::publish(const char* msg, bool retained){
    psclient.publish(mqtt_topic,msg,retained);
}
void MQTT::publish_sub(const char* subtopic, const char* msg){
    char newtopic[60];
    strcpy(newtopic,mqtt_topic);
    strcat(newtopic,"/");
    strcat(newtopic,subtopic);
    psclient.publish(newtopic,msg);
}


void MQTT::remoteLog(const char* str, int32_t num){
    udp.beginPacket("192.168.179.174",13377);
    struct tm timeinfo;
    getLocalTime(&timeinfo);
    udp.printf("<13>%s %02d %02d:%02d:%02d %s Log: %s %d",months[timeinfo.tm_mon],timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec,hostname,str,num);
    udp.endPacket();
  }

  void MQTT::remoteLog(const char* str){
    udp.beginPacket("192.168.179.174",13377);
    struct tm timeinfo;
    getLocalTime(&timeinfo);
    udp.printf("<13>%s %02d %02d:%02d:%02d %s Log: %s",months[timeinfo.tm_mon],timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec,hostname,str);
    udp.endPacket();
  }
  
  void MQTT::remoteLog(const char* str,const char* str2){
    udp.beginPacket("192.168.179.174",13377);
    struct tm timeinfo;
    getLocalTime(&timeinfo);
    udp.printf("<13>%s %02d %02d:%02d:%02d %s Log: %s %s",months[timeinfo.tm_mon],timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec,hostname,str,str2);
    udp.endPacket();
  }
  
  void MQTT::remoteLogf(const char* format, ...){
    udp.beginPacket("192.168.179.174",13377);
    struct tm timeinfo;
    getLocalTime(&timeinfo);
    char loc_buf[256];
    char * temp = loc_buf;
    va_list arg;
    va_start(arg, format);
    int len = snprintf(temp, sizeof(loc_buf), "<13>%s %02d %02d:%02d:%02d %s Log: ", months[timeinfo.tm_mon],timeinfo.tm_mday,timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec,hostname);
    if(len < (int)sizeof(loc_buf)){
      len += vsnprintf(temp+len, sizeof(loc_buf)-len, format, arg);
    }
    va_end(arg);
    if(len >= (int)sizeof(loc_buf)){  // comparation of same sign type for the compiler
      udp.write((uint8_t*)temp, len);
      udp.print("...Memory issue at log buffer!");
      udp.endPacket();
      return;
    }
    udp.write((uint8_t*)temp, len);
    udp.endPacket();
    return;
  }