#include <Blinker.h>

using namespace NetworkUtils;

extern NetworkMgr* network;
extern MqttTask* tMqtt;
extern OpenThermTask* tOt;
extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings;


class MainTask : public Task {
public:
  explicit MainTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
    this->blinker = new Blinker();

    network->setDelayCallback([this](unsigned int time) {
      this->delay(time);
    })->setYieldCallback([this]() {
      this->yield();
    });
  }

  ~MainTask() {
    delete this->blinker;
  }

protected:
  enum class PumpStartReason {NONE, HEATING, ANTISTUCK};

  Blinker* blinker = nullptr;
  unsigned long miscRunned = 0;
  unsigned long lastHeapInfo = 0;
  unsigned int minFreeHeap = 0;
  unsigned int minMaxFreeBlockHeap = 0;
  bool restartSignalReceived = false;
  unsigned long restartSignalReceivedTime = 0;
  bool heatingEnabled = false;
  unsigned long heatingDisabledTime = 0;
  PumpStartReason extPumpStartReason = PumpStartReason::NONE;
  unsigned long externalPumpStartTime = 0;
  bool ntpStarted = false;
  bool emergencyDetected = false;
  unsigned long emergencyFlipTime = 0;

  #if defined(ARDUINO_ARCH_ESP32)
  const char* getTaskName() override {
    return "Main";
  }

  uint32_t getTaskStackSize() override {
    return 6000;
  }

  /*BaseType_t getTaskCore() override {
    return 1;
  }*/

  int getTaskPriority() override {
    return 3;
  }
  #endif

  void setup() {}

  void loop() {
    network->loop();

    if (fsNetworkSettings.tick() == FD_WRITE) {
      Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
    }

    if (fsSettings.tick() == FD_WRITE) {
      Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
    }

    if (fsSensorsSettings.tick() == FD_WRITE) {
      Log.sinfoln(FPSTR(L_SENSORS_SETTINGS), F("Updated"));
    }

    if (vars.actions.restart) {
      this->restartSignalReceivedTime = millis();
      this->restartSignalReceived = true;
      vars.actions.restart = false;

      Log.sinfoln(FPSTR(L_MAIN), F("Received restart signal"));
    }

    if (!vars.states.restarting && this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 5000) {
      vars.states.restarting = true;

      // save settings
      fsSettings.updateNow();

      // save sensors settings
      fsSensorsSettings.updateNow();

      // force save network settings
      if (fsNetworkSettings.updateNow() == FD_FILE_ERR && LittleFS.begin()) {
        fsNetworkSettings.write();
      }

      Log.sinfoln(FPSTR(L_MAIN), F("Restart scheduled in 10 sec."));
    }

    vars.mqtt.connected = tMqtt->isConnected();
    vars.network.connected = network->isConnected();
    vars.network.rssi = network->isConnected() ? WiFi.RSSI() : 0;

    if (settings.system.logLevel >= TinyLoggerLevel::SILENT && settings.system.logLevel <= TinyLoggerLevel::VERBOSE) {
      if (Log.getLevel() != settings.system.logLevel) {
        Log.setLevel(static_cast<TinyLoggerLevel>(settings.system.logLevel));
      }
    }

    if (network->isConnected()) {
      if (!this->ntpStarted) {
        if (strlen(settings.system.ntp.server)) {
          configTime(0, 0, settings.system.ntp.server);
          setenv("TZ", settings.system.ntp.timezone, 1);
          tzset();

          this->ntpStarted = true;
        }
      }

      if (settings.mqtt.enabled && !tMqtt->isEnabled()) {
        tMqtt->enable();

      } else if (!settings.mqtt.enabled && tMqtt->isEnabled()) {
        tMqtt->disable();
      }

    } else {
      if (this->ntpStarted) {
        this->ntpStarted = false;
      }

      if (tMqtt->isEnabled()) {
        tMqtt->disable();
      }

      Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, false, false);
    }

    this->yield();
    if (this->misc()) {
      this->yield();
    }
    this->ledStatus();


    // anti memory leak
    while (Serial.available() > 0) {
      Serial.read();
    }

    // heap info
    this->heap();
  }

  bool misc() {
    if (millis() - this->miscRunned < 1000) {
      return false;
    }

    // restart if required
    if (this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 15000) {
      this->restartSignalReceived = false;

      ESP.restart();
    }

    this->heating();
    this->emergency();
    this->cascadeControl();
    this->externalPump();
    this->miscRunned = millis();

    return true;
  }

  void heap() {
    unsigned int freeHeap = getFreeHeap();
    unsigned int maxFreeBlockHeap = getMaxFreeBlockHeap();

    // critical heap
    if (!vars.states.restarting && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
      this->restartSignalReceivedTime = millis();
      this->restartSignalReceived = true;
      vars.states.restarting = true;
    }

    if (settings.system.logLevel < TinyLoggerLevel::VERBOSE) {
      return;
    }

    size_t minFreeHeap = getFreeHeap(true);
    size_t minFreeHeapDiff = 0;
    if (minFreeHeap < this->minFreeHeap || this->minFreeHeap == 0) {
      minFreeHeapDiff = this->minFreeHeap - minFreeHeap;
      this->minFreeHeap = minFreeHeap;
    }

    size_t minMaxFreeBlockHeap = getMaxFreeBlockHeap(true);
    size_t minMaxFreeBlockHeapDiff = 0;
    if (minMaxFreeBlockHeap < this->minMaxFreeBlockHeap || this->minMaxFreeBlockHeap == 0) {
      minMaxFreeBlockHeapDiff = this->minMaxFreeBlockHeap - minMaxFreeBlockHeap;
      this->minMaxFreeBlockHeap = minMaxFreeBlockHeap;
    }

    if (millis() - this->lastHeapInfo > 20000 || minFreeHeapDiff > 0 || minMaxFreeBlockHeapDiff > 0) {
      Log.sverboseln(
        FPSTR(L_MAIN),
        F("Free heap size: %u of %u bytes (min: %u, diff: %u), max free block: %u (min: %u, diff: %u, frag: %hhu%%)"),
        freeHeap, getTotalHeap(), this->minFreeHeap, minFreeHeapDiff, maxFreeBlockHeap, this->minMaxFreeBlockHeap, minMaxFreeBlockHeapDiff, getHeapFrag()
      );
      this->lastHeapInfo = millis();
    }
  }

  void heating() {
    // freeze protection
    {
      float lowTemp = 255.0f;
      uint8_t availableSensors = 0;

      if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
        auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY, settings.heating.indoorTempAvgType);
        if (value < lowTemp) {
          lowTemp = value;
        }

        availableSensors++;
      }

      if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::HEATING_TEMP)) {
        auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_TEMP, Sensors::ValueType::PRIMARY);
        if (value < lowTemp) {
          lowTemp = value;
        }

        availableSensors++;
      }

      if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP)) {
        auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP, Sensors::ValueType::PRIMARY);
        if (value < lowTemp) {
          lowTemp = value;
        }

        availableSensors++;
      }

      if (availableSensors) {
        if (vars.master.heating.freezing) {
          if (lowTemp - static_cast<float>(settings.heating.freezeProtection.highTemp) + 0.0001f >= 0.0f) {
            vars.master.heating.freezing = false;

            Log.sinfoln(
              FPSTR(L_MAIN),
              F("No freezing detected. Current low temp: %.2f, threshold (high): %hhu"),
              lowTemp, settings.heating.freezeProtection.highTemp
            );
          }

        } else {
          if (static_cast<float>(settings.heating.freezeProtection.lowTemp) - lowTemp + 0.0001f >= 0.0f) {
            vars.master.heating.freezing = true;

            if (!settings.heating.enabled) {
              settings.heating.enabled = true;
              fsSettings.update();
            }

            Log.sinfoln(
              FPSTR(L_MAIN),
              F("Freezing detected! Current low temp: %.2f, threshold (low): %hhu"),
              lowTemp, settings.heating.freezeProtection.lowTemp
            );
          }
        }

      } else if (vars.master.heating.freezing) {
        vars.master.heating.freezing = false;

        Log.sinfoln(FPSTR(L_MAIN), F("No sensors available, freeze protection unavailable!"));
      }
    }
  }

  void emergency() {
    // flags
    uint8_t emergencyFlags = 0b00000000;

    // set outdoor sensor flag
    if (settings.equitherm.enabled) {
      if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP)) {
        emergencyFlags |= 0b00000001;
      }
    }

    // set indoor sensor flags
    if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
      if (!settings.equitherm.enabled && settings.pid.enabled) {
        emergencyFlags |= 0b00000010;
      }

      if (settings.opentherm.options.nativeOTC) {
        emergencyFlags |= 0b00000100;
      }
    }

    // if any flags is true
    if ((emergencyFlags & 0b00001111) != 0) {
      if (!this->emergencyDetected) {
        // flip flag
        this->emergencyDetected = true;
        this->emergencyFlipTime = millis();

      } else if (this->emergencyDetected && !vars.emergency.state) {
        // enable emergency
        if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
          vars.emergency.state = true;
          Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled (%hhu)"), emergencyFlags);
        }
      }

    } else {
      if (this->emergencyDetected) {
        // flip flag
        this->emergencyDetected = false;
        this->emergencyFlipTime = millis();

      } else if (!this->emergencyDetected && vars.emergency.state) {
        // disable emergency
        if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
          vars.emergency.state = false;
          Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode disabled"));
        }
      }
    }
  }

  void ledStatus() {
    uint8_t errors[4];
    uint8_t errCount = 0;
    static uint8_t errPos = 0;
    static unsigned long endBlinkTime = 0;
    static bool ledOn = false;
    static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;

    if (settings.system.statusLedGpio != configuredGpio) {
      if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
        digitalWrite(configuredGpio, LOW);
      }

      if (GPIO_IS_VALID(settings.system.statusLedGpio)) {
        configuredGpio = settings.system.statusLedGpio;
        pinMode(configuredGpio, OUTPUT);
        digitalWrite(configuredGpio, LOW);
        this->blinker->init(configuredGpio);

      } else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
        configuredGpio = GPIO_IS_NOT_CONFIGURED;
      }
    }

    if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
      return;
    }

    if (!network->isConnected()) {
      errors[errCount++] = 2;
    }

    if (!vars.slave.connected) {
      errors[errCount++] = 3;
    }

    if (vars.slave.fault.active) {
      errors[errCount++] = 4;
    }

    if (vars.emergency.state) {
      errors[errCount++] = 5;
    }


    if (this->blinker->ready()) {
      endBlinkTime = millis();
    }

    if (!this->blinker->running() && millis() - endBlinkTime >= 5000) {
      if (errCount == 0) {
        if (!ledOn) {
          digitalWrite(configuredGpio, HIGH);
          ledOn = true;
        }

        return;

      } else if (ledOn) {
        digitalWrite(configuredGpio, LOW);
        ledOn = false;
        endBlinkTime = millis();
        return;
      }

      if (errPos >= errCount) {
        errPos = 0;

        // end of error list
        this->blinker->blink(10, 50, 50);

      } else {
        this->blinker->blink(errors[errPos++], 300, 300);
      }
    }

    this->blinker->tick();
  }

  void cascadeControl() {
    static uint8_t configuredInputGpio = GPIO_IS_NOT_CONFIGURED;
    static uint8_t configuredOutputGpio = GPIO_IS_NOT_CONFIGURED;
    static bool inputTempValue = false;
    static unsigned long inputChangedTs = 0;
    static bool outputTempValue = false;
    static unsigned long outputChangedTs = 0;

    // input
    if (settings.cascadeControl.input.enabled) {
      if (settings.cascadeControl.input.gpio != configuredInputGpio) {
        if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
          pinMode(configuredInputGpio, OUTPUT);
          digitalWrite(configuredInputGpio, LOW);

          Log.sinfoln(FPSTR(L_CASCADE_INPUT), F("Deinitialized on GPIO %hhu"), configuredInputGpio);
        }

        if (GPIO_IS_VALID(settings.cascadeControl.input.gpio)) {
          configuredInputGpio = settings.cascadeControl.input.gpio;
          pinMode(configuredInputGpio, INPUT);

          Log.sinfoln(FPSTR(L_CASCADE_INPUT), F("Initialized on GPIO %hhu"), configuredInputGpio);

        } else if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
          configuredInputGpio = GPIO_IS_NOT_CONFIGURED;

          Log.swarningln(FPSTR(L_CASCADE_INPUT), F("Failed initialize: GPIO %hhu is not valid!"), configuredInputGpio);
        }
      }

      if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
        bool value;
        if (digitalRead(configuredInputGpio) == HIGH) {
          value = true ^ settings.cascadeControl.input.invertState;
        } else {
          value = false ^ settings.cascadeControl.input.invertState;
        }

        if (value != vars.cascadeControl.input) {
          if (value != inputTempValue) {
            inputTempValue = value;
            inputChangedTs = millis();

          } else if (millis() - inputChangedTs >= settings.cascadeControl.input.thresholdTime * 1000u) {
            vars.cascadeControl.input = value;

            Log.sinfoln(
              FPSTR(L_CASCADE_INPUT),
              F("State changed to %s"),
              value ? F("TRUE") : F("FALSE")
            );
          }

        } else if (value != inputTempValue) {
          inputTempValue = value;
        }
      }
    }

    if (!settings.cascadeControl.input.enabled || configuredInputGpio == GPIO_IS_NOT_CONFIGURED) {
      if (!vars.cascadeControl.input) {
        vars.cascadeControl.input = true;

        Log.sinfoln(
          FPSTR(L_CASCADE_INPUT),
          F("Disabled, state changed to %s"),
          vars.cascadeControl.input ? F("TRUE") : F("FALSE")
        );
      }
    }


    // output
    if (settings.cascadeControl.output.enabled) {
      if (settings.cascadeControl.output.gpio != configuredOutputGpio) {
        if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
          pinMode(configuredOutputGpio, OUTPUT);
          digitalWrite(configuredOutputGpio, LOW);

          Log.sinfoln(FPSTR(L_CASCADE_OUTPUT), F("Deinitialized on GPIO %hhu"), configuredOutputGpio);
        }

        if (GPIO_IS_VALID(settings.cascadeControl.output.gpio)) {
          configuredOutputGpio = settings.cascadeControl.output.gpio;
          pinMode(configuredOutputGpio, OUTPUT);
          digitalWrite(
            configuredOutputGpio,
            settings.cascadeControl.output.invertState
              ? HIGH
              : LOW
          );

          Log.sinfoln(FPSTR(L_CASCADE_OUTPUT), F("Initialized on GPIO %hhu"), configuredOutputGpio);

        } else if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
          configuredOutputGpio = GPIO_IS_NOT_CONFIGURED;

          Log.swarningln(FPSTR(L_CASCADE_OUTPUT), F("Failed initialize: GPIO %hhu is not valid!"), configuredOutputGpio);
        }
      }

      if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
        bool value = false;
        if (settings.cascadeControl.output.onFault && vars.slave.fault.active) {
          value = true;

        } else if (settings.cascadeControl.output.onLossConnection && !vars.slave.connected) {
          value = true;

        } else if (settings.cascadeControl.output.onEnabledHeating && settings.heating.enabled && vars.cascadeControl.input) {
          value = true;
        }

        if (value != vars.cascadeControl.output) {
          if (value != outputTempValue) {
            outputTempValue = value;
            outputChangedTs = millis();

          } else if (millis() - outputChangedTs >= settings.cascadeControl.output.thresholdTime * 1000u) {
            vars.cascadeControl.output = value;

            digitalWrite(
              configuredOutputGpio,
              vars.cascadeControl.output ^ settings.cascadeControl.output.invertState
                ? HIGH
                : LOW
            );

            Log.sinfoln(
              FPSTR(L_CASCADE_OUTPUT),
              F("State changed to %s"),
              value ? F("TRUE") : F("FALSE")
            );
          }

        } else if (value != outputTempValue) {
          outputTempValue = value;
        }
      }
    }

    if (!settings.cascadeControl.output.enabled || configuredOutputGpio == GPIO_IS_NOT_CONFIGURED) {
      if (vars.cascadeControl.output) {
        vars.cascadeControl.output = false;

        if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
          digitalWrite(
            configuredOutputGpio,
            vars.cascadeControl.output ^ settings.cascadeControl.output.invertState
              ? HIGH
              : LOW
          );
        }

        Log.sinfoln(
          FPSTR(L_CASCADE_OUTPUT),
          F("Disabled, state changed to %s"),
          vars.cascadeControl.output ? F("TRUE") : F("FALSE")
        );
      }
    }
  }

  void externalPump() {
    static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;

    if (settings.externalPump.gpio != configuredGpio) {
      if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
        digitalWrite(configuredGpio, LOW);
      }

      if (GPIO_IS_VALID(settings.externalPump.gpio)) {
        configuredGpio = settings.externalPump.gpio;
        pinMode(configuredGpio, OUTPUT);
        digitalWrite(
          configuredGpio,
          settings.externalPump.invertState
            ? HIGH
            : LOW
        );

      } else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
        configuredGpio = GPIO_IS_NOT_CONFIGURED;
      }
    }

    if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
      if (vars.externalPump.state) {
        vars.externalPump.state = false;
        vars.externalPump.lastEnabledTime = millis();

        Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: use = off"));
      }

      return;
    }

    if (!vars.master.heating.enabled && this->heatingEnabled) {
      this->heatingEnabled = false;
      this->heatingDisabledTime = millis();

    } else if (vars.master.heating.enabled && !this->heatingEnabled) {
      this->heatingEnabled = true;
    }

    if (!settings.externalPump.use) {
      if (vars.externalPump.state) {
        digitalWrite(
          configuredGpio,
          settings.externalPump.invertState
            ? HIGH
            : LOW
        );

        vars.externalPump.state = false;
        vars.externalPump.lastEnabledTime = millis();

        Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: use = off"));
      }

      return;
    }

    if (vars.externalPump.state && !this->heatingEnabled) {
      if (this->extPumpStartReason == MainTask::PumpStartReason::HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) {
        digitalWrite(
          configuredGpio,
          settings.externalPump.invertState
            ? HIGH
            : LOW
        );

        vars.externalPump.state = false;
        vars.externalPump.lastEnabledTime = millis();

        Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: expired post circulation time"));

      } else if (this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK && millis() - this->externalPumpStartTime >= (settings.externalPump.antiStuckTime * 1000u)) {
        digitalWrite(
          configuredGpio,
          settings.externalPump.invertState
            ? HIGH
            : LOW
        );

        vars.externalPump.state = false;
        vars.externalPump.lastEnabledTime = millis();

        Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: expired anti stuck time"));
      }

    } else if (vars.externalPump.state && this->heatingEnabled && this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK) {
      this->extPumpStartReason = MainTask::PumpStartReason::HEATING;

    } else if (!vars.externalPump.state && this->heatingEnabled) {
      vars.externalPump.state = true;
      this->externalPumpStartTime = millis();
      this->extPumpStartReason = MainTask::PumpStartReason::HEATING;

      digitalWrite(
        configuredGpio,
        settings.externalPump.invertState
          ? LOW
          : HIGH
      );

      Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: heating on"));

    } else if (!vars.externalPump.state && (vars.externalPump.lastEnabledTime == 0 || millis() - vars.externalPump.lastEnabledTime >= (settings.externalPump.antiStuckInterval * 1000lu))) {
      vars.externalPump.state = true;
      this->externalPumpStartTime = millis();
      this->extPumpStartReason = MainTask::PumpStartReason::ANTISTUCK;

      digitalWrite(
        configuredGpio,
        settings.externalPump.invertState
          ? LOW
          : HIGH
      );

      Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: anti stuck"));
    }
  }
};
