#include <WiFi.h>

#define NTP_SERVER     "pool.ntp.org"
#define UTC_OFFSET     28800
#define UTC_OFFSET_DST 0

/* spiffs session */
#include "SPIFFS.h"
bool fsInit = false;
String opchannel_text = ""; //ouptut channel data for save to spiffs

/* input session */
#define ipTotal 4
struct ip_struct {
  String type, id, valkey, value, unit, temp; //type,id,inputkey,value* ,unit,temp
} ipsensor[ipTotal] = {
  { "ph",   "10", "ph",   "0", "pH",    "0"},
  { "cond", "11", "cond", "0", "us/cm", "0"},
  { "fcl",  "12", "fcl",  "0", "mg/L",  "0"},
  { "orp",  "13", "orp",  "0", "mV",    "0"}
} ;

/* ouptput session */
#define opTotal  8
struct op_struct {
  String hoa, mode, key, lo, hi, db, DD, MM, hh, mm, wd, dur, to; //hoa,mode,inputkey,splow,sphigh,spdeadband,day,month,hour,minutes,weekday,duration,timeout
} opchannel[opTotal] = {
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
  { "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
};
unsigned long op_actiontimer[opTotal]    = { 0, 0, 0, 0, 0, 0, 0, 0 }; //keep relay output turn-on time
uint16_t      op_remainduration[opTotal] = { 0, 0, 0, 0, 0, 0, 0, 0 }; //keep remain duration when priority stop it
uint8_t       op_setpointtype[opTotal]   = { 0, 0, 0, 0, 0, 0, 0, 0 }; //type 0:setpoint low / 1:setpoint high
uint8_t       op_status[opTotal]         = { 0, 0, 0, 0, 0, 0, 0, 0 }; //0: not run, 1: mark need turn on 2: status on 3: status off timeout
int8_t        priority                   = -1;

/* other session */
bool  dir1 = 0;
bool  dir2 = 1;

void setup() {
  Serial.begin(115200);

  loadOutputConfig();

  /*
    // WiFi.begin("Wokwi-GUEST", "", 6);
    WiFi.begin("HighTalent", "abba123456", 6);
    while (WiFi.status() != WL_CONNECTED) {
     delay(250);
    }

    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    configTime(UTC_OFFSET, UTC_OFFSET_DST, NTP_SERVER);
  */

  //for test input config
  ipsensor[0].value = "7";
  ipsensor[1].value = "50";

  //for test output config
  opchannel[0].mode = "onoff";
  opchannel[0].key  = "ph";
  opchannel[0].lo   = "5.5";
  opchannel[0].hi   = "7.8";
  opchannel[0].db   = "0";
  opchannel[0].dur  = "120";

  opchannel[2].mode = "onoff";
  opchannel[2].key  = "cond";
  opchannel[2].lo   = "10";
  opchannel[2].hi   = "70";
  opchannel[2].db   = "12";
  opchannel[2].dur  = "120";
  //opchannel[0].DD   = "";
  //opchannel[0].MM   = "";
  //opchannel[0].hh   = "16";
  //opchannel[0].mm   = "04";
  //opchannel[0].wd   = "";
  //opchannel[0].dur  = "120";
  //opchannel[0].to   = "60";

  //
  opchannel[1].mode = "timer";
  //opchannel[1].key  = "ph";
  //opchannel[1].lo  = "5.5";
  //opchannel[1].hi  = "8.5";
  //opchannel[1].dur  = "120";
  //opchannel[1].to   = "60";

  //opchannel[1].DD   = "";
  //opchannel[1].MM   = "";
  opchannel[1].hh   = "17";
  opchannel[1].mm   = "52";
  //opchannel[1].wd   = "";
  opchannel[1].dur  = "120";
  opchannel[1].to   = "60";

  saveOutputConfig();

  Serial.println("-------- setup done --------");

}

void loop() {

  if (ipsensor[0].value.toFloat() > 9) dir1 = 0;
  if (ipsensor[0].value.toFloat() < 5 ) dir1 = 1;
  if (dir1) ipsensor[0].value = String((ipsensor[0].value.toFloat() + 0.5), 2); else ipsensor[0].value = String((ipsensor[0].value.toFloat() - 0.5), 2);
  Serial.printf("%s : %s %s \n", ipsensor[0].valkey.c_str(), ipsensor[0].value.c_str(), ipsensor[0].unit.c_str());

  if (ipsensor[1].value.toFloat() > 100) dir2 = 0;
  if (ipsensor[1].value.toFloat() < 20 ) dir2 = 1;
  if (dir2)  ipsensor[1].value  = String((ipsensor[1].value.toFloat() + 15), 2);  else ipsensor[1].value  = String((ipsensor[1].value.toFloat() - 15), 2);
  Serial.printf("%s : %s %s \n", ipsensor[1].valkey.c_str(), ipsensor[1].value.c_str(), ipsensor[1].unit.c_str());

  handleRelayTask();
  delay(3000);

}

void handleRelayTask() {
  bool debug = true;
  Serial.println("----------------------------\n Handle Ouptut Channel Task\n----------------------------");

  verfiyTimer() ;
  for (int i = 0; i < opTotal; i++) {             //Do all channels

    if (debug) Serial.print( i == priority ? "[ *" : "[  " );
    if (debug) Serial.printf("Channel #%02d ] hoa %4s | %6s mode ", i, opchannel[i].hoa.c_str(), opchannel[i].mode.c_str() );

    if ((opchannel[i].hoa == "auto") && (opchannel[i].mode == "onoff")) {    //mode : onoff mode
      //default
      if ( opchannel[i].dur == "*") opchannel[i].dur = "30"; //default druration 30s
      if ( opchannel[i].to == "*")  opchannel[i].to = "60";  //default cd time 60s

      if (debug) Serial.printf("| %s ( %s - %s )\n", opchannel[i].key, opchannel[i].lo, opchannel[i].hi);

      if (op_status[i] == 0) {  //onoff status 0 : normal
        if (checkSetpoint(i)) op_status[i] = 1;
      }
      if (op_status[i] == 1)  {
        if (op_status[priority] == 2) {  //when active, priority running so do nothing
          Serial.printf("\n!! relay(%02d) terminate because first priority are running...\n", i);
          op_status[i] = 0;
        } else {    //when active, no priority running so go to switch relay on
          Serial.printf("need to switch on relay(%02d) duration %s s\n", i, opchannel[i].dur.c_str());
          if (i == priority) {  //this is priority, try to switch of other relay
            Serial.println("!! priority lock & terminate other channel...");
            for (int i = 0; i < opTotal; i++) {  //search all channel
              if (i != priority) {  //non priority so check run status
                if ((op_status[i] == 2) || (op_status[i] == 5)) { //when relay running stop it
                  if (opchannel[i].mode == "timer") {
                    op_remainduration[i] = opchannel[i].dur.toInt() - (millis() - op_actiontimer[i]) / 1000 ;
                    Serial.printf("!! relay(%02d) are running... turn it off... remain duration : %d s\n", i, op_remainduration[i]);
                    op_status[i] = 4;
                  } else {
                    Serial.printf("!! relay(%02d) are running... turn it off...\n", i);
                    op_status[i] = 0;
                  }
                  //switch off relay
                }
              }
            }
          }
          //switch on relay
          op_status[i] = 2;
          op_actiontimer[i]  = millis();
        }
      }
      if (op_status[i] == 2) {
        Serial.printf("!! relay(%02d) status : [ ON ]\n", i);
        if ( millis() - op_actiontimer[i] > opchannel[i].dur.toInt() * 1000 ) {  //turn off relay after duration
          Serial.printf("!! time's up... switch off relay(%02d) and pause status for %s s\n", i, opchannel[i].to.c_str());
          //switch off relay
          op_status[i] = 3;
          op_actiontimer[i]  = millis();
        }
        if (((millis() - op_actiontimer[i]) > 1000 ) && checkDeadband(i)) {
          Serial.printf("!! switch off relay(%02d) and pause status for %s s\n", i, opchannel[i].to.c_str());
          //switch off relay
          op_status[i] = 3;
          op_actiontimer[i]  = millis();
        }
      }
      if (op_status[i] == 3) {
        Serial.printf("!! waiting for relay(%02d) timeout...\n", i);
        if ( millis() - op_actiontimer[i] >  opchannel[i].to.toInt() * 1000 ) { //clear status after duration time
          Serial.printf("!! time's up... free relay(%02d) status...\n", i);
          op_status[i] = 0;
        }
      }
    }

    else if ((opchannel[i].hoa == "auto") && (opchannel[i].mode == "timer"))  {    //mode : timer mode
      //default
      if ( opchannel[i].mm == "*")  opchannel[i].mm  = "00"; //default second 00
      if ( opchannel[i].dur == "*") opchannel[i].dur = "30"; //default druration 30s
      if ( opchannel[i].to == "*")  opchannel[i].to = "60";  //default cd time 60s

      if (debug) Serial.printf("| %s/%s %s:%s (%s)\n", opchannel[i].DD.c_str(), opchannel[i].MM.c_str(), opchannel[i].hh.c_str(), opchannel[i].mm.c_str(), opchannel[i].wd.c_str());
      if (op_status[i] == 1) {
        if (op_status[priority] == 2) {  //when active, priority running so do nothing
          Serial.printf("!! relay(%02d) terminate because first priority are running...\n", i);
          op_status[i] = 0;
        } else {    //when active, no priority running so go to switch relay on
          Serial.printf("!! work according to schedule... switch on relay(%02d) duration : %s s\n", i, opchannel[i].dur.c_str());
          if (i == priority) {  //this is priority, try to switch of other relay
            Serial.println("!! priority lock & terminate other channel...");
            for (int i = 0; i < opTotal; i++) {  //search all channel
              if (i != priority) {  //non priority so check run status
                if ((op_status[i] == 2) || (op_status[i] == 5)) {  //when relay running stop it
                  if (opchannel[i].mode == "timer") {
                    op_remainduration[i] = opchannel[i].dur.toInt() - (millis() - op_actiontimer[i]) / 1000 ;
                    Serial.printf("!! relay(%02d) are running... turn it off... remain duration : %d s\n", i, op_remainduration[i]);
                    op_status[i] = 4;
                  } else {
                    Serial.printf("!! relay(%02d) are running... turn it off...\n", i);
                    op_status[i] = 0;
                  }
                  //switch off relay
                }
              }
            }
          }
          //switch on relay
          op_status[i] = 2;
          op_actiontimer[i]  = millis();
        }
      }
      if (op_status[i] == 2) {
        Serial.printf("!! relay(%02d) status : [ ON ]\n", i);
        if ( millis() - op_actiontimer[i] > opchannel[i].dur.toInt() * 1000 ) {  //turn off relay after duration
          Serial.printf("!! time's up... switch off relay(%02d) pause status for a while\n", i);
          //switch off relay
          op_status[i] = 3;
        }
      }
      if (op_status[i] == 3) {
        Serial.printf("!! waiting for relay(%02d) timeout...\n", i);
        if ( millis() - op_actiontimer[i] > 60000 ) { //clear status after least 1 minutes
          Serial.printf("!! time's up... free relay(%02d) status...\n", i);
          op_status[i] = 0;
        }
      }

      if (op_status[i] == 4) {
        if (op_status[priority] != 2) {  //when active, priority running so do nothing
          Serial.printf("!! turn on relay(%02d) for remain duration : %d s\n", i, op_remainduration[i]);
          //turn on relay
          op_status[i] = 5;
          op_actiontimer[i]  = millis();
        }
      }
      if (op_status[i] == 5) {
        Serial.printf("!! relay(%02d) status : [ ON ]\n", i);
        if ( millis() - op_actiontimer[i] > op_remainduration[i] * 1000 ) {  //turn off relay after duration
          Serial.printf("!! time's up... switch off relay(%02d) pause status for a while\n", i);
          //switch off relay
          op_status[i] = 3;
        }
      }
    }
    else if ((opchannel[i].hoa == "auto") && (opchannel[i].mode == "manual"))  {
      if (debug) Serial.println("| manual");
    }
    else if (opchannel[i].hoa == "hand") {
      if (debug) Serial.println("| hand on");
      if (op_status[i] != 2) {
        //switch on relay
        op_status[i] = 2;
      }
      Serial.printf("!! relay(%02d) status : [ HOA ON ]\n", i);
    }
    else if (opchannel[i].hoa == "off") {
      if (debug) Serial.println("");
      if (op_status[i] != 0) {
        //switch off relay
        op_status[i] = 0;
        Serial.printf("!! relay(%02d) status : [ HOA OFF ]\n", i);
      }
    }

  }
  Serial.println("");
}

bool initSPIFFS() {
  bool succ = true;
  if (!SPIFFS.begin(true)) {
    Serial.println("An Error has occurred while mounting SPIFFS");
    succ = false;
  }
  //if (SPIFFS.format()) Serial.println("Success formatting"); else Serial.println("Error formatting");
  return succ;
}

void saveOutputConfig() {
  bool debug     = false;
  opchannel_text = "";
  if (debug) Serial.println("----------------------------\n save output config to spiffs...\n----------------------------");

  for (int i = 0; i < opTotal; i++) {
    opchannel_text += opchannel[i].hoa + "," + opchannel[i].mode + "," + opchannel[i].key + "," + opchannel[i].lo + "," + opchannel[i].hi + "," + opchannel[i].db + "," + opchannel[i].DD + "," + opchannel[i].MM + "," + opchannel[i].hh + "," + opchannel[i].mm + "," + opchannel[i].wd + "," + opchannel[i].dur + "," + opchannel[i].to + "\n"    ;
  }
  if (debug) Serial.println("content of output.cfg : ");
  if (debug) Serial.println(opchannel_text);

  if (!fsInit) fsInit = initSPIFFS();
  if ( fsInit ) {
    String rcfilename = "/output.cfg";
    Serial.print("writing output config to spiffs ");
    File file = SPIFFS.open(rcfilename, FILE_WRITE);
    if (!file) {
      Serial.println("failed to open file for writing"); return;
    }
    if (file.print(opchannel_text)) Serial.println("− success!!"); else Serial.println("− failed!!");
  } else Serial.println("spiffs init fails...");

}

void loadOutputConfig() {
  bool debug = false;
  int ptr    = 0;
  int row    = 0;
  if (debug) Serial.println("----------------------------\n load output config from spiffs...\n----------------------------");

  if (!fsInit) fsInit = initSPIFFS();
  if ( fsInit ) {
    String rcfilename = "/output.cfg";

    Serial.print("reading output config from spiffs ");
    File file = SPIFFS.open(rcfilename);
    if (!file) {
      Serial.println("failed to open file for reading");
      return;
    } else {
      char string[file.size()];
      file.read((uint8_t *)string, sizeof(string));
      string[file.size()] = '\0';
      file.close();
      if (sizeof(string) > 0) {
        Serial.println("- success!!");
        opchannel_text = String(string);
      } else {
        Serial.println("- failed!! try init. output config file...");
        saveOutputConfig();
      }
    }
  } else {
    Serial.println("spiffs init fails...");
  }
  if (debug) Serial.println("content of output.cfg : ");
  if (debug) Serial.println(opchannel_text);
  if (debug) Serial.println("apply ouptut config to system...");
  Serial.println("relay output :  hoa |   mode |    key |     lo |     hi |    db | DD | MM | hh | mm |  wd |   dur |    to");
  for (int i = 0; i < opchannel_text.length(); i++)  {
    if (opchannel_text.charAt(i) == '\n') {
      String inputString = opchannel_text.substring(ptr, i);
      char *token = strtok((char *)inputString.c_str(), ",");
      opchannel[row].hoa  = String(token); token = strtok(NULL, ",");
      opchannel[row].mode = String(token); token = strtok(NULL, ",");
      opchannel[row].key  = String(token); token = strtok(NULL, ",");
      opchannel[row].lo   = String(token); token = strtok(NULL, ",");
      opchannel[row].hi   = String(token); token = strtok(NULL, ",");
      opchannel[row].db   = String(token); token = strtok(NULL, ",");
      opchannel[row].DD   = String(token); token = strtok(NULL, ",");
      opchannel[row].MM   = String(token); token = strtok(NULL, ",");
      opchannel[row].hh   = String(token); token = strtok(NULL, ",");
      opchannel[row].mm   = String(token); token = strtok(NULL, ",");
      opchannel[row].wd   = String(token); token = strtok(NULL, ",");
      opchannel[row].dur  = String(token); token = strtok(NULL, ",");
      opchannel[row].to   = String(token);
      Serial.printf("opchannel[%d] : %4s | %6s | %6s | %6s | %6s | %5s | %2s | %2s | %2s | %2s | %3s | %5s | %5s\n", row, opchannel[row].hoa, opchannel[row].mode, opchannel[row].key, opchannel[row].lo, opchannel[row].hi, opchannel[row].db, opchannel[row].DD, opchannel[row].MM, opchannel[row].hh, opchannel[row].mm, opchannel[row].wd, opchannel[row].dur, opchannel[row].to );
      ptr = i + 1 ;
      row++;
    }
  }
}
/*
  bool checkSetpoint(int idx, String setpointLo, String setpointHi ) {
  bool succ = false;
  if (opchannel[idx].key == "ph") {
    if ((setpointLo != "*") && (ph <= setpointLo.toFloat())) {
      Serial.printf("!! ph : %.2f <= %s | setpoint low trigger ", ph, setpointLo.c_str()); succ = true; op_setpointtype[idx] = 0;
    }
    if ((setpointHi != "*") && (ph >= setpointHi.toFloat())) {
      Serial.printf("!! ph : %.2f >= %s | setpoint high trigger ", ph, setpointHi.c_str()); succ = true; op_setpointtype[idx] = 1;
    }
  } else if (opchannel[idx].key == "ec") {
    if ((setpointLo != "*") && (ec <= setpointLo.toFloat())) {
      Serial.printf("!! ec : %.2f <= %s | setpoint low trigger ", ec, setpointLo.c_str()); succ = true;  op_setpointtype[idx] = 0;
    }
    if ((setpointHi != "*") && (ec >= setpointHi.toFloat())) {
      Serial.printf("!! ec : %.2f >= %s | setpoint high trigger ", ec, setpointHi.c_str()); succ = true; op_setpointtype[idx] = 1;
    }
  }
  return succ;
  }
*/
bool checkSetpoint(int idx) {
  bool succ = false;
  for (int i = 0; i < ipTotal; i++) {
    if (opchannel[idx].key == ipsensor[i].valkey ) {
      if ((opchannel[idx].lo != "*") && (ipsensor[i].value.toFloat() <= opchannel[idx].lo.toFloat())) {
        Serial.printf("!! %s : %s <= %s | setpoint low trigger ", ipsensor[i].valkey.c_str(), ipsensor[i].value.c_str(), opchannel[idx].lo.c_str()); succ = true; op_setpointtype[idx] = 0;
      }
      if ((opchannel[idx].hi != "*") && (ipsensor[i].value.toFloat() >= opchannel[idx].hi.toFloat())) {
        Serial.printf("!! %s : %s >= %s | setpoint high trigger ", ipsensor[i].valkey.c_str(), ipsensor[i].value, opchannel[idx].hi.c_str()); succ = true; op_setpointtype[idx] = 1;
      }
    }
  }

  return succ;
}

/*
  bool checkDeadband(int idx, String setpointLo, String setpointHi, String deadBand )  {
  bool succ = false;
  if ((deadBand == "*" ) || (deadBand == "0" )) return succ;
  if (opchannel[idx].key == "ph") {
    if (( op_setpointtype[idx] == 0) && (ph >= setpointLo.toFloat() + deadBand.toFloat())) {
      Serial.printf("!! ph : %.2f >= %s + %s | deadband reached ", ph, setpointLo.c_str(), deadBand.c_str()); succ = true;
    }
    if (( op_setpointtype[idx] == 1) && (ph <= setpointHi.toFloat() - deadBand.toFloat())) {
      Serial.printf("!! ph : %.2f <= %s - %s | deadband reached ", ph, setpointHi.c_str(), deadBand.c_str()); succ = true;
    }
  } else if (opchannel[idx].key == "ec") {
    if ((setpointLo != "*") && (ec >= setpointLo.toFloat() + deadBand.toFloat())) {
      Serial.printf("!! ec : %.2f >= %s + %s | deadband reached ", ec, setpointLo.c_str(), deadBand.c_str()); succ = true;
    }
    if ((setpointHi != "*") && (ec <= setpointHi.toFloat() - deadBand.toFloat())) {
      Serial.printf("!! ec : %.2f <= %s - %s | deadband reached ", ec, setpointHi.c_str(), deadBand.c_str()); succ = true;
    }
  }
  return succ;
  }
*/

bool checkDeadband(int idx) {
  bool succ = false;
  if (opchannel[idx].db  == "*" ) return succ;
  for (int i = 0; i < ipTotal; i++) {
    if (opchannel[idx].key == ipsensor[i].valkey ) {
      if (( op_setpointtype[idx] == 0) && (ipsensor[i].value.toFloat()  >= opchannel[idx].lo.toFloat() + opchannel[idx].db.toFloat())) {
        Serial.printf("!! %s : %s >= %s + %s | deadband reached ", ipsensor[i].valkey.c_str(), ipsensor[i].value.c_str(), opchannel[idx].lo.c_str(), opchannel[idx].db.c_str()); succ = true; return succ;
      }
      if (( op_setpointtype[idx] == 1) && (ipsensor[i].value.toFloat()  <= opchannel[idx].hi.toFloat() - opchannel[idx].db.toFloat())) {
        Serial.printf("!! %s : %s <= %s - %s | deadband reached ", ipsensor[i].valkey.c_str(), ipsensor[i].value.c_str(), opchannel[idx].hi.c_str(), opchannel[idx].db.c_str()); succ = true; return succ;
      }
    }
  } return succ;
}

void verfiyTimer() {
  bool debug = false;
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time"); return;
  }
  Serial.println(&timeinfo, "System Clock : %d/%m/%Y %H:%M (%a)" );

  char timeVar[16];
  strftime( timeVar, sizeof(timeVar), "%d,%m,%H,%M,%a", &timeinfo );   //eg: 02,23,10,45,Fri

  String timeVal[5];
  char* token = strtok(timeVar, ",");
  uint8_t i = 0;
  while (token != NULL) {
    timeVal[i] = String(token); token = strtok(NULL, ","); i++;
  }

  for (int i = 0; i < opTotal; i++) {
    if (opchannel[i].mode != "onoff") {
      bool succ = true;
      if ((opchannel[i].DD != "*") && (opchannel[i].DD != timeVal[0])) succ = false;
      if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[0], opchannel[i].DD, succ);
      if ((opchannel[i].MM != "*") && (opchannel[i].MM != timeVal[1])) succ = false;
      if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[1], opchannel[i].MM, succ);
      if ((opchannel[i].hh != "*") && (opchannel[i].hh != timeVal[2])) succ = false;
      if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[2], opchannel[i].hh, succ);
      if ((opchannel[i].mm != "*") && (opchannel[i].mm != timeVal[3])) succ = false;
      if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[3], opchannel[i].mm, succ);
      if ((opchannel[i].wd != "*") && (opchannel[i].wd != timeVal[4])) succ = false;
      if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[4], opchannel[i].wd, succ);
      if ( debug ) Serial.printf("timer rc[%d] - trigger : %d\n", i, succ );
      if ( op_status[i] == 0 ) op_status[i] = succ;
    }
  }
}