#include "tm1637.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

namespace esphome {
namespace tm1637 {

static const char *const TAG = "display.tm1637";
const uint8_t TM1637_CMD_DATA = 0x40;  //!< Display data command
const uint8_t TM1637_CMD_CTRL = 0x80;  //!< Display control command
const uint8_t TM1637_CMD_ADDR = 0xc0;  //!< Display address command
const uint8_t TM1637_UNKNOWN_CHAR = 0b11111111;

// Data command bits
const uint8_t TM1637_DATA_WRITE = 0x00;          //!< Write data
const uint8_t TM1637_DATA_READ_KEYS = 0x02;      //!< Read keys
const uint8_t TM1637_DATA_AUTO_INC_ADDR = 0x00;  //!< Auto increment address
const uint8_t TM1637_DATA_FIXED_ADDR = 0x04;     //!< Fixed address

//
//      A
//     ---
//  F |   | B
//     -G-
//  E |   | C
//     ---
//      D   X
// XABCDEFG
const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = {
    0b00000000,           // ' ', ord 0x20
    0b10110000,           // '!', ord 0x21
    0b00100010,           // '"', ord 0x22
    TM1637_UNKNOWN_CHAR,  // '#', ord 0x23
    TM1637_UNKNOWN_CHAR,  // '$', ord 0x24
    0b01001001,           // '%', ord 0x25
    TM1637_UNKNOWN_CHAR,  // '&', ord 0x26
    0b00000010,           // ''', ord 0x27
    0b01001110,           // '(', ord 0x28
    0b01111000,           // ')', ord 0x29
    0b01000000,           // '*', ord 0x2A
    TM1637_UNKNOWN_CHAR,  // '+', ord 0x2B
    0b00010000,           // ',', ord 0x2C
    0b00000001,           // '-', ord 0x2D
    0b10000000,           // '.', ord 0x2E
    TM1637_UNKNOWN_CHAR,  // '/', ord 0x2F
    0b01111110,           // '0', ord 0x30
    0b00110000,           // '1', ord 0x31
    0b01101101,           // '2', ord 0x32
    0b01111001,           // '3', ord 0x33
    0b00110011,           // '4', ord 0x34
    0b01011011,           // '5', ord 0x35
    0b01011111,           // '6', ord 0x36
    0b01110000,           // '7', ord 0x37
    0b01111111,           // '8', ord 0x38
    0b01111011,           // '9', ord 0x39
    0b01001000,           // ':', ord 0x3A
    0b01011000,           // ';', ord 0x3B
    TM1637_UNKNOWN_CHAR,  // '<', ord 0x3C
    TM1637_UNKNOWN_CHAR,  // '=', ord 0x3D
    TM1637_UNKNOWN_CHAR,  // '>', ord 0x3E
    0b01100101,           // '?', ord 0x3F
    0b01101111,           // '@', ord 0x40
    0b01110111,           // 'A', ord 0x41
    0b00011111,           // 'B', ord 0x42
    0b01001110,           // 'C', ord 0x43
    0b00111101,           // 'D', ord 0x44
    0b01001111,           // 'E', ord 0x45
    0b01000111,           // 'F', ord 0x46
    0b01011110,           // 'G', ord 0x47
    0b00110111,           // 'H', ord 0x48
    0b00110000,           // 'I', ord 0x49
    0b00111100,           // 'J', ord 0x4A
    TM1637_UNKNOWN_CHAR,  // 'K', ord 0x4B
    0b00001110,           // 'L', ord 0x4C
    TM1637_UNKNOWN_CHAR,  // 'M', ord 0x4D
    0b00010101,           // 'N', ord 0x4E
    0b01111110,           // 'O', ord 0x4F
    0b01100111,           // 'P', ord 0x50
    0b11111110,           // 'Q', ord 0x51
    0b00000101,           // 'R', ord 0x52
    0b01011011,           // 'S', ord 0x53
    0b00000111,           // 'T', ord 0x54
    0b00111110,           // 'U', ord 0x55
    0b00111110,           // 'V', ord 0x56
    0b00111111,           // 'W', ord 0x57
    TM1637_UNKNOWN_CHAR,  // 'X', ord 0x58
    0b00100111,           // 'Y', ord 0x59
    0b01101101,           // 'Z', ord 0x5A
    0b01001110,           // '[', ord 0x5B
    TM1637_UNKNOWN_CHAR,  // '\', ord 0x5C
    0b01111000,           // ']', ord 0x5D
    TM1637_UNKNOWN_CHAR,  // '^', ord 0x5E
    0b00001000,           // '_', ord 0x5F
    0b00100000,           // '`', ord 0x60
    0b01110111,           // 'a', ord 0x61
    0b00011111,           // 'b', ord 0x62
    0b00001101,           // 'c', ord 0x63
    0b00111101,           // 'd', ord 0x64
    0b01001111,           // 'e', ord 0x65
    0b01000111,           // 'f', ord 0x66
    0b01011110,           // 'g', ord 0x67
    0b00010111,           // 'h', ord 0x68
    0b00010000,           // 'i', ord 0x69
    0b00111100,           // 'j', ord 0x6A
    TM1637_UNKNOWN_CHAR,  // 'k', ord 0x6B
    0b00001110,           // 'l', ord 0x6C
    TM1637_UNKNOWN_CHAR,  // 'm', ord 0x6D
    0b00010101,           // 'n', ord 0x6E
    0b00011101,           // 'o', ord 0x6F
    0b01100111,           // 'p', ord 0x70
    TM1637_UNKNOWN_CHAR,  // 'q', ord 0x71
    0b00000101,           // 'r', ord 0x72
    0b01011011,           // 's', ord 0x73
    0b00000111,           // 't', ord 0x74
    0b00011100,           // 'u', ord 0x75
    0b00011100,           // 'v', ord 0x76
    TM1637_UNKNOWN_CHAR,  // 'w', ord 0x77
    TM1637_UNKNOWN_CHAR,  // 'x', ord 0x78
    0b00100111,           // 'y', ord 0x79
    TM1637_UNKNOWN_CHAR,  // 'z', ord 0x7A
    0b00110001,           // '{', ord 0x7B
    0b00000110,           // '|', ord 0x7C
    0b00000111,           // '}', ord 0x7D
    0b01100011,           // '~', ord 0x7E (degree symbol)
};
void TM1637Display::setup() {
  this->clk_pin_->setup();               // OUTPUT
  this->clk_pin_->digital_write(false);  // LOW
  this->dio_pin_->setup();               // OUTPUT
  this->dio_pin_->digital_write(false);  // LOW

  this->display();
}
void TM1637Display::dump_config() {
  ESP_LOGCONFIG(TAG,
                "TM1637:\n"
                "  Intensity: %d\n"
                "  Inverted: %d\n"
                "  Length: %d",
                this->intensity_, this->inverted_, this->length_);
  LOG_PIN("  CLK Pin: ", this->clk_pin_);
  LOG_PIN("  DIO Pin: ", this->dio_pin_);
  LOG_UPDATE_INTERVAL(this);
}

#ifdef USE_BINARY_SENSOR
void TM1637Display::loop() {
  uint8_t val = this->get_keys();
  for (auto *tm1637_key : this->tm1637_keys_)
    tm1637_key->process(val);
}

uint8_t TM1637Display::get_keys() {
  this->start_();
  this->send_byte_(TM1637_CMD_DATA | TM1637_DATA_READ_KEYS);
  this->start_();
  uint8_t key_code = read_byte_();
  this->stop_();
  if (key_code != 0xFF) {
    // Invert key_code:
    //    Bit |  7  6  5  4  3  2  1  0
    //  ------+-------------------------
    //   From | S0 S1 S2 K1 K2 1  1  1
    //     To | S0 S1 S2 K1 K2 0  0  0
    key_code = ~key_code;
    // Shift bits to:
    //    Bit | 7  6  5  4  3  2  1  0
    //  ------+------------------------
    //     To | 0  0  0  0  K2 S2 S1 S0
    key_code = (uint8_t) ((key_code & 0x80) >> 7 | (key_code & 0x40) >> 5 | (key_code & 0x20) >> 3 | (key_code & 0x08));
  }
  return key_code;
}
#endif

void TM1637Display::update() {
  for (uint8_t &i : this->buffer_)
    i = 0;
  if (this->writer_.has_value())
    (*this->writer_)(*this);
  this->display();
}

float TM1637Display::get_setup_priority() const { return setup_priority::PROCESSOR; }
void TM1637Display::bit_delay_() { delayMicroseconds(100); }
void TM1637Display::start_() {
  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->bit_delay_();
}

void TM1637Display::stop_() {
  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);
  bit_delay_();
  this->clk_pin_->pin_mode(gpio::FLAG_INPUT);
  bit_delay_();
  this->dio_pin_->pin_mode(gpio::FLAG_INPUT);
  bit_delay_();
}

void TM1637Display::display() {
  ESP_LOGVV(TAG, "Display %02X%02X%02X%02X", buffer_[0], buffer_[1], buffer_[2], buffer_[3]);

  // Write DATA CMND
  this->start_();
  this->send_byte_(TM1637_CMD_DATA);
  this->stop_();

  // Write ADDR CMD + first digit address
  this->start_();
  this->send_byte_(TM1637_CMD_ADDR);

  // Write the data bytes
  if (this->inverted_) {
    for (int8_t i = this->length_ - 1; i >= 0; i--) {
      this->send_byte_(this->buffer_[i]);
    }
  } else {
    for (auto b : this->buffer_) {
      this->send_byte_(b);
    }
  }

  this->stop_();

  // Write display CTRL CMND + brightness
  this->start_();
  this->send_byte_(TM1637_CMD_CTRL + ((this->intensity_ & 0x7) | (this->on_ ? 0x08 : 0x00)));
  this->stop_();
}
bool TM1637Display::send_byte_(uint8_t b) {
  uint8_t data = b;
  for (uint8_t i = 0; i < 8; i++) {
    // CLK low
    this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);
    this->bit_delay_();
    // Set data bit
    if (data & 0x01) {
      this->dio_pin_->pin_mode(gpio::FLAG_INPUT);
    } else {
      this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);
    }

