/**
 * @file PowerScheduler.hpp
 * @author Phillip Kühne
 * @brief The actual power scheduler class, which keeps track of the power
 * budget and allocates power to different components.
 * @version 0.1
 * @date 2024-12-21
 *
 * @copyright (c) 2024
 *
 */

#include "PowerParameters.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <Arduino.h>
#include <vector>

#ifndef PowerScheduler_h
#define PowerScheduler_h

#define TAG "PowerScheduler"

class PowerScheduler {
private:
  static constexpr uint16_t DEFAULT_SLACK_TIME_MS = 100;
  static constexpr uint16_t MUTEX_TIMEOUT_MS = 10;
  SemaphoreHandle_t powerSchedulerMutex;
  PowerScheduler(float i_limit_ma, float i_max_ma);

  // RAII for mutex
  class PowerSchedulerMutex {
  private:
    SemaphoreHandle_t &mutex;
    bool locked;

  public:
    PowerSchedulerMutex(SemaphoreHandle_t &mutex) : mutex(mutex), locked(false) {
      locked =
          (xSemaphoreTake(mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) == pdTRUE);
      if (!locked) {
        ESP_LOGW(TAG, "Could not take powerScheduler mutex");
      }
    }

    void unlock() {
      if (locked) {
        xSemaphoreGive(mutex);
        locked = false;
      }
    }

    void lock() {
      if (!locked) {
        locked = (xSemaphoreTake(mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) ==
                  pdTRUE);
        if (!locked) {
          ESP_LOGW(TAG, "Could not take power mutex");
        }
      }
    }

    ~PowerSchedulerMutex() {
      if (locked) {
        xSemaphoreGive(mutex);
      }
    }
    bool isLocked() { return locked; }
  };

public:
  ~PowerScheduler();
  /// @brief Initialize the singleton instance of the power manager
  /// @return reference to the power manager
  static PowerScheduler &getPowerScheduler(float i_limit_ma = 0,
                                           float i_max_ma = 0);
  /// @brief Get the current free current budget (to C1 discharge)
  /// @return the amount of power that is currently available (in mA)
  float getFreeLimitCurrentBudget(void);
  /// @brief Get the current hard maximum free current (to C2 discharge)
  /// @return the maximum amount of power that can be allocated (in mA)
  float getFreeMaximumCurrentBudget(void);

  /// @brief Request an allowance of a certain number of milliamperes from the
  /// power scheduler without waiting for it (meaning it will not be scheduled
  /// for future allocation). Only one can be active per consumer.
  /// @param neededCurrent the amount of current we want to be accounted for (in
  /// mA)
  /// @return whether the current could be successfully allocated
  /// @note This takes existing power consumption by the same consumer into
  /// account, so requesting the same or less power will always succeed. Also,
  /// small amounts of power (below 1 mA) will always be granted.
  bool tryAccquireCurrentAllowance(PowerParameters::PowerConsumers consumer,
                                   float neededcurrent,
                                   uint16_t requestedDurationMs = 0);
  /// @brief "Return" the current currently allocated to a consumer
  /// @param consumer the active consumer to release the current for
  /// @note This will delete all power requests for the given consumer
  void releaseCurrent(PowerParameters::PowerConsumers consumer);

  /// @brief Wait for a certain amount of current to be available. This will
  /// "reseve a spot in the queue". Only one can be active per consumer.
  /// @param neededCurrent the amount of power we want to be accounted for (in
  /// mW)
  /// @param TicksToWait the amount of time to wait for the power to become
  /// available
  /// @return whether the power could be successfully allocatedy
  bool waitForCurrentAllowance(PowerParameters::PowerConsumers consumer,
                               float neededCurrent,
                               uint16_t maxSlackTimeMs = DEFAULT_SLACK_TIME_MS,
                               uint16_t requestedDurationMs = 0);
  /// @brief Put the ESP32 into deep sleep mode, without a method to wake up
  /// again. Basically this is a shutdown.
  void beginPermanentDeepSleep(void);

  //// @brief Get currently granted power
  /// @return the amount of power that is currently allocated (in mA)
  float getCurrentCurrent(void);

  /// @brief Power consumer data structure
  struct CurrentAllowance {
    PowerParameters::PowerConsumers consumer;
    uint16_t maxSlackTimeMs;
    uint16_t requestedDurationMs;
    TaskHandle_t taskHandle;
    float neededCurrent;
    TickType_t requestedAt;
    TickType_t grantedAt;
    bool granted;
  };

  // @brief waiting task wakeup reasons
  enum PowerWakeupReasons {
    POWER_AVAILABLE = 1,
    POWER_EXPIRED = 2,
  };
  // @brief Responsible for recalculating the current budgets
  void recalculateCurrentBudgets(void);

  // @brief Get current consumption of a consumer
  float getConsumerCurrent(PowerParameters::PowerConsumers consumer);

  // @brief Retrieve the current allowance for a given consumer
  CurrentAllowance *
  getCurrentAllowance(PowerParameters::PowerConsumers consumer);
  // @brief Retrieve the current allowance for a given task
  CurrentAllowance *getCurrentAllowance(TaskHandle_t taskHandle);

  // @brief Retrieve the allowance that will expire next
  CurrentAllowance *getNextExpiringAllowance(void);

protected:
  // Current above which there will be no new scheduling
  uint16_t limitCurrent;
  // Absolute maximum current that can be allocated
  uint16_t maximumCurrent;

  // Current budget that is currently available to limitCurrent
  int16_t freeLimitCurrentBudget;
  // Current budget that is currently available to maximumCurrent
  int16_t freeMaximumCurrentBudget;

  // @brief Responsible for selecting the next task to be granted power
  void checkWaitingTasks(void);

  // @brief Mutex to protect the power scheduler from concurrent access

  std::vector<PowerScheduler::CurrentAllowance> currentAllowances;
};

static PowerScheduler *powerSchedulerInstance;

#endif // PowerScheduler_h