#include <FS.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include "zeushd_macro.h"
#include <ESP_MultiResetDetector.h>
#include <ArduinoJson.h>
#include <WebSocketsServer.h>
#include <Hash.h>
#include <AsyncMqtt_Generic.h>
#include "Simpletimer.h"
#include <Ticker.h>
#include <NTPClient.h>
#include "RTClib.h"
#include "CountDown.h"

const uint8     fwVersion[3]              = {1,1,0};
const char      server[32]                = "zeushd.io.vn";
String          fwUrlOTA                  = "http://hungdo0993.duckdns.org:8080/zeushd/light/zeushd.bin";
String          systemURL                 = "";
char            msg[MSG_BUFFER_SIZE];
char            ssidWiFi[32]              = "";
char            passWiFi[32]              = "";
char            uid[32]                   = "";
char            fwVersionString[45]       = "000000000000|z|2.0.0|z|null|z|null|z|null";
bool            flgSetupConfig;
bool            flgRTCSync;
bool            flgReadTime;
bool            flgAutoMode;
bool            flgUpdateFirmware;
bool            flgSchedulingFile;
bool            flgSystemFile;
bool            flgLedStatusUpdate;
bool            enRTC;
bool            flgInverted;
bool            flgReIniDevice;
uint8           colorMap[nColorMax]       = {0, 2, 3, 4, 5, 7};
uint8           cntMQTTConnect            = 0;
uint8           Pin[nColorMax]            = {255};
uint8           pinFAN                    = 255;
uint8           macAddress[6]             = {0};
uint8           stLedStatus               = setupConfig;
uint8           cntLedStatus              = 0;
uint8           cntReceivedData           = 0;
uint8           cntReset                  = 0;
uint8           cntReadTime               = 0;
uint8           schedulingMaxIndex        = 0;
uint16          bitChannel                = 0;
uint16          freqLed                   = 1000;
uint16          colorValue[nColorMax]     = {0};
uint16          colorValueOld[nColorMax]  = {0};
uint32          schedulingVersion         = 0; 
uint32          currentTime               = 0;
Scheduling      scheduling[30];

MultiResetDetector* mrd;
WebSocketsServer webSocket = WebSocketsServer(81);
AsyncMqttClient mqttClient;
Simpletimer mainTimer_1m;
Simpletimer mainTimer_1s;
Simpletimer mainTimer_20ms;
Ticker ledStatusTimer;
Ticker receivedDataTimer;
Ticker mqttReconnectTimer;
Ticker wifiReconnectTimer;
WiFiClient client;
CountDown autoModeCountDown(CountDown::MINUTES);

WiFiEventHandler wifiConnectHandler;
WiFiEventHandler wifiDisconnectHandler;

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "vn.pool.ntp.org", 25200, 60000);
RTC_DS1307 rtc;

void connectToWifi() {
  #if (SERIAL_DEBUG)
    Serial.println("Connecting to Wi-Fi...");
  #endif
  WiFi.begin(ssidWiFi, passWiFi);
}

void connectToMqtt() {
  if (cntMQTTConnect < cntMQTT_TO) {
    #if (SERIAL_DEBUG)
      Serial.println("Connecting to MQTT...");
    #endif
    mqttClient.connect();
    cntMQTTConnect++;
  } else {
    cntMQTTConnect = 0;
    WiFi.disconnect();
  }
}

void onWifiConnect(const WiFiEventStationModeGotIP& event) {
  (void) event;
  #if (SERIAL_DEBUG)
    Serial.print("Connected to Wi-Fi. IP address: "); Serial.println(WiFi.localIP());
  #endif
  flgLedStatusUpdate = true;
  stLedStatus = connectingServer;
  wifiReconnectTimer.detach();
  mqttReconnectTimer.attach(10, connectToMqtt);
  connectToMqtt();
}

void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
  (void) event;
  #if (SERIAL_DEBUG)
    Serial.println("Disconnected from Wi-Fi.");
  #endif
  flgLedStatusUpdate = true;
  stLedStatus = connectingWiFi;
  mqttReconnectTimer.detach();
  wifiReconnectTimer.once(10, connectToWifi);
  connectToWifi();
}