    this->bit_delay_();
    // CLK high
    this->clk_pin_->pin_mode(gpio::FLAG_INPUT);
    this->bit_delay_();
    data = data >> 1;
  }
  // Wait for acknowledge
  // CLK to zero
  this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->dio_pin_->pin_mode(gpio::FLAG_INPUT);
  this->bit_delay_();
  // CLK to high
  this->clk_pin_->pin_mode(gpio::FLAG_INPUT);
  this->bit_delay_();
  uint8_t ack = this->dio_pin_->digital_read();
  if (ack == 0) {
    this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);
  }

  this->bit_delay_();
  this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->bit_delay_();

  return ack;
}

uint8_t TM1637Display::read_byte_() {
  uint8_t retval = 0;
  // Prepare DIO to read data
  this->dio_pin_->pin_mode(gpio::FLAG_INPUT);
  this->bit_delay_();
  // Data is shifted out by the TM1637 on the CLK falling edge
  for (uint8_t bit = 0; bit < 8; bit++) {
    this->clk_pin_->pin_mode(gpio::FLAG_INPUT);
    this->bit_delay_();
    // Read next bit
    retval <<= 1;
    if (this->dio_pin_->digital_read()) {
      retval |= 0x01;
    }
    this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);
    this->bit_delay_();
  }
  // Return DIO to output mode
  // Dummy ACK
  this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->bit_delay_();
  this->clk_pin_->pin_mode(gpio::FLAG_INPUT);
  this->bit_delay_();
  this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT);
  this->bit_delay_();
  this->dio_pin_->pin_mode(gpio::FLAG_INPUT);
  this->bit_delay_();
  return retval;
}

