// SPDX-License-Identifier: MIT
/*
 * Copyright (C) 2023-2024 Mathieu Carbou and others
 */
#include "MycilaJSY.h"
#include "MycilaLogger.h"

#include <algorithm>

#define TAG "JSY"
#define FLUSH_BUFFER_SIZE 256

static const uint8_t JSY_READ_MSG[] = {0x01, 0x03, 0x00, 0x48, 0x00, 0x0E, 0x44, 0x18};
static uint8_t BUFFER[FLUSH_BUFFER_SIZE];

void Mycila::JSYClass::begin() {
  if (_enabled)
    return;

  if (!JSYConfig.isEnabled()) {
    Logger.warn(TAG, "Disable JSY");
    return;
  }

  int32_t valRX = JSYConfig.getRXPin();
  if (GPIO_IS_VALID_GPIO(valRX)) {
    _pinRX = (gpio_num_t)valRX;
  } else {
    Logger.error(TAG, "Disable JSY: Invalid JSY RX pin: %u", _pinRX);
    _pinRX = GPIO_NUM_NC;
    return;
  }

  int32_t valTX = JSYConfig.getTXPin();
  if (GPIO_IS_VALID_OUTPUT_GPIO(valTX)) {
    _pinTX = (gpio_num_t)valTX;
  } else {
    Logger.error(TAG, "Disable JSY: Invalid JSY TX pin: %u", _pinTX);
    _pinTX = GPIO_NUM_NC;
    return;
  }

  Logger.info(TAG, "Enable JSY...");
  Logger.debug(TAG, "- JSY RX Pin: %u", _pinRX);
  Logger.debug(TAG, "- JSY TX Pin: %u", _pinTX);
  Logger.debug(TAG, "- Async: %s", _async ? "true" : "false");

  const JSYBaudRate baudRates[] = {JSYBaudRate::BAUD_4800, JSYBaudRate::BAUD_9600, JSYBaudRate::BAUD_19200, JSYBaudRate::BAUD_38400};
  bool ok = false;
  for (int i = 0; i < 4; i++) {
    _serial->begin((uint32_t)baudRates[i], SERIAL_8N1, _pinTX, _pinRX);
    while (!_serial)
      yield();
    if (_read(4)) {
      Logger.debug(TAG, "- JSY Speed Detected: %u bps", (uint32_t)baudRates[i]);
      if (baudRates[i] == _baudRate) {
        ok = true;
        break;
      }
      _setBaudRate(_baudRate);
      _serial->end();
      _serial->begin((uint32_t)_baudRate, SERIAL_8N1, _pinTX, _pinRX);
      while (!_serial)
        yield();
      if (!_read(4)) {
        Logger.error(TAG, "Unable to read JSY data after baud rate change");
        break;
      }
      ok = true;
      break;
    } else {
      _serial->end();
    }
  }

  if (!ok) {
    Logger.error(TAG, "Unable to read JSY data with any baud rate");
    _serial->end();
    return;
  }

  if (_async && xTaskCreateUniversal(_task, "Mycila-JSY", MYCILA_JSY_ASYNC_STACK_SIZE, this, 1, nullptr, MYCILA_JSY_ASYNC_CORE) != pdPASS) {
    Logger.error(TAG, "Unable to create JSY async task");
    return;
  }

  _enabled = true;
}

void Mycila::JSYClass::end() {
  if (_disable()) {
    _serial->end();
  }
}

void Mycila::JSYClass::endAndResetEnergy() {
  _disable();

  Logger.warn(TAG, "Energy Reset...");

  const uint8_t data[] = {0x01, 0x10, 0x00, 0x0C, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF3, 0xFA};
  _serial->write(data, 13);
  _serial->flush();
  _readSerial();

  // Note: do not end() _serial: ESP needs to restart right after sending the reset command
  Logger.debug(TAG, "Energy Reset done");
}

bool Mycila::JSYClass::read() {
  if (!_enabled)
    return false;
  return _read();
}

void Mycila::JSYClass::toJson(const JsonObject& root) {
  root["current1"] = current1;
  root["current2"] = current2;
  root["enabled"] = _enabled;
  root["energy_returned1"] = energyReturned1;
  root["energy_returned2"] = energyReturned2;
  root["energy1"] = energy1;
  root["energy2"] = energy2;
  root["frequency"] = frequency;
  root["power_factor1"] = powerFactor1;
  root["power_factor2"] = powerFactor2;
  root["power1"] = power1;
  root["power2"] = power2;
  root["voltage1"] = voltage1;
  root["voltage2"] = voltage2;
}

bool Mycila::JSYClass::_read(uint8_t maxCount) {
  while (maxCount > 0 && !_read()) {
    if (--maxCount == 0)
      return false;
    delay(60);
  }
  return true;
}

