#include "vbus.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cinttypes>

namespace esphome::vbus {

static const char *const TAG = "vbus";

// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical)
static constexpr size_t VBUS_MAX_LOG_BYTES = 64;

void VBus::dump_config() {
  ESP_LOGCONFIG(TAG, "VBus:");
  check_uart_settings(9600);
}

static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) {
  for (int i = 0; i < count; i++, septet >>= 1) {
    if (septet & 1)
      data[start + i] |= 0x80;
  }
}

static bool checksum(const uint8_t *data, int start, int count) {
  uint8_t csum = 0x7f;
  for (int i = 0; i < count; i++)
    csum = (csum - data[start + i]) & 0x7f;
  return csum == 0;
}

void VBus::loop() {
  if (!available())
    return;

  while (available()) {
    uint8_t c;
    read_byte(&c);

    if (c == 0xaa) {
      this->state_ = 1;
      this->buffer_.clear();
      continue;
    }
    if (c & 0x80) {
      this->state_ = 0;
      continue;
    }
    if (this->state_ == 0)
      continue;

    if (this->state_ == 1) {
      this->buffer_.push_back(c);
      if (this->buffer_.size() == 7) {
        this->protocol_ = this->buffer_[4];
        this->source_ = (this->buffer_[3] << 8) + this->buffer_[2];
        this->dest_ = (this->buffer_[1] << 8) + this->buffer_[0];
        this->command_ = (this->buffer_[6] << 8) + this->buffer_[5];
      }
      if ((this->protocol_ == 0x20) && (this->buffer_.size() == 15)) {
        this->state_ = 0;
        if (!checksum(this->buffer_.data(), 0, 15)) {
          ESP_LOGE(TAG, "P2 checksum failed");
          continue;
        }
        septet_spread(this->buffer_.data(), 7, 6, this->buffer_[13]);
        uint16_t id = (this->buffer_[8] << 8) + this->buffer_[7];
        uint32_t value = encode_uint32(this->buffer_[12], this->buffer_[11], this->buffer_[10], this->buffer_[9]);
        ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04" PRIx32 " (%" PRIu32 ")", this->command_, this->source_,
                 this->dest_, id, value, value);
      } else if ((this->protocol_ == 0x10) && (this->buffer_.size() == 9)) {
        if (!checksum(this->buffer_.data(), 0, 9)) {
          ESP_LOGE(TAG, "P1 checksum failed");
          this->state_ = 0;
          continue;
        }
        this->frames_ = this->buffer_[7];
        if (this->frames_) {
          this->state_ = 2;
          this->cframe_ = 0;
          this->fbcount_ = 0;
          this->buffer_.clear();
        } else {
          this->state_ = 0;
          ESP_LOGD(TAG, "P1 empty message");
        }
      } else if (this->buffer_.size() > 15) {
        ESP_LOGW(TAG, "Unknown protocol 0x%02x, discarding", this->protocol_);
        this->state_ = 0;
      }
      continue;
    }

    if (this->state_ == 2) {
      this->fbytes_[this->fbcount_++] = c;
      if (this->fbcount_ < 6)
        continue;
      this->fbcount_ = 0;
      if (!checksum(this->fbytes_, 0, 6)) {
        ESP_LOGE(TAG, "frame checksum failed");
        continue;
      }
      septet_spread(this->fbytes_, 0, 4, this->fbytes_[4]);
      for (int i = 0; i < 4; i++)
        this->buffer_.push_back(this->fbytes_[i]);
      if (++this->cframe_ < this->frames_)
        continue;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
      char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)];
      size_t log_bytes = std::min(this->buffer_.size(), static_cast<size_t>(VBUS_MAX_LOG_BYTES));
#endif
      ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_,
               format_hex_to(hex_buf, this->buffer_.data(), log_bytes));
      for (auto &listener : this->listeners_)
        listener->on_message(this->command_, this->source_, this->dest_, this->buffer_);
      this->state_ = 0;
      continue;
    }
  }
}

void VBusListener::on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector<uint8_t> &message) {
  if ((this->command_ != 0xffff) && (this->command_ != command))
    return;
  if ((this->source_ != 0xffff) && (this->source_ != source))
    return;
  if ((this->dest_ != 0xffff) && (this->dest_ != dest))
    return;
  this->handle_message(message);
}

}  // namespace esphome::vbus
