#ifdef USE_ARDUINO

#include "i2c_bus_arduino.h"
#include <Arduino.h>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

namespace esphome {
namespace i2c {

static const char *const TAG = "i2c.arduino";

void ArduinoI2CBus::setup() {
  recover_();

#if defined(USE_ESP32)
  static uint8_t next_bus_num = 0;
  if (next_bus_num == 0) {
    wire_ = &Wire;
  } else {
    wire_ = new TwoWire(next_bus_num);  // NOLINT(cppcoreguidelines-owning-memory)
  }
  this->port_ = next_bus_num;
  next_bus_num++;
#elif defined(USE_ESP8266)
  wire_ = new TwoWire();  // NOLINT(cppcoreguidelines-owning-memory)
#elif defined(USE_RP2040)
  static bool first = true;
  if (first) {
    wire_ = &Wire;
    first = false;
  } else {
    wire_ = &Wire1;  // NOLINT(cppcoreguidelines-owning-memory)
  }
#endif

  this->set_pins_and_clock_();

  this->initialized_ = true;
  if (this->scan_) {
    ESP_LOGV(TAG, "Scanning bus for active devices");
    this->i2c_scan_();
  }
}

void ArduinoI2CBus::set_pins_and_clock_() {
#ifdef USE_RP2040
  wire_->setSDA(this->sda_pin_);
  wire_->setSCL(this->scl_pin_);
  wire_->begin();
#else
  wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
#endif
  if (timeout_ > 0) {  // if timeout specified in yaml
#if defined(USE_ESP32)
    // https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp
    wire_->setTimeOut(timeout_ / 1000);  // unit: ms
#elif defined(USE_ESP8266)
    // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h
    wire_->setClockStretchLimit(timeout_);  // unit: us
#elif defined(USE_RP2040)
    // https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h
    wire_->setTimeout(timeout_ / 1000);  // unit: ms
#endif
  }
  wire_->setClock(frequency_);
}

void ArduinoI2CBus::dump_config() {
  ESP_LOGCONFIG(TAG, "I2C Bus:");
  ESP_LOGCONFIG(TAG,
                "  SDA Pin: GPIO%u\n"
                "  SCL Pin: GPIO%u\n"
                "  Frequency: %u Hz",
                this->sda_pin_, this->scl_pin_, this->frequency_);
  if (timeout_ > 0) {
#if defined(USE_ESP32)
    ESP_LOGCONFIG(TAG, "  Timeout: %u ms", this->timeout_ / 1000);
#elif defined(USE_ESP8266)
    ESP_LOGCONFIG(TAG, "  Timeout: %u us", this->timeout_);
#elif defined(USE_RP2040)
    ESP_LOGCONFIG(TAG, "  Timeout: %u ms", this->timeout_ / 1000);
#endif
  }
  switch (this->recovery_result_) {
    case RECOVERY_COMPLETED:
      ESP_LOGCONFIG(TAG, "  Recovery: bus successfully recovered");
      break;
    case RECOVERY_FAILED_SCL_LOW:
      ESP_LOGCONFIG(TAG, "  Recovery: failed, SCL is held low on the bus");
      break;
    case RECOVERY_FAILED_SDA_LOW:
      ESP_LOGCONFIG(TAG, "  Recovery: failed, SDA is held low on the bus");
      break;
  }
  if (this->scan_) {
    ESP_LOGI(TAG, "Results from bus scan:");
    if (scan_results_.empty()) {
      ESP_LOGI(TAG, "Found no devices");
    } else {
      for (const auto &s : scan_results_) {
        if (s.second) {
          ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
        } else {
          ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
        }
      }
    }
  }
}

ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
                                     uint8_t *read_buffer, size_t read_count) {
#if defined(USE_ESP8266)
  this->set_pins_and_clock_();  // reconfigure Wire global state in case there are multiple instances
#endif
  if (!initialized_) {
    ESP_LOGD(TAG, "i2c bus not initialized!");
    return ERROR_NOT_INITIALIZED;
  }

  ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str());

  uint8_t status = 0;
  if (write_count != 0 || read_count == 0) {
    wire_->beginTransmission(address);
    size_t ret = wire_->write(write_buffer, write_count);
    if (ret != write_count) {
      ESP_LOGV(TAG, "TX failed");
      return ERROR_UNKNOWN;
    }
    status = wire_->endTransmission(read_count == 0);
  }
  if (status == 0 && read_count != 0) {
    size_t ret2 = wire_->requestFrom(address, read_count, true);
    if (ret2 != read_count) {
      ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2);
      return ERROR_TIMEOUT;
    }
    for (size_t j = 0; j != read_count; j++)
      read_buffer[j] = wire_->read();
  }
  switch (status) {
    case 0:
      return ERROR_OK;
    case 1:
      // transmit buffer not large enough
      ESP_LOGVV(TAG, "TX failed: buffer not large enough");
      return ERROR_UNKNOWN;
    case 2:
    case 3:
      ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
      return ERROR_NOT_ACKNOWLEDGED;
    case 5:
      ESP_LOGVV(TAG, "TX failed: timeout");
      return ERROR_UNKNOWN;
    case 4:
    default:
      ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
      return ERROR_UNKNOWN;
  }
}

