#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"

#ifdef USE_ESP8266

#include "shelly_dimmer.h"
#ifdef USE_SHD_FIRMWARE_DATA
#include "stm32flash.h"
#endif

#ifndef USE_ESP_IDF
#include <HardwareSerial.h>
#endif

#include <algorithm>
#include <cstring>
#include <memory>
#include <numeric>

namespace {

constexpr char TAG[] = "shelly_dimmer";

constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200;  // ms
constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000;  // 100%

// Protocol framing.
constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;

// Supported commands.
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;

// Command payload sizes.
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;

// STM Firmware
#ifdef USE_SHD_FIRMWARE_DATA
constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA;
constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE);
#endif

// Scaling Constants
constexpr float POWER_SCALING_FACTOR = 880373;
constexpr float VOLTAGE_SCALING_FACTOR = 347800;
constexpr float CURRENT_SCALING_FACTOR = 1448;

// Essentially std::size() for pre c++17
template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }

}  // Anonymous namespace

namespace esphome {
namespace shelly_dimmer {

/// Computes a crappy checksum as defined by the Shelly Dimmer protocol.
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) {
  return std::accumulate<decltype(buf), uint16_t>(buf, buf + len, 0);
}

void ShellyDimmer::setup() {
  this->pin_nrst_->setup();
  this->pin_boot0_->setup();

  ESP_LOGI(TAG, "Initializing Shelly Dimmer...");

  // Reset the STM32 and check the firmware version.
  for (int i = 0; i < 2; i++) {
    this->reset_normal_boot_();
    this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0);
    ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_,
             this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION);
    if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
        this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
#ifdef USE_SHD_FIRMWARE_DATA
      // Update firmware if needed.
      ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing");
      if (i > 0) {
        // Upgrade was already performed but the reported version is still not right.
        ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect");
        this->mark_failed();
        return;
      }

      if (!this->upgrade_firmware_()) {
        ESP_LOGW(TAG, "Failed to upgrade firmware");
        this->mark_failed();
        return;
      }

      // Firmware upgrade completed, do the checks again.
      continue;
#else
      ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
      this->mark_failed();
      return;
#endif
    }
    break;
  }

  this->send_settings_();
  // Do an immediate poll to refresh current state.
  this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);

  this->ready_ = true;
}

void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); }

void ShellyDimmer::dump_config() {
  ESP_LOGCONFIG(TAG, "ShellyDimmer:");
  LOG_PIN("  NRST Pin: ", this->pin_nrst_);
  LOG_PIN("  BOOT0 Pin: ", this->pin_boot0_);

  ESP_LOGCONFIG(TAG, "  Leading Edge: %s", YESNO(this->leading_edge_));
  ESP_LOGCONFIG(TAG, "  Warmup Brightness: %d", this->warmup_brightness_);
  // ESP_LOGCONFIG(TAG, "  Warmup Time: %d", this->warmup_time_);
  // ESP_LOGCONFIG(TAG, "  Fade Rate: %d", this->fade_rate_);
  ESP_LOGCONFIG(TAG, "  Minimum Brightness: %d", this->min_brightness_);
  ESP_LOGCONFIG(TAG, "  Maximum Brightness: %d", this->max_brightness_);

  LOG_UPDATE_INTERVAL(this);

  ESP_LOGCONFIG(TAG, "  STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_);
  ESP_LOGCONFIG(TAG, "  STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION,
                USE_SHD_FIRMWARE_MINOR_VERSION);

  if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
      this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
    ESP_LOGE(TAG, "  Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
  }
}

void ShellyDimmer::write_state(light::LightState *state) {
  if (!this->ready_) {
    return;
  }

  float brightness;
  state->current_values_as_brightness(&brightness);

  const uint16_t brightness_int = this->convert_brightness_(brightness);
  if (brightness_int == this->brightness_) {
    ESP_LOGV(TAG, "Not sending unchanged value");
    return;
  }
  ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);

  this->send_brightness_(brightness_int);
}
#ifdef USE_SHD_FIRMWARE_DATA
bool ShellyDimmer::upgrade_firmware_() {
  ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
  this->reset_dfu_boot_();

  // Cleanup with RAII
  auto stm32 = stm32_init(this, STREAM_SERIAL, 1);

  if (!stm32) {
    ESP_LOGW(TAG, "Failed to initialize STM32");
    return false;
  }

  // Erase STM32 flash.
  if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
    ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
    return false;
  }

  static constexpr uint32_t BUFFER_SIZE = 256;

  // Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored
  // in flash memory so all accesses need to be 4-byte aligned.
  uint8_t buffer[BUFFER_SIZE];
  const uint8_t *p = STM_FIRMWARE;
  uint32_t offset = 0;
  uint32_t addr = stm32->dev->fl_start;
  const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES;

  while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
    const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE);
    const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);

    if (len == 0) {
      break;
    }

    std::memcpy(buffer, p, BUFFER_SIZE);
    p += BUFFER_SIZE;

    if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
      ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
      return false;
    }

    addr += len;
    offset += len;
  }

  ESP_LOGI(TAG, "STM32 firmware upgrade successful");

  return true;
}
#endif

