/// @file weikai.cpp
/// @brief  WeiKai component family - classes implementation
/// @date Last Modified: 2024/04/06 15:13:11
/// @details The classes declared in this file can be used by the Weikai family

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

namespace esphome::weikai {

static const char *const TAG = "weikai";

/// @brief measure the time elapsed between two calls
/// @param last_time time of the previous call
/// @return the elapsed time in milliseconds
uint32_t elapsed_ms(uint32_t &last_time) {
  uint32_t e = millis() - last_time;
  last_time = millis();
  return e;
};

/// @brief Converts the parity enum value to a C string
/// @param parity enum
/// @return the string
const char *p2s(uart::UARTParityOptions parity) {
  using namespace uart;
  switch (parity) {
    case UART_CONFIG_PARITY_NONE:
      return "NONE";
    case UART_CONFIG_PARITY_EVEN:
      return "EVEN";
    case UART_CONFIG_PARITY_ODD:
      return "ODD";
    default:
      return "UNKNOWN";
  }
}

/// @brief Display a buffer in hexadecimal format (32 hex values / line) for debug
void print_buffer(const uint8_t *data, size_t length) {
  char hex_buffer[100];
  hex_buffer[(3 * 32) + 1] = 0;
  for (size_t i = 0; i < length; i++) {
    snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
    if (i % 32 == 31) {
      ESP_LOGVV(TAG, "   %s", hex_buffer);
    }
  }
  if (length % 32) {
    // null terminate if incomplete line
    hex_buffer[3 * (length % 32) + 2] = 0;
    ESP_LOGVV(TAG, "   %s", hex_buffer);
  }
}

static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST",  "GMUT",  "SPAGE", "SCR", "LCR",  "FCR",  "SIER",
                                              "SIFR", "TFCNT", "RFCNT", "FSR",   "LSR", "FDAT", "FWCR", "RS485"};
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
                                              "TFTL", "FWTH", "FWTL", "XON1",  "XOFF1", "SADR",  "SAEN", "RTSDLY"};

// method to print a register value as text: used in the log messages ...
const char *reg_to_str(int reg, bool page1) {
  if (reg == WKREG_GPDAT) {
    return "GPDAT";
  } else if (reg == WKREG_GPDIR) {
    return "GPDIR";
  } else {
    return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
  }
}

enum RegType { REG = 0, FIFO = 1 };  ///< Register or FIFO

///////////////////////////////////////////////////////////////////////////////
// The WeikaiRegister methods
///////////////////////////////////////////////////////////////////////////////
WeikaiRegister &WeikaiRegister::operator=(uint8_t value) {
  write_reg(value);
  return *this;
}

WeikaiRegister &WeikaiRegister::operator&=(uint8_t value) {
  value &= read_reg();
  write_reg(value);
  return *this;
}

WeikaiRegister &WeikaiRegister::operator|=(uint8_t value) {
  value |= read_reg();
  write_reg(value);
  return *this;
}

///////////////////////////////////////////////////////////////////////////////
// The WeikaiComponent methods
///////////////////////////////////////////////////////////////////////////////
void WeikaiComponent::loop() {
  if (!this->is_in_loop_state())
    return;

  // If there are some bytes in the receive FIFO we transfers them to the ring buffers
  size_t transferred = 0;
  for (auto *child : this->children_) {
    // we look if some characters has been received in the fifo
    transferred += child->xfer_fifo_to_buffer_();
  }
  if (transferred > 0) {
    ESP_LOGV(TAG, "transferred %d bytes from fifo to buffer", transferred);
  }

#ifdef TEST_COMPONENT
  static uint32_t loop_time = 0;
  static uint32_t loop_count = 0;
  uint32_t time = 0;

  if (test_mode_ == 1) {  // test component in loopback
    ESP_LOGI(TAG, "Component loop %" PRIu32 " for %s : %" PRIu32 " ms since last call", loop_count++, this->get_name(),
             millis() - loop_time);
    loop_time = millis();
    char message[64];
    elapsed_ms(time);  // set time to now
    for (int i = 0; i < this->children_.size(); i++) {
      if (i != ((loop_count - 1) % this->children_.size()))  // we do only one per loop
        continue;
      snprintf(message, sizeof(message), "%s:%s", this->get_name(), children_[i]->get_channel_name());
      children_[i]->uart_send_test_(message);
      uint32_t const start_time = millis();
      while (children_[i]->tx_fifo_is_not_empty_()) {  // wait until buffer empty
        if (millis() - start_time > 1500) {
          ESP_LOGE(TAG, "timeout while flushing - %d bytes left in buffer", children_[i]->tx_in_fifo_());
          break;
        }
        yield();  // reschedule our thread to avoid blocking
      }
      bool status = children_[i]->uart_receive_test_(message);
      ESP_LOGI(TAG, "Test %s => send/received %u bytes %s - execution time %" PRIu32 " ms", message, RING_BUFFER_SIZE,
               status ? "correctly" : "with error", elapsed_ms(time));
    }
  }

  if (this->test_mode_ == 2) {  // test component in echo mode
    for (auto *child : this->children_) {
      uint8_t data = 0;
      if (child->available()) {
        child->read_byte(&data);
        ESP_LOGI(TAG, "echo mode: read -> send %02X", data);
        child->write_byte(data);
      }
    }
  }
  if (test_mode_ == 3) {
    test_gpio_input_();
  }

  if (test_mode_ == 4) {
    test_gpio_output_();
  }
#endif
}