void onMqttConnect(bool sessionPresent) {
  char subTopic[13];
  sprintf(subTopic, "%02X%02X%02X%02X%02X%02X", macAddress[0], macAddress[1], macAddress[2], macAddress[3], macAddress[4], macAddress[5]);
  flgLedStatusUpdate = true;
  stLedStatus = connectedServer;
  mqttReconnectTimer.detach();
  wifiReconnectTimer.detach();
  #if (SERIAL_DEBUG)
    Serial.print("Connected to MQTT broker: "); Serial.print(MQTT_HOST);
    Serial.print(", port: "); Serial.println(MQTT_PORT);
    Serial.print("PubTopic: "); Serial.println(uid);
    Serial.print("SubTopic: "); Serial.println(subTopic);
    Serial.println("************************************************");
    Serial.print("Session present: "); Serial.println(sessionPresent);
  #endif
  uint16_t packetIdSub = mqttClient.subscribe(subTopic, 2);
  #if (SERIAL_DEBUG)
    Serial.print("Subscribing at QoS 2, packetId: "); Serial.println(packetIdSub);
  #endif
  mqttClient.publish(uid, 2, false, fwVersionString);
  #if (SERIAL_DEBUG)
    Serial.println("Publishing at QoS 2");
    Serial.println("************************************************");
  #endif
}

void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
  (void) reason;
  #if (SERIAL_DEBUG)
    Serial.println("Disconnected from MQTT.");
  #endif
  if (WiFi.isConnected()) {
    flgLedStatusUpdate = true;
    stLedStatus = connectingServer;
    mqttReconnectTimer.once(2, connectToMqtt);
    connectToMqtt();
  }
}

void onMqttSubscribe(const uint16_t& packetId, const uint8_t& qos) {
  #if (SERIAL_DEBUG)
    Serial.println("Subscribe acknowledged.");
    Serial.print("  packetId: "); Serial.println(packetId);
    Serial.print("  qos: ");      Serial.println(qos);
  #endif
}

void onMqttUnsubscribe(const uint16_t& packetId)
{
  #if (SERIAL_DEBUG)
    Serial.println("Unsubscribe acknowledged.");
    Serial.print("  packetId: "); Serial.println(packetId);
  #endif
}

void onMqttMessage(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, 
                   const size_t& len, const size_t& index, const size_t& total)
{
  String payloadBuffer = "";
  String dataBuffer = "";
  String topicBuffer = "";
  uint8 noSlot = 0;
  uint8 idxSlot = 0;
  uint8 idxColor = 0;
  for (uint8 i = 0; i < len; i++) {
    payloadBuffer += ((char)payload[i]);
  }
  #if (SERIAL_DEBUG)
    Serial.println("Message received.");
    Serial.print("  topic: ");    Serial.println(topic);
//    Serial.print("  qos: ");      Serial.println(properties.qos);
//    Serial.print("  dup: ");      Serial.println(properties.dup);
//    Serial.print("  retain: ");   Serial.println(properties.retain);
//    Serial.print("  len: ");      Serial.println(len);
//    Serial.print("  index: ");    Serial.println(index);
//    Serial.print("  total: ");    Serial.println(total);
    Serial.print("  payload: ");  Serial.println(payloadBuffer);
  #endif
  digitalWrite(statusLED, LOW);
  receivedDataTimer.attach_ms(100, receivedData);
  if (payloadBuffer == "delete|z|") {
    flgReIniDevice = true;
  }
  if (payloadBuffer == "synctime|z|") {
    flgRTCSync = true;
    flgReadTime = true;
  }
  if (payloadBuffer == "update|z|") {
    flgUpdateFirmware = true;
  }
  if (getString(payloadBuffer, "|z|", 0) == "manual") {
    dataBuffer = payloadBuffer.substring(9);
    #if (SERIAL_DEBUG)
      Serial.println("Mode: Manual");
    #endif
    if (autoModeCountDown.isStopped()){
      autoModeCountDown.start(60);
    }
    flgAutoMode = false;
    uint8 colorBuffer[8] = {0};
    for (idxColor = 0; idxColor < 8; idxColor++) {
      colorBuffer[idxColor] = dataBuffer.substring(idxColor * 3, ((idxColor + 1) * 3)).toInt();
    }
    for (idxColor = 0; idxColor < nColorMax; idxColor++) {
      colorValue[idxColor] = colorBuffer[colorMap[idxColor]] * 10;
      #if (SERIAL_DEBUG)
        Serial.print(colorValue[idxColor]);
        Serial.print(", ");
      #endif
    }
    #if (SERIAL_DEBUG)
        Serial.println();
    #endif
  } else if (getString(payloadBuffer, "|z|", 0) == "saved") {
    flgSchedulingFile = true;
    #if (SERIAL_DEBUG)
      Serial.println("Saved");
    #endif
  } else if (getString(payloadBuffer, "|z|", 0) == "system") {
    flgSystemFile = true;
    systemURL = getString(payloadBuffer, "|z|", 1);
    #if (SERIAL_DEBUG)
      Serial.println("System update");
    #endif
  } else if (getString(payloadBuffer, "|z|", 0) == "auto") {
    #if (SERIAL_DEBUG)
      Serial.println("Mode: Auto");
    #endif
    if (autoModeCountDown.isRunning()){
      autoModeCountDown.stop();
    }
    flgAutoMode = true;
  } else if (getString(payloadBuffer, "|z|", 0) == "restart") {
    ESP.restart();
  }
  mqttClient.publish(uid, 2, false, fwVersionString);
}

