/**
 * @file Power.h
 * @author Phillip Kühne
 * @brief This component provides utilities for keeping track of power usage
 * consumption.
 * @version 0.1
 * @date 2024-11-23
 */

#include "Power.h"

SemaphoreHandle_t Power::powerMutex = NULL;

void vTaskUpdatePowerState(void *pvParameters) {
  for (;;) {
    ESP_LOGV(TAG, "Updating Power State...");
    Power::updatePowerStateHandler();
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}

void Power::begin() {
  // Check if another instance of us already initialized the power scheduler,
  // if not, we will do it.
  ESP_LOGI(TAG, "Initializing Power Management");

  // Create mutex if it doesn't exist
  if (powerMutex == NULL) {
    powerMutex = xSemaphoreCreateMutex();
    if (powerMutex == NULL) {
      ESP_LOGE(TAG, "Failed to create power mutex");
      Serial.println("Failed to create power mutex");
    }
  }

  if (powerScheduler == nullptr) {
    ESP_LOGI(TAG, "Creating Power Scheduler");
    powerScheduler = &PowerScheduler::getPowerScheduler(
        PowerParameters::Battery::CELL_CURRENT_1C_MA,
        PowerParameters::Battery::CELL_CURRENT_2C_MA);
    Power::initPowerState();
    Power::recalculateCurrentBudgets();
    TaskHandle_t xHandle = NULL;
    xTaskCreate(vTaskUpdatePowerState, "vTaskPowerStateUpdate", 4096, NULL,
                tskIDLE_PRIORITY, &xHandle);
    configASSERT(xHandle);

    if (!(powerScheduler->tryAccquireCurrentAllowance(
            PowerParameters::PowerConsumers::ESP,
            PowerParameters::CurrentConsumptions::CURRENT_ESP_AVG))) {
      ESP_LOGE(TAG, "Could not get power for ESP");
      Serial.println("Could not get power for ESP");
    }
  }
}

float Power::getFreeLimitCurrentBudget() {
  return powerScheduler->getFreeLimitCurrentBudget();
}

float Power::getFreeMaximumCurrentBudget() {
  return powerScheduler->getFreeMaximumCurrentBudget();
}

bool Power::tryAccquireCurrentAllowance(
    PowerParameters::PowerConsumers consumer, uint16_t neededCurrent,
    uint16_t requestedDurationMs) {
  return powerScheduler->tryAccquireCurrentAllowance(consumer, neededCurrent,
                                                     requestedDurationMs);
}

void Power::releaseCurrent(PowerParameters::PowerConsumers consumer) {
  powerScheduler->releaseCurrent(consumer);
}

bool Power::waitForCurrentAllowance(PowerParameters::PowerConsumers consumer,
                                    uint16_t neededCurrent,
                                    uint16_t maxSlackTimeMs,
                                    uint16_t requestedDurationMs) {
  return powerScheduler->waitForCurrentAllowance(
      consumer, neededCurrent, maxSlackTimeMs, requestedDurationMs);
}

void Power::beginPermanentDeepSleep(void) {
  return powerScheduler->beginPermanentDeepSleep();
}

float Power::getCurrentCurrent(void) {
  return powerScheduler->getCurrentCurrent();
}

float Power::getBatteryCurrent(void) {
  const float i_3v3 = getCurrentCurrent();
  const float u_3v3 = 3.3;
  const float u_bat = getBatteryVoltage();
  const float eta = PowerParameters::BUCK_BOOST_EFFICIENCY;
  return (u_3v3 * i_3v3) / (u_bat * eta);
}

void Power::recalculateCurrentBudgets(void) {
  return powerScheduler->recalculateCurrentBudgets();
}

float Power::getConsumerCurrent(PowerParameters::PowerConsumers consumer) {
  return powerScheduler->getConsumerCurrent(consumer);
}

float Power::getBatteryVoltage() {
  // Get the battery voltage from the ADC and convert it to a voltage
  // using the voltage divider.
  pinMode(PowerParameters::PinConfig::BAT_ADC_EN, OUTPUT);
  pinMode(PowerParameters::PinConfig::BAT_ADC, INPUT);
  // Enable voltage divider
  digitalWrite(PowerParameters::PinConfig::BAT_ADC_EN, HIGH);
  // Allow voltage to stabilize
  delayMicroseconds(10);
  // Returns value between 0 and 4095 mapping to between 0 and 3.3V
  uint16_t batteryAdcValue = analogRead(PowerParameters::PinConfig::BAT_ADC);
  // Disable voltage divider
  digitalWrite(PowerParameters::PinConfig::BAT_ADC_EN, LOW);
  // Convert ADC value to voltage
  float batteryVoltage =
      (batteryAdcValue / 4095.0 * 3.3) *
      PowerParameters::Battery::BAT_ADC::VOLTAGE_DIVIDER_FACTOR;
  ESP_LOGD(TAG, "Battery ADC value: %d, Calculated voltage: %.2f",
           batteryAdcValue, batteryVoltage);
  return batteryVoltage;
}

float Power::getBatteryChargePercent() { return percentRemaining; }

float Power::getBatteryChargeCoulombs() { return coloumbsRemaining; }

float Power::getBatteryVoltageChargePercent() {
  // Directly get the battery voltage, correct the curve with an offset and
  // calculate the charge state based on the discharge curve.
  float batteryVoltage = getBatteryVoltage() + Power::fullVoltageOffset;
  float chargeState = 0;
  // Clamp edge cases
  if (batteryVoltage >=
      PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[0]) {
    return PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES[0];
  }
  if (batteryVoltage <=
      PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES
          [PowerParameters::Battery::DISCHARGE_CURVE::NUM_POINTS - 1]) {
    return PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES
        [PowerParameters::Battery::DISCHARGE_CURVE::NUM_POINTS - 1];
  }
  float p1_x, p1_y, p2_x, p2_y;
  for (int i = 0; i < PowerParameters::Battery::DISCHARGE_CURVE::NUM_POINTS;
       i++) {
    if (batteryVoltage >=
        PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[i]) {
      p1_y = PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES[i];
      p1_x = PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[i];
      p2_y = PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES[i + 1];
      p2_x = PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[i + 1];
      chargeState =
          ((p2_y - p1_y) / (p2_x - p1_x)) * (batteryVoltage - p1_x) + p1_y;
      return chargeState;
    }
  }
}

void Power::updatePowerStateHandler() {

  // Update supply and charge state flags
  Power::busPowered = !digitalRead(PowerParameters::PinConfig::VUSB_SENS);
  Power::chargingState = digitalRead(PowerParameters::PinConfig::BAT_CHG_STAT);

  // If the battery is charging and fully charged
  if (Power::busPowered && !Power::chargingState) {
    // Calibrate voltage offset on full Battery
    Power::fullVoltageOffset = PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[0] - getBatteryVoltage();
  }

  ESP_LOGD(TAG, "Bus Powered: %d, Charging: %d", busPowered, chargingState);

  float currentCurrent = powerScheduler->getCurrentCurrent();
  int referenceCurrentMa =
      PowerParameters::Battery::DISCHARGE_CURVE::REFERENCE_CURRENT_A * 1000;

  float coloumbsConsumedSinceLastUpdate;

  // Calculate remaining battery charge in Coulombs based on current and time
  if (!Power::busPowered) {
    coloumbsConsumedSinceLastUpdate =
        (currentCurrent / 1000) *
        ((pdTICKS_TO_MS(xTaskGetTickCount() - lastPowerStateUpdate)) / 1000.0);

    // Update coloumbs remaining
    coloumbsRemaining -= coloumbsConsumedSinceLastUpdate;
  }
  float chargeState;

  // If current flow is close enough to reference, get battery charge state via
  // voltage curve
  if (!Power::busPowered) {
    if ((currentCurrent > (referenceCurrentMa * 0.6)) &&
        (currentCurrent < (referenceCurrentMa * 1.4))) {
      // Get battery charge state from voltage curve
      chargeState = getBatteryVoltageChargePercent();
      ESP_LOGD(TAG, "Charge state from voltage: %f", chargeState);
    } else {
      // Estimate battery charge state from charge consumption
      float oldChargeState = lastSOC[latestSoCIndex];
      chargeState = oldChargeState -
                    ((coloumbsConsumedSinceLastUpdate /
                      PowerParameters::Battery::CELL_CHARGE_FULL_COLOUMB) *
                     100);
      ESP_LOGD(TAG, "Charge state from current: %f", chargeState);
    }
  } else {
    // If we are charging, we can't estimate the charge state based on current
    // consumption
    chargeState = getBatteryVoltageChargePercent();
    ESP_LOGD(TAG, "Charge state from voltage (USB powered): %f", chargeState);
  }

  // Bound value between 0 and 100
  chargeState = chargeState > 100 ? 100 : chargeState;
  chargeState = chargeState < 0 ? 0 : chargeState;

  addSoCSample(chargeState);

  for (int i = 0; i < PowerParameters::Battery::AVERAGING_SAMPLES; i++) {
    ESP_LOGD(TAG, "SoC[%d]: %f", i, lastSOC[i]);
  }

  // Update percentage remaining based on charge state average
  float sampleSum = 0;
  for (int i = 0; i < PowerParameters::Battery::AVERAGING_SAMPLES; i++) {
    sampleSum += lastSOC[i];
  }
  Power::percentRemaining =
      sampleSum / PowerParameters::Battery::AVERAGING_SAMPLES;

  // Update last update time
  Power::lastPowerStateUpdate = xTaskGetTickCount();

  // Update the available current (changes based on battery state of charge)
  Power::powerScheduler->recalculateCurrentBudgets();
  ESP_LOGD(TAG, "Current: %f mA, Charge: %f Coulombs, %f %%", currentCurrent,
           coloumbsRemaining, percentRemaining);

  return;
}

float Power::getMax3V3Current() {
  // Conversion from Thesis
  float u_bat = getBatteryVoltage();
  float i_bat = PowerParameters::Battery::CELL_CURRENT_1C_MA;
  float eta = PowerParameters::BUCK_BOOST_EFFICIENCY;
  constexpr float u_3v3 = 3.3;
  return (u_bat * i_bat * eta) / u_3v3;
}

void Power::addSoCSample(float soc) {
  PowerMutex lock(powerMutex);
  if (!lock.isLocked()) {
    ESP_LOGE(TAG, "Could not take power to add SoC sample");
    return;
  }
  latestSoCIndex =
      (latestSoCIndex + 1) % PowerParameters::Battery::AVERAGING_SAMPLES;
  lastSOC[latestSoCIndex] = soc;
}

void Power::initPowerState(void) {
  // Initialize the power state
  lastPowerStateUpdate = xTaskGetTickCount();
  // TODO: Get initial battery charge state based on voltage, set coloumbs based
  // on that
  const float initialChargePercentages = getBatteryVoltageChargePercent();
  for (int i = 0; i < PowerParameters::Battery::AVERAGING_SAMPLES; i++) {
    lastSOC[i] = initialChargePercentages;
  }
  coloumbsRemaining = initialChargePercentages / 100 *
                      PowerParameters::Battery::CELL_CHARGE_FULL_COLOUMB;
  constexpr float fullColoumbs =
      PowerParameters::Battery::CELL_CHARGE_FULL_COLOUMB;
  percentRemaining = initialChargePercentages;
  // Set up flags and pins for them
  pinMode(PowerParameters::PinConfig::VUSB_SENS, INPUT_PULLUP);
  pinMode(PowerParameters::PinConfig::BAT_CHG_STAT, INPUT_PULLUP);
  busPowered = digitalRead(PowerParameters::PinConfig::VUSB_SENS);
  chargingState = digitalRead(PowerParameters::PinConfig::BAT_CHG_STAT);
}

void Power::dumpPowerStatistics() {
  Serial.printf("======== Dezibot Power Statistics ========\r\n");
  Serial.printf("Current: %f mA\r\n", Power::getCurrentCurrent());
  Serial.printf("Battery Voltage: %f V\r\n", Power::getBatteryVoltage());
  Serial.printf("Battery Charge: %f %%\r\n", Power::getBatteryChargePercent());
  Serial.printf("Battery Charge: %f Coulombs\r\n",
                Power::getBatteryChargeCoulombs());
  Serial.printf("Max 3.3V Current in this state (1C, 2C): %f mA, %f mA \r\n",
                Power::getMax3V3Current(), Power::getMax3V3Current() * 2);
  Serial.printf("=========================================\r\n");
}

void Power::dumpConsumerStatistics() {
  Serial.printf("======== Dezibot Consumer Statistics ========\r\n");
  Serial.printf("ESP: %f mA\r\n", Power::getConsumerCurrent(
                                      PowerParameters::PowerConsumers::ESP));
  Serial.printf("WIFI: %f mA\r\n", Power::getConsumerCurrent(
                                       PowerParameters::PowerConsumers::WIFI));
  Serial.printf("LED_RGB_TOP_LEFT: %f mA\r\n",
                Power::getConsumerCurrent(
                    PowerParameters::PowerConsumers::LED_RGB_TOP_LEFT));
  Serial.printf("LED_RGB_TOP_RIGHT: %f mA\r\n",
                Power::getConsumerCurrent(
                    PowerParameters::PowerConsumers::LED_RGB_TOP_RIGHT));
  Serial.printf("LED_RGB_BOTTOM: %f mA\r\n",
                Power::getConsumerCurrent(
                    PowerParameters::PowerConsumers::LED_RGB_BOTTOM));
  Serial.printf(
      "RGBW_SENSOR: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::RGBW_SENSOR));
  Serial.printf("LED_IR_BOTTOM: %f mA\r\n",
                Power::getConsumerCurrent(
                    PowerParameters::PowerConsumers::LED_IR_BOTTOM));
  Serial.printf(
      "LED_IR_FRONT: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::LED_IR_FRONT));
  Serial.printf(
      "PT_IR: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::PT_IR));
  Serial.printf(
      "PT_DL: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::PT_DL));
  Serial.printf(
      "LED_UV: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::LED_UV));
  Serial.printf(
      "DISPLAY_OLED: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::DISPLAY_OLED));
  Serial.printf(
      "MOTOR_LEFT: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::MOTOR_LEFT));
  Serial.printf(
      "MOTOR_RIGHT: %f mA\r\n",
      Power::getConsumerCurrent(PowerParameters::PowerConsumers::MOTOR_RIGHT));
  Serial.printf("IMU: %f mA\r\n", Power::getConsumerCurrent(
                                      PowerParameters::PowerConsumers::IMU));
  Serial.printf("=============================================\r\n");
}

bool Power::isUSBPowered() { return busPowered; }

bool Power::isBatteryPowered() { return !busPowered; }

bool Power::isBatteryCharging() { return chargingState && busPowered; }

bool Power::isBatteryDischarging() { return !chargingState && !busPowered; }

bool Power::isBatteryFullyCharged() { return !chargingState && busPowered; }
int Power::latestSoCIndex = 0;
float Power::lastSOC[PowerParameters::Battery::AVERAGING_SAMPLES] = {0};
TickType_t Power::lastPowerStateUpdate = 0;
float Power::coloumbsRemaining =
    PowerParameters::Battery::CELL_CHARGE_FULL_COLOUMB;
float Power::percentRemaining = 100.0;
PowerScheduler *Power::powerScheduler = nullptr;

bool Power::busPowered = false;

bool Power::chargingState = false;

float Power::fullVoltageOffset = 0;

Power::Power() {}

Power::~Power() {}