#if defined(TEST_COMPONENT)
void WeikaiComponent::test_gpio_input_() {
  static bool init_input{false};
  static uint8_t state{0};
  uint8_t value;
  char bin_buf[9];  // 8 binary digits + null
  if (!init_input) {
    init_input = true;
    // set all pins in input mode
    this->reg(WKREG_GPDIR, 0) = 0x00;
    ESP_LOGI(TAG, "initializing all pins to input mode");
    state = this->reg(WKREG_GPDAT, 0);
    ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, format_bin_to(bin_buf, state));
  }
  value = this->reg(WKREG_GPDAT, 0);
  if (value != state) {
    ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, format_bin_to(bin_buf, value));
    state = value;
  }
}

void WeikaiComponent::test_gpio_output_() {
  static bool init_output{false};
  static uint8_t state{0};
  char bin_buf[9];  // 8 binary digits + null
  if (!init_output) {
    init_output = true;
    // set all pins in output mode
    this->reg(WKREG_GPDIR, 0) = 0xFF;
    ESP_LOGI(TAG, "initializing all pins to output mode");
    this->reg(WKREG_GPDAT, 0) = state;
    ESP_LOGI(TAG, "setting all outputs to 0");
  }
  state = ~state;
  this->reg(WKREG_GPDAT, 0) = state;
  ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, format_bin_to(bin_buf, state));
  delay(100);  // NOLINT
}
#endif

///////////////////////////////////////////////////////////////////////////////
// The WeikaiGPIOPin methods
///////////////////////////////////////////////////////////////////////////////
bool WeikaiComponent::read_pin_val_(uint8_t pin) {
  this->input_state_ = this->reg(WKREG_GPDAT, 0);
  char bin_buf[9];
  ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin),
            format_bin_to(bin_buf, this->input_state_));
  return this->input_state_ & (1 << pin);
}

void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
  if (value) {
    this->output_state_ |= (1 << pin);
  } else {
    this->output_state_ &= ~(1 << pin);
  }
  char bin_buf[9];
  ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
            format_bin_to(bin_buf, this->output_state_));
  this->reg(WKREG_GPDAT, 0) = this->output_state_;
}

void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) {
  if (flags == gpio::FLAG_INPUT) {
    this->pin_config_ &= ~(1 << pin);  // clear bit (input mode)
  } else {
    if (flags == gpio::FLAG_OUTPUT) {
      this->pin_config_ |= 1 << pin;  // set bit (output mode)
    } else {
      ESP_LOGE(TAG, "pin %d direction invalid", pin);
    }
  }
  char bin_buf[9];
  ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, format_bin_to(bin_buf, this->pin_config_));
  this->reg(WKREG_GPDIR, 0) = this->pin_config_;  // TODO check ~
}

void WeikaiGPIOPin::setup() {
  ESP_LOGCONFIG(TAG, "Setting GPIO pin %d mode to %s", this->pin_,
                flags_ == gpio::FLAG_INPUT          ? "Input"
                : this->flags_ == gpio::FLAG_OUTPUT ? "Output"
                                                    : "NOT SPECIFIED");
  this->pin_mode(this->flags_);
}