void onMqttPublish(const uint16_t& packetId)
{
  #if (SERIAL_DEBUG)
    Serial.println("Publish acknowledged.");
    Serial.print("  packetId: "); Serial.println(packetId);
  #endif
}

uint32 timeToSec (uint8 hour, uint8 min, uint8 sec)
{
  return hour * 3600 + min * 60 + sec;
}

void ledStatus(uint8 cnt) {
  if (digitalRead(statusLED)) {
    if (cntLedStatus == cnt) {
      cntLedStatus = 0;
      digitalWrite(statusLED, LOW);
    } else {
      cntLedStatus++;
    }
  } else {
    digitalWrite(statusLED, HIGH);
  }
}

void receivedData() {
  if (cntReceivedData < 1) {
    digitalWrite(statusLED, !digitalRead(statusLED));
    cntReceivedData++;
  } else {
    digitalWrite(statusLED, LOW);
    receivedDataTimer.detach();
    cntReceivedData = 0;
  }
}

String getString(String data, String separator, uint8 index) {
  uint8 idx = 0;
  uint8 found = 0;
  bool  foundSeparator = true;
  uint8 strIndex[] = {0, -separator.length()};
  uint8 maxIndex = data.length()-1;
  String result = "";
  for(idx = 0; idx <= maxIndex && found <= index; idx ++){
    foundSeparator = true;
    for(uint8 _idx = 0; _idx < separator.length(); _idx++) {
      foundSeparator = foundSeparator && (data.charAt(idx + _idx) == separator.charAt(_idx));
    }
    if(foundSeparator || idx == maxIndex) {
      found++;
      strIndex[0] = strIndex[1] + separator.length();
      strIndex[1] = (idx == maxIndex) ? idx + 1 : idx;
    }
  }
  result = found > index ? data.substring(strIndex[0], strIndex[1]) : "";
  return result;
}

void getChar(String data, String separator, char* output, uint8 index) {
  uint8 idx = 0;
  uint8 _idx = 0;
  uint8 found = 0;
  bool  foundSeparator = true;
  uint8 strIndex[] = {0, -separator.length()};
  uint8 maxIndex = data.length()-1;
  String result = "";
  for(idx = 0; idx <= maxIndex && found <= index; idx ++){
    foundSeparator = true;
    for(_idx = 0; _idx < separator.length(); _idx++) {
      foundSeparator = foundSeparator && (data.charAt(idx + _idx) == separator.charAt(_idx));
    }
    if(foundSeparator || idx == maxIndex) {
      found++;
      strIndex[0] = strIndex[1] + separator.length();
      strIndex[1] = (idx == maxIndex) ? idx + 1 : idx;
    }
  }
  result = found > index ? data.substring(strIndex[0], strIndex[1]) : "";
  result.toCharArray(output, strIndex[1] - strIndex[0] +1);
}

void saveConfigCallback() {
  DynamicJsonDocument json(1024);
  json["ssidWiFi"]  = ssidWiFi;
  json["passWiFi"]  = passWiFi;
  json["uid"]       = uid;
  File configFile = SPIFFS.open("/configWiFi.json", "w");
  #if (SERIAL_DEBUG)
    if (!configFile) {
      Serial.println("failed to open config file for writing");
    } else {
      serializeJsonPretty(json, Serial);
    }
  #endif
  serializeJson(json, configFile);
  configFile.close();
  ESP.restart();
}

