#include "tm1638.h"
#include "sevenseg.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

namespace esphome::tm1638 {

static const char *const TAG = "display.tm1638";
static const uint8_t TM1638_REGISTER_FIXEDADDRESS = 0x44;
static const uint8_t TM1638_REGISTER_AUTOADDRESS = 0x40;
static const uint8_t TM1638_REGISTER_READBUTTONS = 0x42;
static const uint8_t TM1638_REGISTER_DISPLAYOFF = 0x80;
static const uint8_t TM1638_REGISTER_DISPLAYON = 0x88;
static const uint8_t TM1638_REGISTER_7SEG_0 = 0xC0;
static const uint8_t TM1638_REGISTER_LED_0 = 0xC1;
static const uint8_t TM1638_UNKNOWN_CHAR = 0b11111111;

static const uint8_t TM1638_SHIFT_DELAY = 4;  // clock pause between commands, default 4ms

void TM1638Component::setup() {
  this->clk_pin_->setup();  // OUTPUT
  this->dio_pin_->setup();  // OUTPUT
  this->stb_pin_->setup();  // OUTPUT

  this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->stb_pin_->pin_mode(gpio::FLAG_OUTPUT);

  this->clk_pin_->digital_write(false);
  this->dio_pin_->digital_write(false);
  this->stb_pin_->digital_write(false);

  this->set_intensity(intensity_);

  this->reset_();  // all LEDs off
}

void TM1638Component::dump_config() {
  ESP_LOGCONFIG(TAG,
                "TM1638:\n"
                "  Intensity: %u",
                this->intensity_);
  LOG_PIN("  CLK Pin: ", this->clk_pin_);
  LOG_PIN("  DIO Pin: ", this->dio_pin_);
  LOG_PIN("  STB Pin: ", this->stb_pin_);
  LOG_UPDATE_INTERVAL(this);
}

void TM1638Component::loop() {
  if (this->listeners_.empty())
    return;

  uint8_t keys = this->get_keys();
  for (auto &listener : this->listeners_)
    listener->keys_update(keys);
}

uint8_t TM1638Component::get_keys() {
  uint8_t buttons = 0;

  this->stb_pin_->digital_write(false);

  this->shift_out_(TM1638_REGISTER_READBUTTONS);

  this->dio_pin_->pin_mode(gpio::FLAG_INPUT);

  delayMicroseconds(10);

  for (uint8_t i = 0; i < 4; i++) {  // read the 4 button registers
    uint8_t v = this->shift_in_();
    buttons |= v << i;  // shift bits to correct slots in the byte
  }

  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);

  this->stb_pin_->digital_write(true);

  return buttons;
}

void TM1638Component::update() {  // this is called at the interval specified in the config.yaml
  if (this->writer_.has_value()) {
    (*this->writer_)(*this);
  }

  this->display();
}

float TM1638Component::get_setup_priority() const { return setup_priority::PROCESSOR; }

void TM1638Component::display() {
  for (uint8_t i = 0; i < 8; i++) {
    this->set_7seg_(i, buffer_[i]);
  }
}

void TM1638Component::reset_() {
  uint8_t num_commands = 16;  // 16 addresses, 8 for 7seg and 8 for LEDs
  uint8_t commands[num_commands];

  for (uint8_t i = 0; i < num_commands; i++) {
    commands[i] = 0;
  }

  this->send_command_sequence_(commands, num_commands, TM1638_REGISTER_7SEG_0);
}

/////////////// LEDs /////////////////

void TM1638Component::set_led(int led_pos, bool led_on_off) {
  this->send_command_(TM1638_REGISTER_FIXEDADDRESS);

  uint8_t commands[2];

  commands[0] = TM1638_REGISTER_LED_0 + (led_pos << 1);
  commands[1] = led_on_off;

  this->send_commands_(commands, 2);
}

void TM1638Component::set_7seg_(int seg_pos, uint8_t seg_bits) {
  this->send_command_(TM1638_REGISTER_FIXEDADDRESS);

  uint8_t commands[2] = {};

  commands[0] = TM1638_REGISTER_7SEG_0 + (seg_pos << 1);
  commands[1] = seg_bits;

  this->send_commands_(commands, 2);
}

void TM1638Component::set_intensity(uint8_t brightness_level) {
  this->intensity_ = brightness_level;

  this->send_command_(TM1638_REGISTER_FIXEDADDRESS);

  if (brightness_level > 0) {
    this->send_command_((uint8_t) (TM1638_REGISTER_DISPLAYON | intensity_));
  } else {
    this->send_command_(TM1638_REGISTER_DISPLAYOFF);
  }
}