/// Perform I2C bus recovery, see:
/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf
/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
void ArduinoI2CBus::recover_() {
  ESP_LOGI(TAG, "Performing bus recovery");

  // For the upcoming operations, target for a 100kHz toggle frequency.
  // This is the maximum frequency for I2C running in standard-mode.
  // The actual frequency will be lower, because of the additional
  // function calls that are done, but that is no problem.
  const auto half_period_usec = 1000000 / 100000 / 2;

  // Activate input and pull up resistor for the SCL pin.
  pinMode(scl_pin_, INPUT_PULLUP);  // NOLINT

  // This should make the signal on the line HIGH. If SCL is pulled low
  // on the I2C bus however, then some device is interfering with the SCL
  // line. In that case, the I2C bus cannot be recovered.
  delayMicroseconds(half_period_usec);
  if (digitalRead(scl_pin_) == LOW) {  // NOLINT
    ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus");
    recovery_result_ = RECOVERY_FAILED_SCL_LOW;
    return;
  }

  // From the specification:
  // "If the data line (SDA) is stuck LOW, send nine clock pulses. The
  //  device that held the bus LOW should release it sometime within
  //  those nine clocks."
  // We don't really have to detect if SDA is stuck low. We'll simply send
  // nine clock pulses here, just in case SDA is stuck. Actual checks on
  // the SDA line status will be done after the clock pulses.

  // Make sure that switching to output mode will make SCL low, just in
  // case other code has setup the pin for a HIGH signal.
  digitalWrite(scl_pin_, LOW);  // NOLINT

  delayMicroseconds(half_period_usec);
  for (auto i = 0; i < 9; i++) {
    // Release pull up resistor and switch to output to make the signal LOW.
    pinMode(scl_pin_, INPUT);   // NOLINT
    pinMode(scl_pin_, OUTPUT);  // NOLINT
    delayMicroseconds(half_period_usec);

    // Release output and activate pull up resistor to make the signal HIGH.
    pinMode(scl_pin_, INPUT);         // NOLINT
    pinMode(scl_pin_, INPUT_PULLUP);  // NOLINT
    delayMicroseconds(half_period_usec);

    // When SCL is kept LOW at this point, we might be looking at a device
    // that applies clock stretching. Wait for the release of the SCL line,
    // but not forever. There is no specification for the maximum allowed
    // time. We yield and reset the WDT, so as to avoid triggering reset.
    // No point in trying to recover the bus by forcing a uC reset. Bus
    // should recover in a few ms or less else not likely to recovery at
    // all.
    auto wait = 250;
    while (wait-- && digitalRead(scl_pin_) == LOW) {  // NOLINT
      App.feed_wdt();
      delayMicroseconds(half_period_usec * 2);
    }
    if (digitalRead(scl_pin_) == LOW) {  // NOLINT
      ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle");
      recovery_result_ = RECOVERY_FAILED_SCL_LOW;
      return;
    }
  }

  // Activate input and pull resistor for the SDA pin, so we can verify
  // that SDA is pulled HIGH in the following step.
  pinMode(sda_pin_, INPUT_PULLUP);  // NOLINT
  digitalWrite(sda_pin_, LOW);      // NOLINT

  // By now, any stuck device ought to have sent all remaining bits of its
  // transaction, meaning that it should have freed up the SDA line, resulting
  // in SDA being pulled up.
  if (digitalRead(sda_pin_) == LOW) {  // NOLINT
    ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
    recovery_result_ = RECOVERY_FAILED_SDA_LOW;
    return;
  }

  // From the specification:
  // "I2C-bus compatible devices must reset their bus logic on receipt of
  //  a START or repeated START condition such that they all anticipate
  //  the sending of a target address, even if these START conditions are
  //  not positioned according to the proper format."
  // While the 9 clock pulses from above might have drained all bits of a
  // single byte within a transaction, a device might have more bytes to
  // transmit. So here we'll generate a START condition to snap the device
  // out of this state.
  // SCL and SDA are already high at this point, so we can generate a START
  // condition by making the SDA signal LOW.
  delayMicroseconds(half_period_usec);
  pinMode(sda_pin_, INPUT);   // NOLINT
  pinMode(sda_pin_, OUTPUT);  // NOLINT

  // From the specification:
  // "A START condition immediately followed by a STOP condition (void
  //  message) is an illegal format. Many devices however are designed to
  //  operate properly under this condition."
  // Finally, we'll bring the I2C bus into a starting state by generating
  // a STOP condition.
  delayMicroseconds(half_period_usec);
  pinMode(sda_pin_, INPUT);         // NOLINT
  pinMode(sda_pin_, INPUT_PULLUP);  // NOLINT

  recovery_result_ = RECOVERY_COMPLETED;
}
}  // namespace i2c
}  // namespace esphome

#endif  // USE_ESP_IDF