void setupConfigFile() {
  if (SPIFFS.begin()) {
    #if (SERIAL_DEBUG)
      Serial.println("mounted file system");
    #endif
    if (SPIFFS.exists("/configWiFi.json")) {
      #if (SERIAL_DEBUG)
          Serial.println("reading config file");
      #endif
      File configFile = SPIFFS.open("/configWiFi.json", "r");
      if (configFile) {
        #if (SERIAL_DEBUG)
          Serial.println("opened config file");
        #endif
        size_t size = configFile.size();
        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> configBuffer(new char[size]);
        configFile.readBytes(configBuffer.get(), size);
        DynamicJsonDocument configJson(1024);
        DeserializationError configError = deserializeJson(configJson, configBuffer.get());
        #if (SERIAL_DEBUG)
          serializeJson(configJson, Serial);
          Serial.println();
        #endif
        if (!configError) {
          if (configJson.containsKey("ssidWiFi")) {
            strcpy(ssidWiFi, configJson["ssidWiFi"]);
          } else {
            flgSetupConfig = true;
          }
          if (configJson.containsKey("passWiFi")) {
            strcpy(passWiFi, configJson["passWiFi"]);
          } else {
            flgSetupConfig = true;
          }
          if (configJson.containsKey("uid")) {
            strcpy(uid, configJson["uid"]);
          } else {
            flgSetupConfig = true;
          }
          if (strlen(ssidWiFi) > 0 && strlen(uid) > 0) {
            flgSetupConfig = false;
          }
        } else {
          #if (SERIAL_DEBUG)
            Serial.println("failed to load json config");
          #endif
        }
      }
      configFile.close();
    } else {
      #if (SERIAL_DEBUG)
        Serial.println("failed to mount FS");
      #endif
      flgSetupConfig = true;
    }
  } else {
    #if (SERIAL_DEBUG)
      Serial.println("khong khoi tao dc");
    #endif
  }
}

void setupScheduling() {
  uint8 idxColor = 0;
  uint8 idxSlot = 0;
  if (SPIFFS.exists("/scheduling.json")) {
    #if (SERIAL_DEBUG)
      Serial.println("reading scheduling file");
    #endif
    File schedulingFile = SPIFFS.open("/scheduling.json", "r");
    if (schedulingFile) {
      size_t size = schedulingFile.size();
      // Allocate a buffer to store contents of the file.
      std::unique_ptr<char[]> schedulingBuffer(new char[size]);
      schedulingFile.readBytes(schedulingBuffer.get(), size);
      DynamicJsonDocument schedulingJson(2048);
      DeserializationError schedulingError = deserializeJson(schedulingJson, schedulingBuffer.get());
      #if (SERIAL_DEBUG)
        serializeJson(schedulingJson, Serial);
        Serial.println();
      #endif
      if (!schedulingError) {
        if (schedulingJson.containsKey("version")) {
          schedulingVersion = schedulingJson["version"];
        }
        if (schedulingJson.containsKey("scheduling")) {
          if (schedulingJson["scheduling"].size() > 0){
            schedulingMaxIndex = schedulingJson["scheduling"].size() - 1;
            for (idxSlot = 0; idxSlot <= schedulingMaxIndex; idxSlot++) {
              scheduling[idxSlot].time = schedulingJson["scheduling"][idxSlot][0];
              scheduling[idxSlot].time *= 60;
              for (idxColor = 0; idxColor < nColorMax; idxColor++){
                scheduling[idxSlot].color[idxColor] = schedulingJson["scheduling"][idxSlot][colorMap[idxColor] + 3];
              }
            }
          } else {
            schedulingMaxIndex = 0;
          } 
        }
      } else {
        #if (SERIAL_DEBUG)
          Serial.println("failed to load json config");
        #endif
      }
    }
    schedulingFile.close();
  } else {
    #if (SERIAL_DEBUG)
        Serial.println("no scheduling file");
    #endif
  }
}

