/**
 * @file PowerScheduler.cpp
 * @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 "PowerScheduler.h"
#include "PowerManager.h"

bool PowerScheduler::tryAccquireCurrentAllowance(
    PowerParameters::PowerConsumers consumer, float neededCurrent,
    uint16_t requestedDurationMs) {
  float existingConsumption = getConsumerCurrent(consumer);
  const bool currentLimitNotExceeded =
      this->freeLimitCurrentBudget + existingConsumption > 0;
  const bool neededCurrentAvailableBelowMaximum =
      this->freeMaximumCurrentBudget + existingConsumption >= neededCurrent;
  const bool currentIsInsignificant =
      neededCurrent < PowerParameters::CURRENT_INSIGNIFICANT;
  if (currentIsInsignificant ||
      (currentLimitNotExceeded && neededCurrentAvailableBelowMaximum)) {
    if (existingConsumption > 0) {
      releaseCurrent(consumer);
    }
    PowerSchedulerMutex lock(powerSchedulerMutex);
    if (lock.isLocked() == false) {
      ESP_LOGE(
          TAG,
          "Failed to Acquire PowerScheduler Mutex during Current Allocation");
      return false;
    }
    this->currentAllowances.push_back(PowerScheduler::CurrentAllowance{
        .consumer = consumer,
        .maxSlackTimeMs = 0,
        .requestedDurationMs = requestedDurationMs,
        .taskHandle = xTaskGetCurrentTaskHandle(),
        .neededCurrent = neededCurrent,
        .requestedAt = xTaskGetTickCount(),
        .grantedAt = xTaskGetTickCount(),
        .granted = true});
    this->recalculateCurrentBudgets();
    lock.unlock();
    return true;
  } else {
    ESP_LOGI(TAG,
             "Task %p denied %f mA of power;"
             "currently allocated: %f mA;"
             "total available (2C): %f mA",
             xTaskGetCurrentTaskHandle(), neededCurrent, getCurrentCurrent(),
             maximumCurrent);
    return false;
  }
}

void PowerScheduler::releaseCurrent(PowerParameters::PowerConsumers consumer) {
  PowerSchedulerMutex lock(powerSchedulerMutex);
  if (lock.isLocked() == false) {
    return;
  }
  for (auto it = currentAllowances.begin(); it != currentAllowances.end();
       ++it) {
    if (it->consumer == consumer) {
      currentAllowances.erase(it);
      break;
    }
  }
  lock.unlock();
  recalculateCurrentBudgets();
  // Check if there are tasks waiting for power
  checkWaitingTasks();
}

bool PowerScheduler::waitForCurrentAllowance(
    PowerParameters::PowerConsumers consumer, float neededCurrent,
    uint16_t maxSlackTimeMs, uint16_t requestedDurationMs) {
  if (tryAccquireCurrentAllowance(consumer, neededCurrent,
                                  requestedDurationMs)) {
    return true;
  } else {

    // Suspend the task while waiting for power to be available
    TaskHandle_t currentTask = xTaskGetCurrentTaskHandle();
    TickType_t initialTickCount = xTaskGetTickCount();
    PowerScheduler::CurrentAllowance newAllowance = {
        .consumer = consumer,
        .maxSlackTimeMs = maxSlackTimeMs,
        .requestedDurationMs = requestedDurationMs,
        .taskHandle = currentTask,
        .neededCurrent = neededCurrent,
        .requestedAt = initialTickCount,
        .granted = false};
    PowerSchedulerMutex lock(powerSchedulerMutex);
    if (lock.isLocked() == false) {
      return false;
    }
    this->currentAllowances.push_back(newAllowance);
    lock.unlock();
    uint32_t notificationValue;
    BaseType_t notificationStatus = xTaskNotifyWait(
        0, 0, &notificationValue, pdMS_TO_TICKS(maxSlackTimeMs));
    // Code below will be executed after the task is woken up
    while (notificationStatus == pdPASS) {
      if (notificationValue ==
          PowerScheduler::PowerWakeupReasons::POWER_AVAILABLE) {
        // We were woken up because new power is available, check if it is
        // enough
        // Exclude existing consumption from the same consumer, as it will be
        // gone if this is granted
        float existingConsumption = getConsumerCurrent(consumer);
        const bool currentLimitNotExceeded =
            this->freeLimitCurrentBudget + existingConsumption > 0;
        const bool neededCurrentAvailableBelowMaximum =
            this->freeMaximumCurrentBudget + existingConsumption >=
            neededCurrent;
        const bool currentIsInsignificant = neededCurrent < 1;
        if (currentIsInsignificant ||
            (currentLimitNotExceeded && neededCurrentAvailableBelowMaximum)) {
          PowerSchedulerMutex lock(powerSchedulerMutex);
          if (lock.isLocked() == false) {
            return false;
          }
          if (existingConsumption > 0) {
            releaseCurrent(consumer);
          }
          this->currentAllowances.push_back(
              {.consumer = consumer,
               .maxSlackTimeMs = 0,
               .requestedDurationMs = requestedDurationMs,
               .taskHandle = xTaskGetCurrentTaskHandle(),
               .neededCurrent = neededCurrent,
               .requestedAt = initialTickCount,
               .grantedAt = xTaskGetTickCount(),
               .granted = true});
          ESP_LOGI(TAG, "%d mA granted to consumer %d with a delay of %d ms",
                   neededCurrent, static_cast<int>(consumer),
                   xTaskGetTickCount() - initialTickCount);
          return true;
        } else {
          // Still not enough power available for us. Wait the remaining ticks.
          xTaskNotifyWait(0, 0, &notificationValue,
                          pdMS_TO_TICKS(maxSlackTimeMs) -
                              (xTaskGetTickCount() - initialTickCount));
        }
      }
    }
    if (notificationStatus == pdFALSE) {
      // We waited long enough...
      // Give up and remove the task from the list of waiting tasks
      PowerSchedulerMutex lock(powerSchedulerMutex);
      if (lock.isLocked() == false) {
        return false;
      }
      for (auto it = currentAllowances.begin(); it != currentAllowances.end();
           ++it) {
        if (it->consumer == consumer && it->requestedAt == initialTickCount) {
          currentAllowances.erase(it);
          break;
        }
      }
      ESP_LOGW(TAG,
               "Task %p timed out waiting for %f mA of power;"
               "currently allocated: %f mA;"
               "total available (2C): %f mA",
               xTaskGetCurrentTaskHandle(), neededCurrent, getCurrentCurrent(),
               this->maximumCurrent);
      return false;
    } else {
      // Should be impossible to reach
      throw "Reached impossible state";
    }
  }
}

void PowerScheduler::checkWaitingTasks(void) {
  // If there are requested allowances, try to grant the one expiring next
  if (this->currentAllowances.size() > 0) {

    PowerScheduler::CurrentAllowance *nextAllowance = getNextCurrentAllowance();
    if (nextAllowance != nullptr) {
      xTaskNotify(nextAllowance->taskHandle,
                  PowerScheduler::PowerWakeupReasons::POWER_AVAILABLE,
                  eSetValueWithOverwrite);
    }
  }
}

void PowerScheduler::recalculateCurrentBudgets(void) {

  // Get the respective maximums and subtract currently flowing currents
  ESP_LOGI(TAG, "Recalculating current budgets...");
  float tempFreeLimitCurrentBudget = PowerManager::getMax3V3Current();
  ESP_LOGI(TAG, "Got max 3V3 current: %.2f", tempFreeLimitCurrentBudget);
  float tempFreeMaximumCurrentBudget = PowerManager::getMax3V3Current() * 2;
  PowerSchedulerMutex lock(powerSchedulerMutex);
  if (lock.isLocked() == false) {
    return;
  }
  for (auto &allowance : currentAllowances) {
    if (allowance.granted) {
      tempFreeLimitCurrentBudget -= allowance.neededCurrent;
      tempFreeMaximumCurrentBudget -= allowance.neededCurrent;
    }
  }
  this->freeLimitCurrentBudget = tempFreeLimitCurrentBudget;
  this->freeMaximumCurrentBudget = tempFreeMaximumCurrentBudget;
  ESP_LOGV(TAG, "Current budgets recalculated: %d mA, %d mA",
           this->freeLimitCurrentBudget, this->freeMaximumCurrentBudget);
}

PowerScheduler::CurrentAllowance *
PowerScheduler::getCurrentAllowance(PowerParameters::PowerConsumers consumer) {
  PowerSchedulerMutex lock(powerSchedulerMutex);
  if (lock.isLocked() == false) {
    return nullptr;
  }
  for (auto &allowance : currentAllowances) {
    if (allowance.consumer == consumer) {
      return &allowance;
    }
  }
  return nullptr;
}
PowerScheduler::CurrentAllowance *
PowerScheduler::getCurrentAllowance(TaskHandle_t taskHandle) {
  PowerSchedulerMutex lock(powerSchedulerMutex);
  if (lock.isLocked() == false) {
    return nullptr;
  }
  for (auto &allowance : currentAllowances) {
    if (allowance.taskHandle == taskHandle) {
      return &allowance;
    }
  }
  return nullptr;
}
PowerScheduler::CurrentAllowance *PowerScheduler::getNextCurrentAllowance(void) {
  TickType_t minTicks = UINT32_MAX;
  CurrentAllowance *nextAllowance = nullptr;

  std::vector<CurrentAllowance> sortedAllowances;
  sortedAllowances.reserve(currentAllowances.size());
  for (auto &allowance : currentAllowances) {
    if (!(allowance.granted)) {
      sortedAllowances.push_back(allowance);
    }
  }
  // Sort the not yet granted requests by how much time they have left before
  // expiring
  std::sort(sortedAllowances.begin(), sortedAllowances.end(),
            [](const CurrentAllowance &a, const CurrentAllowance &b) {
              return a.requestedAt + pdMS_TO_TICKS(a.maxSlackTimeMs) <
                     b.requestedAt + pdMS_TO_TICKS(b.maxSlackTimeMs);
            });

  // Get the first one whose power requirement can be met
  for (CurrentAllowance &allowance : sortedAllowances) {
    const float existingConsumerConsumption =
        getConsumerCurrent(allowance.consumer);
    const bool currentAvailableBelowMaximum =
        this->freeMaximumCurrentBudget + existingConsumerConsumption >=
        allowance.neededCurrent;
    const bool currentInsignificant =
        allowance.neededCurrent < PowerParameters::CURRENT_INSIGNIFICANT;
    if (currentInsignificant || currentAvailableBelowMaximum) {
      return &allowance;
    }
  }
  // Will be nullptr if no allowance was found
  return nullptr;
}

PowerScheduler &PowerScheduler::getPowerScheduler(float i_limit_ma,
                                                  float i_max_ma) {
  if (powerSchedulerInstance == nullptr) {
    powerSchedulerInstance = new PowerScheduler(i_limit_ma, i_max_ma);
  }
  return *powerSchedulerInstance;
}

float PowerScheduler::getCurrentCurrent(void) {
  float currentSum = 0;
  PowerSchedulerMutex lock(powerSchedulerMutex);
  if (lock.isLocked() == false) {
    return false;
  }
  for (auto &allowance : currentAllowances) {
    if (allowance.granted) {
      currentSum += allowance.neededCurrent;
    }
  }
  return currentSum;
}

float PowerScheduler::getFreeLimitCurrentBudget(void) {
  return this->freeLimitCurrentBudget;
}

float PowerScheduler::getFreeMaximumCurrentBudget(void) {
  return this->freeMaximumCurrentBudget;
}

float PowerScheduler::getConsumerCurrent(
    PowerParameters::PowerConsumers consumer) {
  PowerSchedulerMutex lock(powerSchedulerMutex);
  if (lock.isLocked() == false) {
    return false;
  }
  float currentSum = 0;
  for (auto &allowance : currentAllowances) {
    if (allowance.consumer == consumer && allowance.granted) {
      currentSum += allowance.neededCurrent;
    }
  }
  return currentSum;
}

PowerScheduler::PowerScheduler(float i_limit_ma, float i_max_ma)
    : freeLimitCurrentBudget(i_limit_ma), freeMaximumCurrentBudget(i_max_ma) {
  this->currentAllowances = std::vector<CurrentAllowance>();
  this->powerSchedulerMutex = xSemaphoreCreateMutex();
}

PowerScheduler::~PowerScheduler() {
  if (powerSchedulerMutex != NULL) {
    vSemaphoreDelete(powerSchedulerMutex);
  }
}
