#include "micronova.h"
#include "esphome/core/log.h"

namespace esphome::micronova {

static const int STOVE_REPLY_DELAY = 60;
static const uint8_t WRITE_BIT = 1 << 7;  // 0x80

void MicroNovaBaseListener::dump_base_config() {
  ESP_LOGCONFIG(TAG,
                "  Memory Location: %02X\n"
                "  Memory Address: %02X",
                this->memory_location_, this->memory_address_);
}

void MicroNovaListener::dump_base_config() {
  MicroNovaBaseListener::dump_base_config();
  LOG_UPDATE_INTERVAL(this);
}

void MicroNova::setup() {
  if (this->enable_rx_pin_ != nullptr) {
    this->enable_rx_pin_->setup();
    this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT);
    this->enable_rx_pin_->digital_write(false);
  }
  this->current_transmission_.request_transmission_time = millis();
  this->current_transmission_.memory_location = 0;
  this->current_transmission_.memory_address = 0;
  this->current_transmission_.reply_pending = false;
  this->current_transmission_.initiating_listener = nullptr;
}

void MicroNova::dump_config() {
  ESP_LOGCONFIG(TAG, "MicroNova:");
  if (this->enable_rx_pin_ != nullptr) {
    LOG_PIN("  Enable RX Pin: ", this->enable_rx_pin_);
  }
}

void MicroNova::request_update_listeners() {
  ESP_LOGD(TAG, "Schedule listener update");
  for (auto &mv_listener : this->micronova_listeners_) {
    mv_listener->set_needs_update(true);
  }
}

void MicroNova::loop() {
  // Only read one sensor that needs update per loop
  // If STOVE_REPLY_DELAY time has passed since last loop()
  // check for a reply from the stove
  if ((this->current_transmission_.reply_pending) &&
      (millis() - this->current_transmission_.request_transmission_time > STOVE_REPLY_DELAY)) {
    int stove_reply_value = this->read_stove_reply();
    if (this->current_transmission_.initiating_listener != nullptr) {
      this->current_transmission_.initiating_listener->process_value_from_stove(stove_reply_value);
      this->current_transmission_.initiating_listener = nullptr;
    }
    this->current_transmission_.reply_pending = false;
    return;
  } else if (!this->current_transmission_.reply_pending) {
    for (auto &mv_listener : this->micronova_listeners_) {
      if (mv_listener->get_needs_update()) {
        mv_listener->set_needs_update(false);
        this->current_transmission_.initiating_listener = mv_listener;
        mv_listener->request_value_from_stove();
        return;
      }
    }
  }
}

void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaListener *listener) {
  uint8_t write_data[2] = {0, 0};
  uint8_t trash_rx;

  if (this->reply_pending_mutex_.try_lock()) {
    // clear rx buffer.
    // Stove hickups may cause late replies in the rx
    while (this->available()) {
      this->read_byte(&trash_rx);
      ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx);
    }

    write_data[0] = location;
    write_data[1] = address;
    ESP_LOGV(TAG, "Request from stove [%02X,%02X]", write_data[0], write_data[1]);

    this->enable_rx_pin_->digital_write(true);
    this->write_array(write_data, 2);
    this->flush();
    this->enable_rx_pin_->digital_write(false);

    this->current_transmission_.request_transmission_time = millis();
    this->current_transmission_.memory_location = location;
    this->current_transmission_.memory_address = address;
    this->current_transmission_.reply_pending = true;
    this->current_transmission_.initiating_listener = listener;
  } else {
    ESP_LOGE(TAG, "Reply is pending, skipping read request");
  }
}

int MicroNova::read_stove_reply() {
  uint8_t reply_data[2] = {0, 0};
  uint8_t checksum = 0;

  // assert enable_rx_pin is false
  this->read_array(reply_data, 2);

  this->reply_pending_mutex_.unlock();
  ESP_LOGV(TAG, "Reply from stove [%02X,%02X]", reply_data[0], reply_data[1]);

  checksum = ((uint16_t) this->current_transmission_.memory_location +
              (uint16_t) this->current_transmission_.memory_address + (uint16_t) reply_data[1]) &
             0xFF;
  if (reply_data[0] != checksum) {
    ESP_LOGE(TAG, "Checksum missmatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X",
             this->current_transmission_.memory_location, this->current_transmission_.memory_address, reply_data[0],
             reply_data[1], checksum, reply_data[0]);
    return -1;
  }
  return ((int) reply_data[1]);
}

void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) {
  uint8_t write_data[4] = {0, 0, 0, 0};
  uint16_t checksum = 0;

  if (this->reply_pending_mutex_.try_lock()) {
    uint8_t write_location = location | WRITE_BIT;
    write_data[0] = write_location;
    write_data[1] = address;
    write_data[2] = data;

    checksum = ((uint16_t) write_data[0] + (uint16_t) write_data[1] + (uint16_t) write_data[2]) & 0xFF;
    write_data[3] = checksum;

    ESP_LOGV(TAG, "Write 4 bytes [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], write_data[3]);

    this->enable_rx_pin_->digital_write(true);
    this->write_array(write_data, 4);
    this->flush();
    this->enable_rx_pin_->digital_write(false);

    this->current_transmission_.request_transmission_time = millis();
    this->current_transmission_.memory_location = write_location;
    this->current_transmission_.memory_address = address;
    this->current_transmission_.reply_pending = true;
    this->current_transmission_.initiating_listener = nullptr;
  } else {
    ESP_LOGE(TAG, "Reply is pending, skipping write");
  }
}

}  // namespace esphome::micronova
