#pragma once
/*
 * 11.07.2019
 * Класс, который отслеживает действия пользователя по запросу обновления прошивки по воздуху и выполняет эту прошивку.
 * Запрос на обновление - это вызов метода RequestOtaUpdate(), его нужно поместить, например, в обработчик нажатия кнопки, приёма UDP пакета и т.д.
 * Для обновления пользователь должен ДВАЖДЫ запросить обновление в течение заданного промежутка времени (CONFIRMATION_TIMEOUT) во избежание случайного перехода в режим обновления.
 * Режим обновления - это прослушивание специального порта (ESP_OTA_PORT) в ожидании команды обновления прошивки по воздуху (по сети).
 * Режим обновления работает параллельно с основным режимом функционирования, только при ESP_MODE == 1 (WiFi клиент), т.к. требует доступа к ESP по локальной сети и при подключенной кнопке (в данном сетапе, т.к. он вызывается кнопкой).
 * Режим обновления активен в течение заданного промежутка времени (ESP_CONF_TIMEOUT). Потом ESP автоматически перезагружается.
 * Обновление производится из Arduino IDE: меню Инструменты - Порт - <Выбрать обнаруженный СЕТЕВОЙ COM порт из списка> (если он не обнаружен, значит что-то настроено неправильно), затем обычная команда "Загрузка" для прошивки.
 * Для включения опции обновления по воздуху в основном файле должен быть определён идентификатор OTA "#define OTA" и режим "#define ESP_MODE (1U)" (а также в данном проекте должна быть подключена кнопка).
*/

#ifdef OTA

#include <ArduinoOTA.h>
#include <ESP8266mDNS.h>

#define CONFIRMATION_TIMEOUT  (30U)                         // время в сеундах, в течение которого нужно дважды подтвердить старт обновлениЯ по воздуху (иначе сброс в None)

enum OtaPhase                                               // определение стадий процесса обновления по воздуху: нет, получено первое подтверждение, получено второе подтверждение, получено второе подтверждение - в процессе, обновление окончено
{
  None = 0,
  GotFirstConfirm,
  GotSecondConfirm,
  InProgress,
  Done
};


class OtaManager
{
  public:
    static OtaPhase OtaFlag;

    bool RequestOtaUpdate()                                 // пользователь однократно запросил обновление по воздуху; возвращает true, когда переходит в режим обновления - startOtaUpdate()
    {
      if (ESP_MODE != 1)
      {
        #ifdef GENERAL_DEBUG
        Serial.print(F("Запрос обновления по воздуху поддерживается только в режиме ESP_MODE = 1\n"));
        #endif

        return false;
      }

      if (OtaFlag == OtaPhase::None)
      {
        OtaFlag = OtaPhase::GotFirstConfirm;
        momentOfFirstConfirmation = millis();

        #ifdef GENERAL_DEBUG
        Serial.print(F("Получено первое подтверждение обновления по воздуху\nОжидание второго подтверждения\n"));
        #endif

        return false;
      }

      if (OtaFlag == OtaPhase::GotFirstConfirm)
      {
        OtaFlag = OtaPhase::GotSecondConfirm;

        #ifdef GENERAL_DEBUG
        Serial.print(F("Получено второе подтверждение обновления по воздуху\nСтарт режима обновления\n"));
        #endif

        startOtaUpdate();
        return true;
      }

      return false;
    }

    void HandleOtaUpdate()
    {
      if (OtaFlag == OtaPhase::GotFirstConfirm &&
          millis() - momentOfFirstConfirmation >= CONFIRMATION_TIMEOUT * 1000)
      {
        OtaFlag = OtaPhase::None;
        momentOfFirstConfirmation = 0;

        #ifdef GENERAL_DEBUG
        Serial.print(F("Таймаут ожидания второго подтверждения превышен\nСброс флага в исходное состояние\n"));
        #endif

        return;
      }

      if (OtaFlag == OtaPhase::GotSecondConfirm &&
          millis() - momentOfOtaStart >= ESP_CONF_TIMEOUT * 1000)
      {
        OtaFlag = OtaPhase::None;
        momentOfOtaStart = 0;

        #ifdef GENERAL_DEBUG
        Serial.print(F("Таймаут ожидания прошивки по воздуху превышен\nСброс флага в исходное состояние\nПерезагрузка\n"));
        delay(500);
        #endif

        #if defined(ESP8266)
        ESP.reset();
        #else
        ESP.restart();
        #endif

        return;
      }

      if (OtaFlag == OtaPhase::InProgress)
      {
        ArduinoOTA.handle();
      }
    }