uint8_t TM1637Display::print(uint8_t start_pos, const char *str) {
  // ESP_LOGV(TAG, "Print at %d: %s", start_pos, str);
  uint8_t pos = start_pos;
  bool use_dot = false;
  for (; *str != '\0'; str++) {
    uint8_t data = TM1637_UNKNOWN_CHAR;
    if (*str >= ' ' && *str <= '~')
      data = progmem_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']);

    if (data == TM1637_UNKNOWN_CHAR) {
      ESP_LOGW(TAG, "Encountered character '%c' with no TM1637 representation while translating string!", *str);
    }
    // Remap segments, for compatibility with MAX7219 segment definition which is
    // XABCDEFG, but TM1637 is // XGFEDCBA
    if (this->inverted_) {
      // XABCDEFG > XGCBAFED
      data = ((data & 0x80) || use_dot ? 0x80 : 0) |  // no move X
             ((data & 0x40) ? 0x8 : 0) |              // A
             ((data & 0x20) ? 0x10 : 0) |             // B
             ((data & 0x10) ? 0x20 : 0) |             // C
             ((data & 0x8) ? 0x1 : 0) |               // D
             ((data & 0x4) ? 0x2 : 0) |               // E
             ((data & 0x2) ? 0x4 : 0) |               // F
             ((data & 0x1) ? 0x40 : 0);               // G
    } else {
      // XABCDEFG > XGFEDCBA
      data = ((data & 0x80) ? 0x80 : 0) |  // no move X
             ((data & 0x40) ? 0x1 : 0) |   // A
             ((data & 0x20) ? 0x2 : 0) |   // B
             ((data & 0x10) ? 0x4 : 0) |   // C
             ((data & 0x8) ? 0x8 : 0) |    // D
             ((data & 0x4) ? 0x10 : 0) |   // E
             ((data & 0x2) ? 0x20 : 0) |   // F
             ((data & 0x1) ? 0x40 : 0);    // G
    }
    use_dot = *str == '.';
    if (use_dot) {
      if ((!this->inverted_) && (pos != start_pos)) {
        this->buffer_[pos - 1] |= 0b10000000;
      }
    } else {
      if (pos >= 6) {
        ESP_LOGE(TAG, "String is too long for the display!");
        break;
      }
      this->buffer_[pos++] = data;
    }
  }
  return pos - start_pos;
}
uint8_t TM1637Display::print(const char *str) { return this->print(0, str); }
uint8_t TM1637Display::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 TM1637Display::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 TM1637Display::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 TM1637Display::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }

}  // namespace tm1637
}  // namespace esphome