size_t WeikaiGPIOPin::dump_summary(char *buffer, size_t len) const {
  return buf_append_printf(buffer, len, 0, "%u via WeiKai %s", this->pin_, this->parent_->get_name());
}

///////////////////////////////////////////////////////////////////////////////
// The WeikaiChannel methods
///////////////////////////////////////////////////////////////////////////////
void WeikaiChannel::setup_channel() {
  ESP_LOGCONFIG(TAG, "  Setting up UART %s:%s", this->parent_->get_name(), this->get_channel_name());
  // we enable transmit and receive on this channel
  if (this->check_channel_down()) {
    ESP_LOGCONFIG(TAG, "  Error channel %s not working", this->get_channel_name());
  }
  this->reset_fifo_();
  this->receive_buffer_.clear();
  this->set_line_param_();
  this->set_baudrate_();
}

void WeikaiChannel::dump_channel() {
  ESP_LOGCONFIG(TAG,
                "  UART %s\n"
                "    Baud rate: %" PRIu32 " Bd\n"
                "    Data bits: %u\n"
                "    Stop bits: %u\n"
                "    Parity: %s",
                this->get_channel_name(), this->baud_rate_, this->data_bits_, this->stop_bits_, p2s(this->parity_));
}

void WeikaiChannel::reset_fifo_() {
  // enable transmission and reception
  this->reg(WKREG_SCR) = SCR_RXEN | SCR_TXEN;
  // we reset and enable transmit and receive FIFO
  this->reg(WKREG_FCR) = FCR_TFEN | FCR_RFEN | FCR_TFRST | FCR_RFRST;
}

void WeikaiChannel::set_line_param_() {
  this->data_bits_ = 8;  // always equal to 8 for WeiKai (cant be changed)
  uint8_t lcr = 0;
  if (this->stop_bits_ == 2)
    lcr |= LCR_STPL;
  switch (this->parity_) {  // parity selection settings
    case uart::UART_CONFIG_PARITY_ODD:
      lcr |= (LCR_PAEN | LCR_PAR_ODD);
      break;
    case uart::UART_CONFIG_PARITY_EVEN:
      lcr |= (LCR_PAEN | LCR_PAR_EVEN);
      break;
    default:
      break;  // no parity 000x
  }
  this->reg(WKREG_LCR) = lcr;  // write LCR
  char bin_buf[9];
  ESP_LOGV(TAG, "    line config: %d data_bits, %d stop_bits, parity %s register [%s]", this->data_bits_,
           this->stop_bits_, p2s(this->parity_), format_bin_to(bin_buf, lcr));
}

void WeikaiChannel::set_baudrate_() {
  if (this->baud_rate_ > this->parent_->crystal_ / 16) {
    baud_rate_ = this->parent_->crystal_ / 16;
    ESP_LOGE(TAG, " Requested baudrate too high for crystal=%" PRIu32 " Hz. Has been reduced to %" PRIu32 " Bd",
             this->parent_->crystal_, this->baud_rate_);
  };
  uint16_t const val_int = this->parent_->crystal_ / (this->baud_rate_ * 16) - 1;
  uint16_t val_dec = (this->parent_->crystal_ % (this->baud_rate_ * 16)) / (this->baud_rate_ * 16);
  uint8_t const baud_high = (uint8_t) (val_int >> 8);
  uint8_t const baud_low = (uint8_t) (val_int & 0xFF);
  while (val_dec > 0x0A)
    val_dec /= 0x0A;
  uint8_t const baud_dec = (uint8_t) (val_dec);

  this->parent_->page1_ = true;  // switch to page 1
  this->reg(WKREG_SPAGE) = 1;
  this->reg(WKREG_BRH) = baud_high;
  this->reg(WKREG_BRL) = baud_low;
  this->reg(WKREG_BRD) = baud_dec;
  this->parent_->page1_ = false;  // switch back to page 0
  this->reg(WKREG_SPAGE) = 0;

  ESP_LOGV(TAG, "    Crystal=%" PRId32 " baudrate=%" PRId32 " => registers [%d %d %d]", this->parent_->crystal_,
           this->baud_rate_, baud_high, baud_low, baud_dec);
}

inline bool WeikaiChannel::tx_fifo_is_not_empty_() { return this->reg(WKREG_FSR) & FSR_TFDAT; }

