#include "xpt2046.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

#include <algorithm>

namespace esphome::xpt2046 {

static const char *const TAG = "xpt2046";

void XPT2046Component::setup() {
  if (this->irq_pin_ != nullptr) {
    // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state
    // while the channels are read and wiring it as an interrupt is not straightforward and would
    // need careful masking. A GPIO poll is cheap so we'll just use that.

    this->irq_pin_->setup();  // INPUT
    this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
    this->irq_pin_->setup();
    this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE);
  }
  this->spi_setup();
  this->read_adc_(0xD0);  // ADC powerdown, enable PENIRQ pin
}

void XPT2046Component::update_touches() {
  int16_t data[6], x_raw, y_raw, z_raw;
  bool touch = false;

  enable();

  int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */);
  int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */);
  z_raw = touch_pressure_1 + 0xfff - touch_pressure_2;
  ESP_LOGVV(TAG, "Touchscreen Update z = %d", z_raw);
  touch = (z_raw >= this->threshold_);
  if (touch) {
    read_adc_(0xD1 /* X */);  // dummy Y measure, 1st is always noisy
    data[0] = this->read_adc_(0x91 /* Y */);
    data[1] = this->read_adc_(0xD1 /* X */);  // make 3 x-y measurements
    data[2] = this->read_adc_(0x91 /* Y */);
    data[3] = this->read_adc_(0xD1 /* X */);
    data[4] = this->read_adc_(0x91 /* Y */);
  }

  data[5] = this->read_adc_(0xD0 /* X */);  // Last X touch power down

  disable();

  if (touch) {
    x_raw = best_two_avg(data[1], data[3], data[5]);
    y_raw = best_two_avg(data[0], data[2], data[4]);

    ESP_LOGD(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw);

    this->add_raw_touch_position_(0, x_raw, y_raw, z_raw);
  }
}

void XPT2046Component::dump_config() {
  ESP_LOGCONFIG(TAG,
                "XPT2046:\n"
                "  X min: %d\n"
                "  X max: %d\n"
                "  Y min: %d\n"
                "  Y max: %d\n"
                "  Swap X/Y: %s\n"
                "  Invert X: %s\n"
                "  Invert Y: %s\n"
                "  threshold: %d",
                this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_, YESNO(this->swap_x_y_),
                YESNO(this->invert_x_), YESNO(this->invert_y_), this->threshold_);
  LOG_PIN("  IRQ Pin: ", this->irq_pin_);
  LOG_UPDATE_INTERVAL(this);
}

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

int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) {
  int16_t delta_a, delta_b, delta_c;
  int16_t reta = 0;

  delta_a = (value1 > value2) ? value1 - value2 : value2 - value1;
  delta_b = (value1 > value3) ? value1 - value3 : value3 - value1;
  delta_c = (value3 > value2) ? value3 - value2 : value2 - value3;

  if (delta_a <= delta_b && delta_a <= delta_c) {
    reta = (value1 + value2) >> 1;
  } else if (delta_b <= delta_a && delta_b <= delta_c) {
    reta = (value1 + value3) >> 1;
  } else {
    reta = (value2 + value3) >> 1;
  }

  return reta;
}

int16_t XPT2046Component::read_adc_(uint8_t ctrl) {  // NOLINT
  uint8_t data[2];

  this->write_byte(ctrl);
  delay(1);
  data[0] = this->read_byte();
  data[1] = this->read_byte();

  return ((data[0] << 8) | data[1]) >> 3;
}

}  // namespace esphome::xpt2046