void setupSystemFile() {
  uint8 idxColor = 0;
  uint8 idxSlot = 0;
  if (SPIFFS.exists("/system.json")) {
    #if (SERIAL_DEBUG)
      Serial.println("reading system file");
    #endif
    File systemFile = SPIFFS.open("/system.json", "r");
    if (systemFile) {
      size_t size = systemFile.size();
      // Allocate a buffer to store contents of the file.
      std::unique_ptr<char[]> systemBuffer(new char[size]);
      systemFile.readBytes(systemBuffer.get(), size);
      DynamicJsonDocument systemJson(1024);
      DeserializationError systemError = deserializeJson(systemJson, systemBuffer.get());
      #if (SERIAL_DEBUG)
        serializeJson(systemJson, Serial);
        Serial.println();
      #endif
      if (!systemError) {
        if (systemJson.containsKey("rtc")) {
          enRTC = systemJson["rtc"];
        }
        if (systemJson.containsKey("pin")) {
          for (idxColor = 0; idxColor < nColorMax; idxColor++){
            Pin[idxColor] = systemJson["pin"][colorMap[idxColor]];
            if (systemJson["pin"][colorMap[idxColor]] != 255) {
              bitChannel |= 1 << colorMap[idxColor];
            }
          }
          #if (SERIAL_DEBUG)
            Serial.println(bitChannel);
          #endif
          pinFAN = systemJson["pin"][10];
        }
        if (systemJson.containsKey("inverted")) {
          flgInverted = systemJson["inverted"];
        }
        if (systemJson.containsKey("freqLed")) {
          freqLed = systemJson["freqLed"];
        }
      } else {
        #if (SERIAL_DEBUG)
          Serial.println("failed to load json config");
        #endif
      }
    }
    systemFile.close();
  } else {
    #if (SERIAL_DEBUG)
      Serial.println("no system file");
    #endif
  }
}

void webSocketEvent(uint8 num, WStype_t type, uint8 * payload, size_t length) {
  String tempStr;
  String data;
  uint8 scanResult;
  char macAddressConfig[23];
  sprintf(macAddressConfig, "mac|z|%02X%02X%02X%02X%02X%02X|z|%d", macAddress[0], macAddress[1], macAddress[2], macAddress[3], macAddress[4], macAddress[5], bitChannel);
  switch(type) {
    case WStype_DISCONNECTED:
    {
      #if (SERIAL_DEBUG)
        Serial.printf("[%u] Disconnected!\n", num);
      #endif
      break;
    }
    case WStype_CONNECTED:
    {
      #if (SERIAL_DEBUG)
        IPAddress ip = webSocket.remoteIP(num);
        Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
      #endif
      // send message to client
      webSocket.sendTXT(num, "connected|z|");
      break;
    }
    case WStype_TEXT:
    {
      #if (SERIAL_DEBUG)
        Serial.printf("[%u] get Text: %s\n", num, payload);
      #endif
      data.reserve(length+1); // prepare space for the buffer and extra termination character '\0'
      for (int i = 0; i<length; ++i) {
          data += (char)payload[i]; // typecast because String takes uint8_t as something else than char
      }
      if (getString(data, "|z|", 0) == "wifi") {
        webSocket.sendTXT(num, macAddressConfig);
        getChar(data, "|z|", ssidWiFi, 1);
        getChar(data, "|z|", passWiFi, 2);
        getChar(data, "|z|", uid, 3);
        saveConfigCallback();
      }
      if (data == "scan") {
        scanResult = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true);
        uint8 indices[scanResult];    
        for (uint8 i = 0; i < scanResult; i++) {
          indices[i] = i;
        }
        tempStr = "scanned|z|" + String(scanResult);
        for (uint8 i = 0; i < scanResult; i++) {
          for (uint8 j = i + 1; j < scanResult; j++) {
            if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
              std::swap(indices[i], indices[j]);   
            }
          }
        }
        for (uint8 i = 0; i < scanResult; i++) {
          tempStr = tempStr + "|z|" + WiFi.SSID(indices[i]) + "|z|" + WiFi.RSSI(indices[i]);
          delay(0);
        } 
        webSocket.sendTXT(num, tempStr);
        #if (SERIAL_DEBUG)
          Serial.println(tempStr);
        #endif
      }
      break;
    }
    case WStype_BIN:
    {
      #if (SERIAL_DEBUG)
        Serial.printf("[%u] get binary length: %u\n", num, length);
      #endif
      hexdump(payload, length);
      break;
    }
  }
}