bool __attribute__((hot)) Mycila::JSYClass::_read() {
  if (_reading)
    return false;

  _reading = true;

  _serial->write(JSY_READ_MSG, 8);
  _serial->flush();
  size_t count = _readSerial();

  if (count != 61 || BUFFER[0] != 0x01) {
    _reading = false;
    return false;
  }

  voltage1 = ((BUFFER[3] << 24) + (BUFFER[4] << 16) + (BUFFER[5] << 8) + BUFFER[6]) * 0.0001;
  current1 = ((BUFFER[7] << 24) + (BUFFER[8] << 16) + (BUFFER[9] << 8) + BUFFER[10]) * 0.0001;
  power1 = ((BUFFER[11] << 24) + (BUFFER[12] << 16) + (BUFFER[13] << 8) + BUFFER[14]) * (BUFFER[27] == 1 ? -0.0001 : 0.0001);
  energy1 = ((BUFFER[15] << 24) + (BUFFER[16] << 16) + (BUFFER[17] << 8) + BUFFER[18]) * 0.0001;
  powerFactor1 = ((BUFFER[19] << 24) + (BUFFER[20] << 16) + (BUFFER[21] << 8) + BUFFER[22]) * 0.001;
  energyReturned1 = ((BUFFER[23] << 24) + (BUFFER[24] << 16) + (BUFFER[25] << 8) + BUFFER[26]) * 0.0001;
  // BUFFER[27] is the sign of power1
  // BUFFER[28] is the sign of power2
  // BUFFER[29] unused
  // BUFFER[30] unused
  frequency = round(((BUFFER[31] << 24) + (BUFFER[32] << 16) + (BUFFER[33] << 8) + BUFFER[34]) * 0.01);
  voltage2 = ((BUFFER[35] << 24) + (BUFFER[36] << 16) + (BUFFER[37] << 8) + BUFFER[38]) * 0.0001;
  current2 = ((BUFFER[39] << 24) + (BUFFER[40] << 16) + (BUFFER[41] << 8) + BUFFER[42]) * 0.0001;
  power2 = ((BUFFER[43] << 24) + (BUFFER[44] << 16) + (BUFFER[45] << 8) + BUFFER[46]) * (BUFFER[28] == 1 ? -0.0001 : 0.0001);
  energy2 = ((BUFFER[47] << 24) + (BUFFER[48] << 16) + (BUFFER[49] << 8) + BUFFER[50]) * 0.0001;
  powerFactor2 = ((BUFFER[51] << 24) + (BUFFER[52] << 16) + (BUFFER[53] << 8) + BUFFER[54]) * 0.001;
  energyReturned2 = ((BUFFER[55] << 24) + (BUFFER[56] << 16) + (BUFFER[57] << 8) + BUFFER[58]) * 0.0001;

  _reading = false;
  return true;
}

bool Mycila::JSYClass::_setBaudRate(JSYBaudRate baudRate) {
  Logger.debug(TAG, "Set JSY baud rate to %u bps...", (uint32_t)baudRate);

  uint8_t data[] = {0x00, 0x10, 0x00, 0x04, 0x00, 0x01, 0x02, 0x01, 0x00, 0x00, 0x00};
  switch (baudRate) {
    case JSYBaudRate::BAUD_4800:
      data[8] = 0x05;
      data[9] = 0x6B;
      data[10] = 0xD7;
      break;
    case JSYBaudRate::BAUD_9600:
      data[8] = 0x06;
      data[9] = 0x2B;
      data[10] = 0xD6;
      break;
    case JSYBaudRate::BAUD_19200:
      data[8] = 0x07;
      data[9] = 0xEA;
      data[10] = 0x16;
      break;
    case JSYBaudRate::BAUD_38400:
      data[8] = 0x08;
      data[9] = 0xAA;
      data[10] = 0x12;
      break;
    default:
      assert(false);
      break;
  }

  _serial->write(data, 11);
  _serial->flush();
  _readSerial();

  Logger.debug(TAG, "JSY baud rate updated.");

  return true;
}

size_t Mycila::JSYClass::_readSerial() {
  size_t count = 0;
  while (const size_t n = _serial->available()) {
    const size_t pos = count % FLUSH_BUFFER_SIZE;
    count += _serial->readBytes(BUFFER + pos, min(n, FLUSH_BUFFER_SIZE - pos));
  }
  return count;
}

bool Mycila::JSYClass::_disable() {
  if (_enabled) {
    Logger.info(TAG, "Disable JSY...");
    _enabled = false;
    while (_reading)
      delay(100);
    current1 = 0;
    current2 = 0;
    energy1 = 0;
    energy2 = 0;
    energyReturned1 = 0;
    energyReturned2 = 0;
    frequency = 0;
    power1 = 0;
    power2 = 0;
    powerFactor1 = 0;
    powerFactor2 = 0;
    voltage1 = 0;
    voltage2 = 0;
    return true;
  }
  return false;
}

void Mycila::JSYClass::_task(void* pvParameters) {
  // Serial.println("JSY async task started");
  // Serial.println(xPortGetCoreID());
  JSYClass* jsy = reinterpret_cast<JSYClass*>(pvParameters);
  while (jsy->_enabled) {
    jsy->_read();
    if (jsy->_enabled)
      // https://esp32developer.com/programming-in-c-c/tasks/tasks-vs-co-routines
      delay(max(portTICK_PERIOD_MS, static_cast<uint32_t>(MYCILA_JSY_ASYNC_READ_PAUSE_INTERVAL_MS)));
  }
  vTaskDelete(NULL);
}

namespace Mycila {
  JSYClass JSY;
  JSYConfigClass JSYConfig;
} // namespace Mycila