uint16_t ShellyDimmer::convert_brightness_(float brightness) {
  // Special case for zero as only zero means turn off completely.
  if (brightness == 0.0) {
    return 0;
  }

  return remap<uint16_t, float>(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_);
}

void ShellyDimmer::send_brightness_(uint16_t brightness) {
  const uint8_t payload[] = {
      // Brightness (%) * 10.
      static_cast<uint8_t>(brightness & 0xff),
      static_cast<uint8_t>(brightness >> 8),
  };
  static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size");

  this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);

  this->brightness_ = brightness;
}

void ShellyDimmer::send_settings_() {
  const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_);

  float brightness = 0.0;
  if (this->state_ != nullptr) {
    this->state_->current_values_as_brightness(&brightness);
  }
  const uint16_t brightness_int = this->convert_brightness_(brightness);
  ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);

  const uint8_t payload[] = {
      // Brightness (%) * 10.
      static_cast<uint8_t>(brightness_int & 0xff),
      static_cast<uint8_t>(brightness_int >> 8),
      // Leading / trailing edge [0x01 = leading, 0x02 = trailing].
      this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02},
      0x00,
      // Fade rate.
      static_cast<uint8_t>(fade_rate & 0xff),
      static_cast<uint8_t>(fade_rate >> 8),
      // Warmup brightness.
      static_cast<uint8_t>(this->warmup_brightness_ & 0xff),
      static_cast<uint8_t>(this->warmup_brightness_ >> 8),
      // Warmup time.
      static_cast<uint8_t>(this->warmup_time_ & 0xff),
      static_cast<uint8_t>(this->warmup_time_ >> 8),
  };
  static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size");

  this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);

  // Also send brightness separately as it is ignored above.
  this->send_brightness_(brightness_int);
}

bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) {
  ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str());

  // Prepare a command frame.
  uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
  const size_t frame_len = this->frame_command_(frame, cmd, payload, len);

  // Write the frame and wait for acknowledgement.
  int retries = SHELLY_DIMMER_MAX_RETRIES;
  while (retries--) {
    this->write_array(frame, frame_len);
    this->flush();

    ESP_LOGD(TAG, "Command sent, waiting for reply");
    const uint32_t tx_time = millis();
    while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
      if (this->read_frame_()) {
        return true;
      }
      delay(1);
    }
    ESP_LOGW(TAG, "Timeout while waiting for reply");
  }
  ESP_LOGW(TAG, "Failed to send command");
  return false;
}

size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) {
  size_t pos = 0;

  // Generate a frame.
  data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
  data[1] = ++this->seq_;
  data[2] = cmd;
  data[3] = len;
  pos += 4;

  if (payload != nullptr) {
    std::memcpy(data + 4, payload, len);
    pos += len;
  }

  // Calculate checksum for the payload.
  const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len);
  data[pos++] = static_cast<uint8_t>(csum >> 8);
  data[pos++] = static_cast<uint8_t>(csum & 0xff);
  data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
  return pos;
}

int ShellyDimmer::handle_byte_(uint8_t c) {
  const uint8_t pos = this->buffer_pos_;

  if (pos == 0) {
    // Must be start byte.
    return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
  } else if (pos < 4) {
    // Header.
    return 1;
  }

  // Decode payload length from header.
  const uint8_t payload_len = this->buffer_[3];
  if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
    return -1;
  }

  if (pos < 4 + payload_len + 1) {
    // Payload.
    return 1;
  }

  if (pos == 4 + payload_len + 1) {
    // Verify checksum.
    const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
    const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
    if (csum != csum_verify) {
      return -1;
    }
    return 1;
  }

  if (pos == 4 + payload_len + 2) {
    // Must be end byte.
    return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
  }
  return -1;
}