/////////////// DISPLAY PRINT /////////////////

uint8_t TM1638Component::print(uint8_t start_pos, const char *str) {
  uint8_t pos = start_pos;
  bool last_was_dot = false;

  for (; *str != '\0'; str++) {
    uint8_t data = TM1638_UNKNOWN_CHAR;

    if (*str >= ' ' && *str <= '~') {
      // Subtract 32 to account for ASCII offset
      data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]);
    } else {
      ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str);
    }

    if (*str == '.') {
      // Merge dot onto previous character unless we're at the start or last was also a dot
      if (pos != start_pos && !last_was_dot) {
        pos--;
      }
      if (pos >= 8) {
        ESP_LOGI(TAG, "TM1638 String is too long for the display!");
        break;
      }
      // Turn on the dot on the previous position
      this->buffer_[pos] |= 0b10000000;
      last_was_dot = true;
    } else {
      // Not a dot, write the character to display
      if (pos >= 8) {
        ESP_LOGI(TAG, "TM1638 String is too long for the display!");
        break;
      }
      this->buffer_[pos] = data;
      last_was_dot = false;
    }

    pos++;
  }
  return pos - start_pos;
}

/////////////// PRINT /////////////////

uint8_t TM1638Component::print(const char *str) { return this->print(0, str); }

uint8_t TM1638Component::printf(uint8_t pos, const char *format, ...) {
  va_list arg;
  va_start(arg, format);
  char buffer[64];
  int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
  va_end(arg);
  if (ret > 0)
    return this->print(pos, buffer);
  return 0;
}
uint8_t TM1638Component::printf(const char *format, ...) {
  va_list arg;
  va_start(arg, format);
  char buffer[64];
  int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
  va_end(arg);
  if (ret > 0)
    return this->print(buffer);
  return 0;
}

uint8_t TM1638Component::strftime(uint8_t pos, const char *format, ESPTime time) {
  char buffer[64];
  size_t ret = time.strftime(buffer, sizeof(buffer), format);
  if (ret > 0)
    return this->print(pos, buffer);
  return 0;
}
uint8_t TM1638Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }

//////////////// SPI   ////////////////

void TM1638Component::send_command_(uint8_t value) {
  this->stb_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->stb_pin_->digital_write(false);
  this->shift_out_(value);
  this->stb_pin_->digital_write(true);
}

void TM1638Component::send_commands_(uint8_t const commands[], uint8_t num_commands) {
  this->stb_pin_->digital_write(false);

  for (uint8_t i = 0; i < num_commands; i++) {
    uint8_t command = commands[i];
    this->shift_out_(command);
  }
  this->stb_pin_->digital_write(true);
}

void TM1638Component::send_command_leave_open_(uint8_t value) {
  this->stb_pin_->digital_write(false);
  this->shift_out_(value);
}

void TM1638Component::send_command_sequence_(uint8_t commands[], uint8_t num_commands, uint8_t starting_address) {
  this->send_command_(TM1638_REGISTER_AUTOADDRESS);
  this->send_command_leave_open_(starting_address);

  for (uint8_t i = 0; i < num_commands; i++) {
    this->shift_out_(commands[i]);
  }

  this->stb_pin_->digital_write(true);
}

uint8_t TM1638Component::shift_in_() {
  uint8_t value = 0;

  for (int i = 0; i < 8; ++i) {
    value |= dio_pin_->digital_read() << i;
    delayMicroseconds(TM1638_SHIFT_DELAY);
    this->clk_pin_->digital_write(true);
    delayMicroseconds(TM1638_SHIFT_DELAY);
    this->clk_pin_->digital_write(false);
    delayMicroseconds(TM1638_SHIFT_DELAY);
  }
  return value;
}

void TM1638Component::shift_out_(uint8_t val) {
  for (int i = 0; i < 8; i++) {
    this->dio_pin_->digital_write((val & (1 << i)));
    delayMicroseconds(TM1638_SHIFT_DELAY);

    this->clk_pin_->digital_write(true);
    delayMicroseconds(TM1638_SHIFT_DELAY);

    this->clk_pin_->digital_write(false);
    delayMicroseconds(TM1638_SHIFT_DELAY);
  }
}

}  // namespace esphome::tm1638
