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

namespace esphome {
namespace tca6408 {

static const char *const TAG = "tca6408";

// Registers
static const uint8_t REG_INPUT    = 0x00;
static const uint8_t REG_OUTPUT   = 0x01;
static const uint8_t REG_POLARITY = 0x02;
static const uint8_t REG_CONFIG   = 0x03;

void TCA6408Component::setup() {
  ESP_LOGCONFIG(TAG, "Setting up TCA6408 @ 0x%02X", this->address_);

  // Default safe state: all inputs
  this->mode_mask_ = 0xFF;
  this->output_mask_ = 0x00;
  this->input_mask_ = 0x00;
  this->polarity_mask_ = 0x00;

  if (this->write_register(REG_POLARITY, 0x00) != i2c::ERROR_OK) {
    ESP_LOGE(TAG, "Failed to init polarity register");
    this->mark_failed();
    return;
  }

  if (this->write_register(REG_CONFIG, this->mode_mask_) != i2c::ERROR_OK) {
    ESP_LOGE(TAG, "Failed to init config register");
    this->mark_failed();
    return;
  }

  this->read_inputs_();

  if (this->interrupt_pin_ != nullptr) {
    this->interrupt_pin_->setup();
    this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
    this->interrupt_pin_->attach_interrupt(
        &TCA6408Component::gpio_intr,
        this,
        gpio::INTERRUPT_FALLING_EDGE);

    this->enable_loop();
    ESP_LOGCONFIG(TAG, "Interrupt mode enabled");
  } else {
    this->enable_loop();
    ESP_LOGCONFIG(TAG, "Polling mode enabled");
  }

  ESP_LOGCONFIG(TAG, "TCA6408 setup complete");
}

void IRAM_ATTR TCA6408Component::gpio_intr(TCA6408Component *arg) {
  arg->interrupt_triggered_ = true;
  arg->enable_loop_soon_any_context();
}

void TCA6408Component::loop() {
  if (this->interrupt_triggered_) {
    this->interrupt_triggered_ = false;

    this->read_inputs_();
    this->reset_pin_cache_();

    if (this->interrupt_pin_ && this->interrupt_pin_->digital_read()) {
      // INT released -> stop looping until next interrupt
      this->disable_loop();
    }
  }
}

void TCA6408Component::dump_config() {
  ESP_LOGCONFIG(TAG, "TCA6408:");
  LOG_I2C_DEVICE(this);
  ESP_LOGCONFIG(TAG, "  Address: 0x%02X", this->address_);
  LOG_PIN("  INT Pin: ", this->interrupt_pin_);
}

bool TCA6408Component::read_inputs_() {
  uint8_t data = 0;

  if (this->read_register(REG_INPUT, &data) != i2c::ERROR_OK) {
    this->status_set_warning();
    return false;
  }

  this->input_mask_ = data;
  this->status_clear_warning();
  return true;
}

bool TCA6408Component::digital_read_hw(uint8_t pin) {
  // full refresh for accuracy
  return this->read_inputs_();
}

bool TCA6408Component::digital_read_cache(uint8_t pin) {
  bool value = (this->input_mask_ >> pin) & 0x01;

  if (this->polarity_mask_ & (1 << pin)) {
    value = !value;
  }

  return value;
}

void TCA6408Component::digital_write_hw(uint8_t pin, bool value) {
  if (value)
    this->output_mask_ |= (1 << pin);
  else
    this->output_mask_ &= ~(1 << pin);

  if (this->write_register(REG_OUTPUT, this->output_mask_) != i2c::ERROR_OK) {
    this->status_set_warning();
  } else {
    this->status_clear_warning();
  }
}

void TCA6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
  if (flags & gpio::FLAG_OUTPUT) {
    this->mode_mask_ &= ~(1 << pin);
  } else {
    this->mode_mask_ |= (1 << pin);
  }

  this->write_register(REG_CONFIG, this->mode_mask_);
}

void TCA6408Component::set_polarity_inversion(uint8_t pin, bool inverted) {
  if (inverted)
    this->polarity_mask_ |= (1 << pin);
  else
    this->polarity_mask_ &= ~(1 << pin);

  this->write_register(REG_POLARITY, this->polarity_mask_);
}

// ================= GPIO PIN WRAPPER =================

void TCA6408GPIOPin::set_parent(TCA6408Component *parent) {
  this->parent_ = parent;
}

void TCA6408GPIOPin::set_pin(uint8_t pin) {
  this->pin_ = pin;
}

void TCA6408GPIOPin::setup() {
  this->pin_mode(this->flags_);
}

void TCA6408GPIOPin::pin_mode(gpio::Flags flags) {
  this->flags_ = flags;
  this->parent_->pin_mode(this->pin_, flags);
}

bool TCA6408GPIOPin::digital_read() {
  return this->parent_->digital_read_cache(this->pin_) != this->inverted_;
}

void TCA6408GPIOPin::digital_write(bool value) {
  this->parent_->digital_write_hw(this->pin_, value != this->inverted_);
}

void TCA6408GPIOPin::set_inverted(bool inverted) {
  this->inverted_ = inverted;
  this->parent_->set_polarity_inversion(this->pin_, inverted);
}

size_t TCA6408GPIOPin::dump_summary(char *buffer, size_t len) const {
  return snprintf(buffer, len, "TCA6408 pin %u", this->pin_);
}

}  // namespace tca6408
}  // namespace esphome