void setupTask() {
  mainTimer_1s.register_callback(mainTask_1s);
  mainTimer_1m.register_callback(mainTask_1m);
  mainTimer_20ms.register_callback(mainTask_20ms);
}

void setupConnection() {
  char apWiFiName[22];
  sprintf(apWiFiName, "ZeusHD-%02X%02X%02X%02X%02X%02X", macAddress[0], macAddress[1], macAddress[2], macAddress[3], macAddress[4], macAddress[5]);
  if(flgSetupConfig) {
    IPAddress apIP(192, 168, 4, 1);   //Static IP for wifi gateway
    WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); //set Static IP gateway on NodeMCU
    WiFi.softAP(apWiFiName); //turn on WIFI
    webSocket.begin(); //websocket Begin
    webSocket.onEvent(webSocketEvent); //set Event for websocket
    #if (SERIAL_DEBUG)
      Serial.println("Websocket is started");
    #endif
    flgSetupConfig = true;
    stLedStatus = setupConfig;
  } else {
    WiFi.setAutoReconnect(true);
    WiFi.persistent(true);
    wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnect);
    wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnect);
    stLedStatus = connectingWiFi;
    WiFi.mode(WIFI_STA);
    WiFi.softAPdisconnect (true);
    mqttClient.onConnect(onMqttConnect);
    mqttClient.onDisconnect(onMqttDisconnect);
    mqttClient.onSubscribe(onMqttSubscribe);
    mqttClient.onUnsubscribe(onMqttUnsubscribe);
    mqttClient.onMessage(onMqttMessage);
    mqttClient.onPublish(onMqttPublish);
    mqttClient.setServer(server, MQTT_PORT);
    connectToWifi();
  }
  flgLedStatusUpdate = true;
}

void setupIO() {
  uint8 idxColor = 0;
  analogWriteFreq(freqLed);
  analogWriteRange(1000);
  for (idxColor = 0; idxColor < nColorMax; idxColor++) {
    analogWrite(Pin[idxColor], flgInverted ? 1000 : 0);
  }
  pinMode(pinFAN, OUTPUT);
}

void ini() {
  flgSetupConfig            = true;
  flgRTCSync                = false;
  flgReadTime               = true;
  flgAutoMode               = true;
  flgUpdateFirmware         = false;
  flgSchedulingFile         = true;
  flgSystemFile             = false;
  flgLedStatusUpdate        = true;
  enRTC                     = false;
  flgInverted               = false;
  flgReIniDevice            = false;
  bitChannel                = 0;
  freqLed                   = 1000;
  currentTime               = 0;
  for (uint8 idxSlot; idxSlot < 30; idxSlot++) {
    scheduling[idxSlot].time = 86400;
    for (uint8 idxColor; idxColor < nColorMax; idxColor++) {
      scheduling[idxSlot].color[idxColor] = 0;
    }
  }
  for (uint8 idxColor = 0; idxColor < nColorMax; idxColor++) {
    Pin[idxColor] = 255;
    colorValue[idxColor] = 0;
    colorValueOld[idxColor] = 0;
  }
}

void setup() {
  ini();
  WiFi.macAddress(macAddress);
  sprintf(fwVersionString, "%02X%02X%02X%02X%02X%02X|z|%d.%d.%d|z|null|z|null|z|null", macAddress[0], macAddress[1], macAddress[2], macAddress[3], macAddress[4], macAddress[5], fwVersion[0], fwVersion[1], fwVersion[2]);
  mrd = new MultiResetDetector(MRD_TIMEOUT, MRD_ADDRESS);
  #if (SERIAL_DEBUG)
    Serial.begin(115200);
  #endif
  if (mrd->detectMultiReset()) {
    #if (SERIAL_DEBUG)
      Serial.println("Multi Reset Detected");
    #endif
    flgReIniDevice = true;
  } else {
    #if (SERIAL_DEBUG)
      Serial.println("No Multi Reset Detected");
    #endif
  }
  pinMode(statusLED, OUTPUT);
  digitalWrite(statusLED, HIGH);
  pinMode(15, INPUT);
  timeClient.begin();
  setupConfigFile();
  setupScheduling();
  setupSystemFile();
  setupIO();
  setupTask();
  setupConnection();
  if (enRTC) {
    rtc.begin();
  }
}

