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

namespace esphome {
namespace icnt86x {

static const char *const TAG = "icnt86x.touchscreen";

static const uint16_t REG_VERSION   = 0x000A;
static const uint16_t REG_TOUCH_CNT = 0x1001;
static const uint16_t REG_TOUCH_DAT = 0x1002;

static const uint8_t MAX_TOUCHES = 2;
static const uint8_t BYTES_PER_TOUCH = 7;

// Observed event codes from protocol analysis
static const uint8_t EVENT_MOVE = 0x02;
static const uint8_t EVENT_DOWN = 0x03;
// 0x04 = idle/no-contact — chip reports count=1 with this code when untouched

void ICNT86XTouchscreen::setup() {
  if (this->reset_pin_ != nullptr) {
    this->reset_pin_->setup();
    // Drive INT low before and during reset release. Many touch controllers
    // sample INT at reset release to select interrupt output mode; if INT is
    // left pulled high by the level shifter the chip comes up with INT disabled.
    if (this->interrupt_pin_ != nullptr) {
      this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
      this->interrupt_pin_->digital_write(false);
    }
    this->reset_pin_->digital_write(true);
    delay(100);
    this->reset_pin_->digital_write(false);
    delay(100);
    this->reset_pin_->digital_write(true);  // INT still driven low at release
  }

  // Allow chip 100ms to boot, then reconfigure INT as input before attaching ISR
  this->set_timeout(100, [this]() { this->setup_internal_(); });
}

void ICNT86XTouchscreen::setup_internal_() {
  if (this->interrupt_pin_ != nullptr) {
    this->interrupt_pin_->setup();  // reconfigure as input after driving low during reset
  }

  uint8_t ver[4];
  if (!this->read_reg16_(REG_VERSION, ver, sizeof(ver))) {
    this->mark_failed();
    ESP_LOGE(TAG, "Failed to communicate with ICNT86X at 0x%02X", this->address_);
    return;
  }

  ESP_LOGI(TAG, "ICNT86X IC version: %02X%02X, FW version: %02X%02X",
           ver[0], ver[1], ver[2], ver[3]);

  if (this->interrupt_pin_ != nullptr) {
    this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
  }

  this->setup_done_ = true;
}

void ICNT86XTouchscreen::update_touches() {
  // In polling mode loop() never runs, so is_touched_ is never reset between
  // cycles. Reset it here so send_touches_() can detect the press→release
  // transition via !is_touched_ && was_touched_.
  this->is_touched_ = false;

  if (!this->setup_done_) {
    this->skip_update_ = true;
    return;
  }

  uint8_t count = 0;
  if (!this->read_reg16_(REG_TOUCH_CNT, &count, 1)) {
    this->status_set_warning();
    this->skip_update_ = true;
    return;
  }

  this->write_reg16_(REG_TOUCH_CNT, 0x00);

  // Always allow send_touches_() to run — even with zero touches it needs to
  // fire to propagate the release event when !is_touched_ && was_touched_.
  this->skip_update_ = false;

  if (count == 0 || count > MAX_TOUCHES)
    return;

  uint8_t data[MAX_TOUCHES * BYTES_PER_TOUCH];
  if (!this->read_reg16_(REG_TOUCH_DAT, data, count * BYTES_PER_TOUCH)) {
    this->status_set_warning();
    this->skip_update_ = true;
    return;
  }

  for (uint8_t i = 0; i < count; i++) {
    // Per-point layout (7 bytes): [id, x_lo, x_hi, y_lo, y_hi, pressure, event]
    const uint8_t *p = &data[i * BYTES_PER_TOUCH];
    uint8_t event = p[6];
    if (event != EVENT_DOWN && event != EVENT_MOVE)
      continue;
    uint8_t id = p[0];
    int16_t x  = ((uint16_t)p[2] << 8) | p[1];
    int16_t y  = ((uint16_t)p[4] << 8) | p[3];
    this->add_raw_touch_position_(id, x, y);
  }
}

bool ICNT86XTouchscreen::read_reg16_(uint16_t reg, uint8_t *data, size_t len) {
  uint8_t reg_buf[2] = {(uint8_t)(reg >> 8), (uint8_t)(reg & 0xFF)};
  if (this->write(reg_buf, 2) != i2c::ERROR_OK)
    return false;
  return this->read(data, len) == i2c::ERROR_OK;
}

bool ICNT86XTouchscreen::write_reg16_(uint16_t reg, uint8_t value) {
  uint8_t buf[3] = {(uint8_t)(reg >> 8), (uint8_t)(reg & 0xFF), value};
  return this->write(buf, 3) == i2c::ERROR_OK;
}

void ICNT86XTouchscreen::dump_config() {
  ESP_LOGCONFIG(TAG, "ICNT86X Touchscreen:");
  LOG_I2C_DEVICE(this);
  LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_);
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
}

}  // namespace icnt86x
}  // namespace esphome
