#pragma once

#include "effects/Circular.h"
#include "effects/Common.h"
#include "drivers/Esp32DriverRGBW.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

#include <functional>
#include <cstring>

static constexpr int MAX_EFFECT_CONFIG_SIZE = 128;
static constexpr int MAX_EFFECT_CLASS_SIZE = 4 * 1024;

template <typename TLedStrip>
class LedTask
{
public:
  void begin(TLedStrip &strip, Esp32DriverRGBW &driver);

  template <typename TEffectConfig>
  void startEffect(const TEffectConfig &cfg);

private:
  template <typename T>
  friend void _led_task_func(void *);

  QueueHandle_t queue_ = nullptr;
  TLedStrip *ledStrip_ = nullptr;
  Esp32DriverRGBW *driver_ = nullptr;
};

// -----------------------------------------------------------------------------------------------

template <EffectId staticEffectId, typename TLedStrip>
bool dispatchEffectId(EffectId dynamicEffectId, std::function<int()> &effectFunction, TLedStrip &ledStrip,
                      unsigned char *msgBuffer, unsigned char *effectStorage)
{
  if (staticEffectId == dynamicEffectId)
  {
    typename EffectIdToConfig<staticEffectId>::type cfg;
    memcpy(&cfg, msgBuffer + sizeof(EffectId), sizeof(decltype(cfg)));
    using TEffect = typename EffectIdToClass<staticEffectId, TLedStrip>::type;
    static_assert(sizeof(TEffect) < MAX_EFFECT_CLASS_SIZE, "Effect to big for effectStorage, increase MAX_EFFECT_CLASS_SIZE");
    TEffect *effect = new (effectStorage) TEffect(cfg, ledStrip);
    effectFunction = [effect]()
    { return (*effect)(); };
    return true;
  }
  else
    return false;
}

template <typename TLedStrip>
void _led_task_func(void *params)
{
  LedTask<TLedStrip> *task = reinterpret_cast<LedTask<TLedStrip> *>(params);
  unsigned char msgBuffer[MAX_EFFECT_CONFIG_SIZE];
  unsigned char effectStorage[MAX_EFFECT_CLASS_SIZE];

  std::function<int()> effectFunction = []() -> int
  { return 100000; };

  int timeoutMsForEffect = 100000; // huge timeout since there is no effect in the beginning

  while (true)
  {
    if (xQueueReceive(task->queue_, msgBuffer, (TickType_t)(timeoutMsForEffect / portTICK_PERIOD_MS)) == pdTRUE)
    {
      // Read and parse effect from queue
      EffectId id;
      memcpy(&id, msgBuffer, sizeof(EffectId));

      TLedStrip &ledStrip = *(task->ledStrip_);
      // clang-format off
      if      (dispatchEffectId<EffectId::CIRCULAR                      >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
      else if (dispatchEffectId<EffectId::STATIC                        >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
      else if (dispatchEffectId<EffectId::ALEXA_SWIPE                   >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
      else if (dispatchEffectId<EffectId::RANDOM_TWO_COLOR_INTERPOLATION>(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
      else if (dispatchEffectId<EffectId::SWIPE_AND_CHANGE              >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
      else if (dispatchEffectId<EffectId::REVERSE_SWIPE                 >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
      // clang-format on

      timeoutMsForEffect = 0;
    }
    else
    {
      timeoutMsForEffect = effectFunction();
      task->driver_->writeSync(task->ledStrip_->rawData(), task->ledStrip_->numLeds());
    }
  }
}

template <typename TLedStrip>
void LedTask<TLedStrip>::begin(TLedStrip &strip, Esp32DriverRGBW &driver)
{
  queue_ = xQueueCreate(4, MAX_EFFECT_CONFIG_SIZE);
  if (!queue_)
    Serial.println("Failed to create LED effect queue");

  ledStrip_ = &strip;
  driver_ = &driver;

  xTaskCreate(_led_task_func<TLedStrip>, "led task", MAX_EFFECT_CLASS_SIZE + MAX_EFFECT_CONFIG_SIZE + 2048 * 2,
              (void *)(this), 1, nullptr);
}

template <typename TLedStrip>
template <typename TEffectConfig>
void LedTask<TLedStrip>::startEffect(const TEffectConfig &cfg)
{
  static constexpr auto msgSize = sizeof(TEffectConfig) + sizeof(EffectId);
  static_assert(msgSize < MAX_EFFECT_CONFIG_SIZE,
                "Effect config too large, increase MAX_EFFECT_CONFIG_SIZE");

  unsigned char buffer[MAX_EFFECT_CONFIG_SIZE];
  if (queue_ == nullptr)
  {
    Serial.println("Trying to start effect before queue was set up!");
    return;
  }
  EffectId id = EffectConfigToId<TEffectConfig>::id;
  memcpy(buffer, &id, sizeof(EffectId));
  memcpy(buffer + sizeof(EffectId), &cfg, sizeof(TEffectConfig));
  xQueueSend(queue_, (void *)buffer, (TickType_t)10);
}