  private:
    uint64_t momentOfFirstConfirmation = 0;                 // момент времени, когда получено первое подтверждение и с которого начинается отсчёт ожидания второго подтверждения
    uint64_t momentOfOtaStart = 0;                          // момент времени, когда развёрнута WiFi точка доступа для обновления по воздуху

    void startOtaUpdate()
    {
      char espHostName[65];
      sprintf(espHostName, "%s-%u", AP_NAME, ESP.getChipId());
      ArduinoOTA.setPort(ESP_OTA_PORT);
      ArduinoOTA.setHostname(espHostName);
      ArduinoOTA.setPassword(AP_PASS);

      ArduinoOTA.onStart([this]()
      {
        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()

        #ifdef GENERAL_DEBUG
        Serial.printf_P(PSTR("Start updating %s\n"), type.c_str());
        #endif
      });

      ArduinoOTA.onEnd([this]()
      {
        OtaFlag = OtaPhase::Done;

        #ifdef GENERAL_DEBUG
        Serial.print(F("Обновление по воздуху выполнено\nПерезапуск"));
        delay(500);
        #endif
      });

      ArduinoOTA.onProgress([](unsigned int progress, unsigned int total)
      {
        #ifdef GENERAL_DEBUG
        Serial.printf_P(PSTR("Ход выполнения: %u%%\r"), (progress / (total / 100)));
        #endif
      });

      ArduinoOTA.onError([this](ota_error_t error)
      {
        OtaFlag = OtaPhase::None;

        #ifdef GENERAL_DEBUG
        Serial.printf_P(PSTR("Обновление по воздуху завершилось ошибкой [%u]: "), error);
        #endif

        if (error == OTA_AUTH_ERROR)
        {
          #ifdef GENERAL_DEBUG
          Serial.println(F("Auth Failed"));
          #endif
        }
        else if (error == OTA_BEGIN_ERROR)
        {
          #ifdef GENERAL_DEBUG
          Serial.println(F("Begin Failed"));
          #endif
        }
        else if (error == OTA_CONNECT_ERROR)
        {
          #ifdef GENERAL_DEBUG
          Serial.println(F("Connect Failed"));
          #endif
        }
        else if (error == OTA_RECEIVE_ERROR)
        {
          #ifdef GENERAL_DEBUG
          Serial.println(F("Receive Failed"));
          #endif
        }
        else if (error == OTA_END_ERROR)
        {
          #ifdef GENERAL_DEBUG
          Serial.println(F("End Failed"));
          #endif
        }

        #ifdef GENERAL_DEBUG
        Serial.print(F("Сброс флага в исходное состояние\nПереход в режим ожидания запроса прошивки по воздуху\n"));
        #endif
      });

      ArduinoOTA.setRebootOnSuccess(true);
      ArduinoOTA.begin();
      OtaFlag = OtaPhase::InProgress;
      momentOfFirstConfirmation = 0;
      momentOfOtaStart = 0;

      #ifdef GENERAL_DEBUG
      Serial.printf_P(PSTR("Для обновления в Arduino IDE выберите пункт меню Инструменты - Порт - '%s at "), espHostName);
      Serial.print(WiFi.localIP());
      Serial.println(F("'"));
      Serial.printf_P(PSTR("Затем нажмите кнопку 'Загрузка' в течение %u секунд и по запросу введите пароль '%s'\n"), ESP_CONF_TIMEOUT, AP_PASS);
      Serial.println(F("Устройство с Arduino IDE должно быть в одной локальной сети с модулем ESP!"));
      #endif
    }
};

#endif