void mainTask_1m() {
  if (WiFi.isConnected()) {
    timeClient.update();
    if (flgRTCSync) {
      if (enRTC) {
        rtc.adjust(DateTime(2021, 1, 1, timeClient.getHours(), timeClient.getMinutes(), timeClient.getSeconds()));
      }
      flgRTCSync = false;
    }
    currentTime = timeToSec(timeClient.getHours(), timeClient.getMinutes(), timeClient.getSeconds());
  } else {
    if (enRTC) {
      DateTime now = rtc.now();
      currentTime = timeToSec(now.hour(), now.minute(), now.second());
    }
  }
  flgReadTime = false;
}

void mainTask_1s() {
  uint8       idxColor            = 0;
  uint8       idxSlot             = 0;
  uint32      firstTime           = scheduling[0].time;
  uint32      lastTime            = scheduling[schedulingMaxIndex].time;
  uint32      baseTime            = 0;
  uint32      nextTime            = 0;
  uint32      pwmBase             = 0;
  uint32      pwmNext             = 0;
  if (autoModeCountDown.remaining() == 0) {
    flgAutoMode = true;
  }
  if (flgReadTime) {
    mainTask_1m();
  }
  if (digitalRead(15)) {
    if (cntReset < 5) {
      cntReset++;
    } else {
      flgReIniDevice = true;
    }
  } else {
    cntReset = 0;
  }
  if (currentTime < timeToSec(23, 59, 59)) {
    currentTime++;
  } else {
    currentTime = 0;
  }
  if (flgAutoMode) {
    sprintf(fwVersionString, "%02X%02X%02X%02X%02X%02X|z|%d.%d.%d|z|null|z|null|z|%d", macAddress[0], macAddress[1], macAddress[2], macAddress[3], macAddress[4], macAddress[5], fwVersion[0], fwVersion[1], fwVersion[2], currentTime);
    if (currentTime < firstTime || lastTime < currentTime) {
      for (idxColor = 0; idxColor < nColorMax ; idxColor++) {
        if (Pin[idxColor] != 255) {
          colorValue[idxColor] = scheduling[schedulingMaxIndex].color[idxColor] * 10;
        }
      }
    } else {
      for (idxSlot = 0; idxSlot < schedulingMaxIndex; idxSlot++) {
        baseTime = scheduling[idxSlot].time;
        nextTime = scheduling[idxSlot + 1].time;
        if (currentTime == baseTime) {
          for (idxColor = 0; idxColor < nColorMax ; idxColor++) {
            if (Pin[idxColor] != 255) {
              colorValue[idxColor] = scheduling[idxSlot].color[idxColor] * 10;
            }
          }
          break;
        } else if (baseTime < currentTime && currentTime < nextTime) {
          for (idxColor = 0; idxColor < nColorMax ; idxColor++) {
            pwmNext = scheduling[idxSlot + 1].color[idxColor] * 10000;
            pwmBase = scheduling[idxSlot].color[idxColor] * 10000;
            if (Pin[idxColor] != 255) {
              if (pwmNext > pwmBase) {
                colorValue[idxColor] = (uint16)((pwmBase + (((pwmNext - pwmBase) / (nextTime - baseTime)) * (currentTime - baseTime))) / 1000);
              } else if (pwmNext < pwmBase) {
                colorValue[idxColor] = (uint16)((pwmBase - (((pwmBase - pwmNext) / (nextTime - baseTime)) * (currentTime - baseTime))) / 1000);
              } else {
                colorValue[idxColor] = (uint16)(pwmBase / 1000);
              }
            }
          }
          break;
        }
      }
    }
  }
}

void mainTask_20ms() {
  bool  flgFan = false;
  uint8 idxColor = 0;
  for (idxColor = 0; idxColor < nColorMax; idxColor++) {
    if (Pin[idxColor] != 255) {
      if (colorValueOld[idxColor] < colorValue[idxColor]) {
        if ((colorValue[idxColor] - colorValueOld[idxColor]) > 20) {
          colorValueOld[idxColor] += 20;
        } else {
          colorValueOld[idxColor] = colorValue[idxColor];
        }
      } else if (colorValueOld[idxColor] > colorValue[idxColor]) {
        if ((colorValueOld[idxColor] - colorValue[idxColor]) > 20) {
          colorValueOld[idxColor] -= 20;
        } else {
          colorValueOld[idxColor] = colorValue[idxColor];
        }
      }
      analogWrite(Pin[idxColor], flgInverted ? (1000 - colorValueOld[idxColor]) : colorValueOld[idxColor]);
      flgFan = (colorValueOld[idxColor] != 0) | flgFan;
    }
  }
  digitalWrite(pinFAN, flgFan);
}