size_t WeikaiChannel::tx_in_fifo_() {
  size_t tfcnt = this->reg(WKREG_TFCNT);
  if (tfcnt == 0) {
    uint8_t const fsr = this->reg(WKREG_FSR);
    if (fsr & FSR_TFFULL) {
      char bin_buf[9];
      ESP_LOGVV(TAG, "tx FIFO full FSR=%s", format_bin_to(bin_buf, fsr));
      tfcnt = FIFO_SIZE;
    }
  }
  ESP_LOGVV(TAG, "tx FIFO contains %d bytes", tfcnt);
  return tfcnt;
}

size_t WeikaiChannel::rx_in_fifo_() {
  size_t available = this->reg(WKREG_RFCNT);
  uint8_t const fsr = this->reg(WKREG_FSR);
  if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) {
    char bin_buf[9];
    if (fsr & FSR_RFOE)
      ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr));
    if (fsr & FSR_RFLB)
      ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr));
    if (fsr & FSR_RFFE)
      ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr));
    if (fsr & FSR_RFPE)
      ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr));
  }
  if ((available == 0) && (fsr & FSR_RFDAT)) {
    // here we should be very careful because we can have something like this:
    // -  at time t0 we read RFCNT=0 because nothing yet received
    // -  at time t0+delta we might read FIFO not empty because one byte has just been received
    // -  so to be sure we need to do another read of RFCNT and if it is still zero -> buffer full
    available = this->reg(WKREG_RFCNT);
    if (available == 0) {  // still zero ?
      char bin_buf[9];
      ESP_LOGV(TAG, "rx FIFO is full FSR=%s", format_bin_to(bin_buf, fsr));
      available = FIFO_SIZE;
    }
  }
  char bin_buf2[9];
  ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, format_bin_to(bin_buf2, fsr));
  return available;
}

bool WeikaiChannel::check_channel_down() {
  // to check if we channel is up we write to the LCR W/R register
  // note that this will put a break on the tx line for few ms
  WeikaiRegister &lcr = this->reg(WKREG_LCR);
  lcr = 0x3F;
  uint8_t val = lcr;
  if (val != 0x3F) {
    ESP_LOGE(TAG, "R/W of register failed expected 0x3F received 0x%02X", val);
    return true;
  }
  lcr = 0;
  val = lcr;
  if (val != 0x00) {
    ESP_LOGE(TAG, "R/W of register failed expected 0x00 received 0x%02X", val);
    return true;
  }
  return false;
}

bool WeikaiChannel::peek_byte(uint8_t *buffer) {
  auto available = this->receive_buffer_.count();
  if (!available)
    xfer_fifo_to_buffer_();
  return this->receive_buffer_.peek(*buffer);
}

size_t WeikaiChannel::available() {
  size_t available = this->receive_buffer_.count();
  if (!available)
    available = xfer_fifo_to_buffer_();
  return available;
}

bool WeikaiChannel::read_array(uint8_t *buffer, size_t length) {
  bool status = true;
  auto available = this->receive_buffer_.count();
  if (length > available) {
    ESP_LOGW(TAG, "read_array: buffer underflow requested %d bytes only %d bytes available", length, available);
    length = available;
    status = false;
  }
  // retrieve the bytes from ring buffer
  for (size_t i = 0; i < length; i++) {
    this->receive_buffer_.pop(buffer[i]);
  }
  ESP_LOGVV(TAG, "read_array(ch=%d buffer[0]=%02X, length=%d): status %s", this->channel_, *buffer, length,
            status ? "OK" : "ERROR");
  return status;
}

void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) {
  if (length > XFER_MAX_SIZE) {
    ESP_LOGE(TAG, "Write_array: invalid call - requested %d bytes but max size %d", length, XFER_MAX_SIZE);
    length = XFER_MAX_SIZE;
  }
  this->reg(0).write_fifo(const_cast<uint8_t *>(buffer), length);
}

uart::UARTFlushResult WeikaiChannel::flush() {
  uint32_t const start_time = millis();
  while (this->tx_fifo_is_not_empty_()) {  // wait until buffer empty
    if (millis() - start_time > 200) {
      ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_());
      return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
    }
    yield();  // reschedule our thread to avoid blocking
  }
  return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
}

