#include <Arduino.h>
#include "appconfig.h"
#include "esp32cam.h"
#include "logging.h"
#include "sdcardesp32cam.h"
#include "webgallery.h"

CameraApp cameraApp;
StorageSD storage;
WebGallery webGallery;
TaskHandle_t webServerTaskHandle = nullptr;
TaskHandle_t photoTaskHandle = nullptr;

// Stan debounce jest używany wyłącznie wewnątrz ISR.
volatile TickType_t lastButtonInterruptTick = 0;
const TickType_t BUTTON_DEBOUNCE_TICKS = pdMS_TO_TICKS(300);

[[noreturn]] void restartAfterSetupFailure(const char *message)
{
  LOG_PRINTLN(message);
  cameraApp.blinkFlash(3);
  LOG_PRINTF("Restarting in %lu ms\n",
             static_cast<unsigned long>(AppConfig::RESTART_DELAY_MS));
  delay(AppConfig::RESTART_DELAY_MS);
  ESP.restart();

  // Zabezpieczenie na wypadek, gdyby restart sprzętowy nie zadziałał.
  while (true)
  {
    delay(1000);
  }
}

template <typename InitializeFunction>
bool initializeWithRetry(const char *componentName,
                         InitializeFunction initialize)
{
  for (uint8_t attempt = 1;
       attempt <= AppConfig::STARTUP_RETRY_COUNT;
       ++attempt)
  {
    LOG_PRINTF("Initializing %s (attempt %u/%u)\n",
               componentName,
               static_cast<unsigned>(attempt),
               static_cast<unsigned>(AppConfig::STARTUP_RETRY_COUNT));

    if (initialize())
    {
      return true;
    }

    if (attempt < AppConfig::STARTUP_RETRY_COUNT)
    {
      delay(AppConfig::STARTUP_RETRY_DELAY_MS);
    }
  }

  return false;
}

void IRAM_ATTR onButtonInterrupt()
{
  TickType_t now = xTaskGetTickCountFromISR();

  if ((now - lastButtonInterruptTick) > BUTTON_DEBOUNCE_TICKS)
  {
    lastButtonInterruptTick = now;

    if (photoTaskHandle != nullptr)
    {
      BaseType_t higherPriorityTaskWoken = pdFALSE;
      vTaskNotifyGiveFromISR(photoTaskHandle, &higherPriorityTaskWoken);
      if (higherPriorityTaskWoken == pdTRUE)
      {
        portYIELD_FROM_ISR();
      }
    }
  }
}

void takeAndSavePhoto()
{
  LOG_PRINTLN("Taking photo...");

  camera_fb_t *frame = cameraApp.captureWithFlash();

  if (frame == nullptr)
  {
    cameraApp.blinkFlash(3);
    return;
  }

  LOG_PRINTF("JPEG size: %u bytes\n", static_cast<unsigned>(frame->len));

  char filename[32];
  if (storage.saveNextJpeg(frame, filename, sizeof(filename)))
  {
    webGallery.notifyPhotoAdded(filename);
  }
  else
  {
    LOG_PRINTLN("Photo was not saved");
    cameraApp.blinkFlash(3);
  }

  cameraApp.release(frame);
}

void webServerTask(void *parameter)
{
  (void)parameter;
  webGallery.run();
  webServerTaskHandle = nullptr;
  vTaskDelete(nullptr);
}

void setup()
{
  delay(3000);

  LOG_BEGIN(115200);
  LOG_PRINTLN("ESP32-CAM start");

  if (!initializeWithRetry("button and flash", []() {
        return cameraApp.configButtonAndFlash() == 0;
      }))
  {
    restartAfterSetupFailure("Button and flash initialization failed");
  }

  if (!initializeWithRetry("camera", []() {
        return cameraApp.begin();
      }))
  {
    restartAfterSetupFailure("Camera initialization failed");
  }

  if (!initializeWithRetry("SD card", []() {
        return storage.begin();
      }))
  {
    restartAfterSetupFailure("SD card initialization failed");
  }

  photoTaskHandle = xTaskGetCurrentTaskHandle();
  lastButtonInterruptTick = xTaskGetTickCount();
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonInterrupt, FALLING);

  webGallery.begin(storage);
  if (!initializeWithRetry("web server task", []() {
        const BaseType_t taskResult = xTaskCreatePinnedToCore(
            webServerTask,
            "photo-web-server",
            AppConfig::WEB_TASK_STACK_BYTES,
            nullptr,
            AppConfig::WEB_TASK_PRIORITY,
            &webServerTaskHandle,
            AppConfig::WEB_TASK_CORE);
        return taskResult == pdPASS;
      }))
  {
    webServerTaskHandle = nullptr;
    restartAfterSetupFailure("Failed to create the web server task");
  }

  LOG_PRINTLN("Ready");
  cameraApp.blinkFlash(2);
}
void loop()
{
  // pdFALSE zmniejsza licznik o jeden, więc kilka poprawnych naciśnięć nie jest
  // łączonych w jedno i zostanie obsłużonych kolejno po zakończeniu zapisu.
  if (ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(50)) > 0)
  {
    LOG_PRINTLN("Button interrupt detected");
    takeAndSavePhoto();
    LOG_PRINTF(
        "Main task minimum free stack: %u bytes\n",
        static_cast<unsigned>(uxTaskGetStackHighWaterMark(nullptr)));
  }
}