bool ShellyDimmer::read_frame_() {
  while (this->available()) {
    const uint8_t c = this->read();
    this->buffer_[this->buffer_pos_] = c;

    ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_);

    switch (this->handle_byte_(c)) {
      case 0: {
        // Frame successfully received.
        this->handle_frame_();
        this->buffer_pos_ = 0;
        return true;
      }
      case -1: {
        // Failure.
        this->buffer_pos_ = 0;
        break;
      }
      case 1: {
        // Need more data.
        this->buffer_pos_++;
        break;
      }
    }
  }
  return false;
}

bool ShellyDimmer::handle_frame_() {
  const uint8_t seq = this->buffer_[1];
  const uint8_t cmd = this->buffer_[2];
  const uint8_t payload_len = this->buffer_[3];

  ESP_LOGD(TAG, "Got frame: 0x%02x", cmd);

  // Compare with expected identifier as the frame is always a response to
  // our previously sent command.
  if (seq != this->seq_) {
    return false;
  }

  const uint8_t *payload = &this->buffer_[4];

  // Handle response.
  switch (cmd) {
    case SHELLY_DIMMER_PROTO_CMD_POLL: {
      if (payload_len < 16) {
        return false;
      }

      const uint8_t hw_version = payload[0];
      // payload[1] is unused.
      const uint16_t brightness = encode_uint16(payload[3], payload[2]);

      const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]);

      const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]);

      const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]);

      const uint16_t fade_rate = payload[16];

      float power = 0;
      if (power_raw > 0) {
        power = POWER_SCALING_FACTOR / static_cast<float>(power_raw);
      }

      float voltage = 0;
      if (voltage_raw > 0) {
        voltage = VOLTAGE_SCALING_FACTOR / static_cast<float>(voltage_raw);
      }

      float current = 0;
      if (current_raw > 0) {
        current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
      }

      ESP_LOGI(TAG, "Got dimmer data:");
      ESP_LOGI(TAG, "  HW version: %d", hw_version);
      ESP_LOGI(TAG, "  Brightness: %d", brightness);
      ESP_LOGI(TAG, "  Fade rate:  %d", fade_rate);
      ESP_LOGI(TAG, "  Power:      %f W", power);
      ESP_LOGI(TAG, "  Voltage:    %f V", voltage);
      ESP_LOGI(TAG, "  Current:    %f A", current);

      // Update sensors.
      if (this->power_sensor_ != nullptr) {
        this->power_sensor_->publish_state(power);
      }
      if (this->voltage_sensor_ != nullptr) {
        this->voltage_sensor_->publish_state(voltage);
      }
      if (this->current_sensor_ != nullptr) {
        this->current_sensor_->publish_state(current);
      }

      return true;
    }
    case SHELLY_DIMMER_PROTO_CMD_VERSION: {
      if (payload_len < 2) {
        return false;
      }

      this->version_minor_ = payload[0];
      this->version_major_ = payload[1];
      return true;
    }
    case SHELLY_DIMMER_PROTO_CMD_SWITCH:
    case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
      return !(payload_len < 1 || payload[0] != 0x01);
    }
    default: {
      return false;
    }
  }
}

void ShellyDimmer::reset_(bool boot0) {
  ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0);

  this->pin_boot0_->digital_write(boot0);
  this->pin_nrst_->digital_write(false);

  // Wait 50ms for the STM32 to reset.
  delay(50);  // NOLINT

  // Clear receive buffer.
  while (this->available()) {
    this->read();
  }

  this->pin_nrst_->digital_write(true);
  // Wait 50ms for the STM32 to boot.
  delay(50);  // NOLINT

  ESP_LOGD(TAG, "Reset STM32 done");
}

void ShellyDimmer::reset_normal_boot_() {
  // set NONE parity in normal mode

#ifndef USE_ESP_IDF  // workaround for reconfiguring the uart
  Serial.end();
  Serial.begin(115200, SERIAL_8N1);
  Serial.flush();
#endif

  this->flush();
  this->reset_(false);
}

void ShellyDimmer::reset_dfu_boot_() {
  // set EVEN parity in bootloader mode

#ifndef USE_ESP_IDF  // workaround for reconfiguring the uart
  Serial.end();
  Serial.begin(115200, SERIAL_8E1);
  Serial.flush();
#endif

  this->flush();
  this->reset_(true);
}

}  // namespace shelly_dimmer
}  // namespace esphome

#endif  // USE_ESP8266