size_t WeikaiChannel::xfer_fifo_to_buffer_() {
  size_t total = 0;
  size_t to_transfer;
  size_t free;
  while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) {
    // while bytes in fifo and some room in the buffer we transfer
    if (to_transfer > XFER_MAX_SIZE)
      to_transfer = XFER_MAX_SIZE;  // we can only do so much
    if (to_transfer > free)
      to_transfer = free;  // we'll do the rest next time
    if (to_transfer) {
      uint8_t data[to_transfer];
      this->reg(0).read_fifo(data, to_transfer);
      for (size_t i = 0; i < to_transfer; i++)
        this->receive_buffer_.push(data[i]);
      total += to_transfer;
    }
  }  // while work to do
  return total;
}

///
// TEST COMPONENT
//
#ifdef TEST_COMPONENT
/// @addtogroup test_ Test component information
/// @{

/// @brief An increment "Functor" (i.e. a class object that acts like a method with state!)
///
/// Functors are objects that can be treated as though they are a function or function pointer.
class Increment {
 public:
  /// @brief constructor: initialize current value to 0
  Increment() : i_(0) {}
  /// @brief overload of the parenthesis operator.
  /// Returns the current value and auto increment it
  /// @return the current value.
  uint8_t operator()() { return i_++; }

 private:
  uint8_t i_;
};

/// @brief Hex converter to print/display a buffer in hexadecimal format (32 hex values / line).
/// @param buffer contains the values to display
void print_buffer(std::vector<uint8_t> buffer) {
  char hex_buffer[100];
  hex_buffer[(3 * 32) + 1] = 0;
  for (size_t i = 0; i < buffer.size(); i++) {
    snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]);
    if (i % 32 == 31)
      ESP_LOGI(TAG, "   %s", hex_buffer);
  }
  if (buffer.size() % 32) {
    // null terminate if incomplete line
    hex_buffer[3 * (buffer.size() % 32) + 1] = 0;
    ESP_LOGI(TAG, "   %s", hex_buffer);
  }
}

/// @brief test the write_array method
void WeikaiChannel::uart_send_test_(char *message) {
  auto start_exec = micros();
  std::vector<uint8_t> output_buffer(XFER_MAX_SIZE);
  generate(output_buffer.begin(), output_buffer.end(), Increment());  // fill with incrementing number
  size_t to_send = RING_BUFFER_SIZE;
  while (to_send) {
    this->write_array(&output_buffer[0], XFER_MAX_SIZE);  // we send the buffer
    this->flush();
    to_send -= XFER_MAX_SIZE;
  }
  ESP_LOGV(TAG, "%s => sent %d bytes - exec time %d µs", message, RING_BUFFER_SIZE, micros() - start_exec);
}

/// @brief test read_array method
bool WeikaiChannel::uart_receive_test_(char *message) {
  auto start_exec = micros();
  bool status = true;
  size_t received = 0;
  std::vector<uint8_t> buffer(RING_BUFFER_SIZE);

  // we wait until we have received all the bytes
  uint32_t const start_time = millis();
  status = true;
  while (received < RING_BUFFER_SIZE) {
    while (XFER_MAX_SIZE > this->available()) {
      this->xfer_fifo_to_buffer_();
      if (millis() - start_time > 1500) {
        ESP_LOGE(TAG, "uart_receive_test_() timeout: only %d bytes received", this->available());
        break;
      }
      yield();  // reschedule our thread to avoid blocking
    }
    status = this->read_array(&buffer[received], XFER_MAX_SIZE) && status;
    received += XFER_MAX_SIZE;
  }

  uint8_t peek_value = 0;
  this->peek_byte(&peek_value);
  if (peek_value != 0) {
    ESP_LOGE(TAG, "Peek first byte value error");
    status = false;
  }

  for (size_t i = 0; i < RING_BUFFER_SIZE; i++) {
    if (buffer[i] != i % XFER_MAX_SIZE) {
      ESP_LOGE(TAG, "Read buffer contains error b=%x i=%x", buffer[i], i % XFER_MAX_SIZE);
      print_buffer(buffer);
      status = false;
      break;
    }
  }

  ESP_LOGV(TAG, "%s => received %d bytes  status %s - exec time %d µs", message, received, status ? "OK" : "ERROR",
           micros() - start_exec);
  return status;
}

/// @}
#endif

}  // namespace esphome::weikai