void loop() {
  const char* fwUrlOTAstr = fwUrlOTA.c_str();
  mrd->loop();
  if (flgSetupConfig) {
    webSocket.loop();
  } else {
    if (flgReIniDevice) {
      #if (SERIAL_DEBUG)
        Serial.println("ReInitial Deivce");
      #endif
      SPIFFS.remove("/scheduling.json");
      SPIFFS.remove("/configWiFi.json");
      if (!SPIFFS.exists("/scheduling.json") && !SPIFFS.exists("/configWiFi.json")) {
        ESP.restart();
      }
    } else if (flgUpdateFirmware && WiFi.isConnected()) {
      #if (SERIAL_DEBUG)
        Serial.println("Update FW!!!!!!");
      #endif
      t_httpUpdate_return ret = ESPhttpUpdate.update(client, fwUrlOTAstr);
      flgUpdateFirmware = false;
    } else if (flgSchedulingFile && WiFi.isConnected()) {
      bool matchVersion = true;
      WiFiClient client;
      HTTPClient httpClient;
      String url = "/scheduling/";
      url.concat(String(uid));
      url.concat(".json");
      #if (SERIAL_DEBUG)
        Serial.println(url);
      #endif
      File schedulingFile = SPIFFS.open("/scheduling.json", "w");
      if (schedulingFile) {
        httpClient.begin(client,"zeushd.duckdns.org", 80, url);
        int httpCode = httpClient.GET();
        if (httpCode > 0) {
          if (httpCode == HTTP_CODE_OK) {
            DynamicJsonDocument downloadedJson(2048);
            DeserializationError schedulingError = deserializeJson(downloadedJson, httpClient.getString().c_str());
            #if (SERIAL_DEBUG)
              serializeJson(downloadedJson, Serial);
            #endif
            if (!schedulingError) {
              if (downloadedJson.containsKey("version")) {
                matchVersion = downloadedJson["version"] == schedulingVersion;
                if (!matchVersion) {
                  #if (SERIAL_DEBUG)
                    Serial.println("Writing File!!!");
                  #endif
                  schedulingFile.println(httpClient.getString());
                }
              }
            }
          }
        } else { 
          #if (SERIAL_DEBUG)
            Serial.printf("[HTTP] GET... failed, error: %s\n", httpClient.errorToString(httpCode).c_str());
          #endif
        }
      }
      schedulingFile.close();
      httpClient.end();
      if (!matchVersion) {
        setupScheduling();
      }
      flgSchedulingFile = false;
    } else if (flgSystemFile && WiFi.isConnected()) {
      WiFiClient client;
      HTTPClient httpClient;
      #if (SERIAL_DEBUG)
        Serial.println(systemURL);
      #endif
      File systemFile = SPIFFS.open("/system.json", "w");
      if (systemFile) {
        httpClient.begin(client, systemURL);
        int httpCode = httpClient.GET();
        if (httpCode > 0) {
          if (httpCode == HTTP_CODE_OK) {
            systemFile.println(httpClient.getString());
          }
        } else {
          #if (SERIAL_DEBUG)
            Serial.printf("[HTTP] GET... failed, error: %s\n", httpClient.errorToString(httpCode).c_str());
          #endif
        }
      }
      systemFile.close();
      httpClient.end();
      flgReIniDevice = true;
    } else {
      mainTimer_1m.run(60000);
      mainTimer_1s.run(1000);
      mainTimer_20ms.run(20);
    }
  }
  if (flgLedStatusUpdate) {
    switch (stLedStatus) {
      case setupConfig:
        ledStatusTimer.attach_ms(100, ledStatus, (uint8)10);
        break;
      case connectedServer:
        ledStatusTimer.detach();
        digitalWrite(statusLED, LOW);
        break;
      case connectingServer:
        ledStatusTimer.attach_ms(1000, ledStatus, (uint8)0);
        break;
      case connectingWiFi:
        ledStatusTimer.attach_ms(200, ledStatus, (uint8)0);
        break;
    }
    flgLedStatusUpdate = false;
  }
}
