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

namespace esphome {
namespace hydreon_rgxx {

static const char *const TAG = "hydreon_rgxx.sensor";
static const int MAX_DATA_LENGTH_BYTES = 80;
static const uint8_t ASCII_LF = 0x0A;
#define HYDREON_RGXX_COMMA ,
static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)};

void HydreonRGxxComponent::dump_config() {
  this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
  ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
  if (this->is_failed()) {
    ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
  }
  LOG_UPDATE_INTERVAL(this);

  int i = 0;
#define HYDREON_RGXX_LOG_SENSOR(s) \
  if (this->sensors_[i++] != nullptr) { \
    LOG_SENSOR("  ", #s, this->sensors_[i - 1]); \
  }
  HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, );
}

void HydreonRGxxComponent::setup() {
  ESP_LOGCONFIG(TAG, "Setting up hydreon_rgxx...");
  while (this->available() != 0) {
    this->read();
  }
  this->schedule_reboot_();
}

bool HydreonRGxxComponent::sensor_missing_() {
  if (this->sensors_received_ == -1) {
    // no request sent yet, don't check
    return false;
  } else {
    if (this->sensors_received_ == 0) {
      ESP_LOGW(TAG, "No data at all");
      return true;
    }
    for (int i = 0; i < NUM_SENSORS; i++) {
      if (this->sensors_[i] == nullptr) {
        continue;
      }
      if ((this->sensors_received_ >> i & 1) == 0) {
        ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]);
        return true;
      }
    }
    return false;
  }
}

void HydreonRGxxComponent::update() {
  if (this->boot_count_ > 0) {
    if (this->sensor_missing_()) {
      this->no_response_count_++;
      ESP_LOGE(TAG, "data missing %d times", this->no_response_count_);
      if (this->no_response_count_ > 15) {
        ESP_LOGE(TAG, "asking sensor to reboot");
        for (auto &sensor : this->sensors_) {
          if (sensor != nullptr) {
            sensor->publish_state(NAN);
          }
        }
        this->schedule_reboot_();
        return;
      }
    } else {
      this->no_response_count_ = 0;
    }
    this->write_str("R\n");
#ifdef USE_BINARY_SENSOR
    if (this->too_cold_sensor_ != nullptr) {
      this->too_cold_sensor_->publish_state(this->too_cold_);
    }
#endif
    this->too_cold_ = false;
    this->sensors_received_ = 0;
  }
}

void HydreonRGxxComponent::loop() {
  uint8_t data;
  while (this->available() > 0) {
    if (this->read_byte(&data)) {
      buffer_ += (char) data;
      if (this->buffer_.back() == static_cast<char>(ASCII_LF) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) {
        // complete line received
        this->process_line_();
        this->buffer_.clear();
      }
    }
  }
}

/**
 * Communication with the sensor is asynchronous.
 * We send requests and let esphome continue doing its thing.
 * Once we have received a complete line, we process it.
 *
 * Catching communication failures is done in two layers:
 *
 * 1. We check if all requested data has been received
 *    before we send out the next request. If data keeps
 *    missing, we escalate.
 * 2. Request the sensor to reboot. We retry based on
 *    a timeout. If the sensor does not respond after
 *    several boot attempts, we give up.
 */
void HydreonRGxxComponent::schedule_reboot_() {
  this->boot_count_ = 0;
  this->set_interval("reboot", 5000, [this]() {
    if (this->boot_count_ < 0) {
      ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_);
    }
    this->boot_count_--;
    this->write_str("K\n");
    if (this->boot_count_ < -5) {
      ESP_LOGE(TAG, "hydreon_rgxx can't boot, giving up");
      for (auto &sensor : this->sensors_) {
        if (sensor != nullptr) {
          sensor->publish_state(NAN);
        }
      }
      this->mark_failed();
    }
  });
}

bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) {
  return this->buffer_starts_with_(prefix.c_str());
}

bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; }

void HydreonRGxxComponent::process_line_() {
  ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());

  if (buffer_[0] == ';') {
    ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
    return;
  }
  if (this->buffer_starts_with_("PwrDays")) {
    if (this->boot_count_ <= 0) {
      this->boot_count_ = 1;
    } else {
      this->boot_count_++;
    }
    this->cancel_interval("reboot");
    this->no_response_count_ = 0;
    ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
    this->write_str("P\nH\nM\n");  // set sensor to polling mode, high res mode, metric mode
    return;
  }
  if (this->buffer_starts_with_("SW")) {
    std::string::size_type majend = this->buffer_.find('.');
    std::string::size_type endversion = this->buffer_.find(' ', 3);
    if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) {
      ESP_LOGW(TAG, "invalid version string: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
    }
    int major = strtol(this->buffer_.substr(3, majend - 3).c_str(), nullptr, 10);
    int minor = strtol(this->buffer_.substr(majend + 1, endversion - (majend + 1)).c_str(), nullptr, 10);

    if (major > 10 || minor >= 1000 || minor < 0 || major < 0) {
      ESP_LOGW(TAG, "invalid version: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
    }
    this->sw_version_ = major * 1000 + minor;
    ESP_LOGI(TAG, "detected sw version %i", this->sw_version_);
    return;
  }
  bool is_data_line = false;
  for (int i = 0; i < NUM_SENSORS; i++) {
    if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) {
      is_data_line = true;
      break;
    }
  }
  if (is_data_line) {
    std::string::size_type tc = this->buffer_.find("TooCold");
    this->too_cold_ |= tc != std::string::npos;
    if (this->too_cold_) {
      ESP_LOGD(TAG, "Received TooCold");
    }
    for (int i = 0; i < NUM_SENSORS; i++) {
      if (this->sensors_[i] == nullptr) {
        continue;
      }
      std::string::size_type n = this->buffer_.find(PROTOCOL_NAMES[i]);
      if (n == std::string::npos) {
        continue;
      }
      float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr);
      this->sensors_[i]->publish_state(data);
      ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
      this->sensors_received_ |= (1 << i);
    }
  } else {
    ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str());
  }
}

float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; }

}  // namespace hydreon_rgxx
}  // namespace esphome
