#include "max7219digit.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "max7219font.h"

#include <algorithm>
#include <cinttypes>

namespace esphome::max7219digit {

static const char *const TAG = "max7219DIGIT";

static const uint8_t MAX7219_REGISTER_NOOP = 0x00;
static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09;
static const uint8_t MAX7219_REGISTER_INTENSITY = 0x0A;
static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B;
static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C;
static const uint8_t MAX7219_REGISTER_DISPLAY_TEST = 0x0F;
constexpr uint8_t MAX7219_NO_SHUTDOWN = 0x00;
constexpr uint8_t MAX7219_SHUTDOWN = 0x01;
constexpr uint8_t MAX7219_NO_DISPLAY_TEST = 0x00;
constexpr uint8_t MAX7219_DISPLAY_TEST = 0x01;

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

void MAX7219Component::setup() {
  this->spi_setup();
  this->stepsleft_ = 0;
  for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) {
    std::vector<uint8_t> vec(1);
    this->max_displaybuffer_.push_back(vec);
    // Initialize buffer with 0 for display so all non written pixels are blank
    this->max_displaybuffer_[chip_line].resize(get_width_internal(), 0);
  }
  // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway
  this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
  // let's use our own ASCII -> led pattern encoding
  this->send_to_all_(MAX7219_REGISTER_DECODE_MODE, 0);
  // No display test with all the pixels on
  this->send_to_all_(MAX7219_REGISTER_DISPLAY_TEST, MAX7219_NO_DISPLAY_TEST);
  // SET Intsity of display
  this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_);
  // this->send_to_all_(MAX7219_REGISTER_INTENSITY, 1);
  this->display();
  // power up
  this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1);
}

void MAX7219Component::dump_config() {
  ESP_LOGCONFIG(TAG,
                "MAX7219DIGIT:\n"
                "  Number of Chips: %u\n"
                "  Number of Chips Lines: %u\n"
                "  Chips Lines Style : %u\n"
                "  Intensity: %u\n"
                "  Scroll Mode: %u\n"
                "  Scroll Speed: %u\n"
                "  Scroll Dwell: %u\n"
                "  Scroll Delay: %u",
                this->num_chips_, this->num_chip_lines_, this->chip_lines_style_, this->intensity_, this->scroll_mode_,
                this->scroll_speed_, this->scroll_dwell_, this->scroll_delay_);
  LOG_PIN("  CS Pin: ", this->cs_);
  LOG_UPDATE_INTERVAL(this);
}

void MAX7219Component::loop() {
  const uint32_t now = App.get_loop_component_start_time();
  const uint32_t millis_since_last_scroll = now - this->last_scroll_;
  const size_t first_line_size = this->max_displaybuffer_[0].size();
  // check if the buffer has shrunk past the current position since last update
  if ((first_line_size >= this->old_buffer_size_ + 3) || (first_line_size <= this->old_buffer_size_ - 3)) {
    ESP_LOGV(TAG, "Buffer size changed %d to %d", this->old_buffer_size_, first_line_size);
    this->stepsleft_ = 0;
    this->display();
    this->old_buffer_size_ = first_line_size;
  }

  if (!this->scroll_ || (first_line_size <= (size_t) get_width_internal())) {
    ESP_LOGVV(TAG, "Return if there is no need to scroll or scroll is off.");
    this->display();
    return;
  }

  if ((this->stepsleft_ == 0) && (millis_since_last_scroll < this->scroll_delay_)) {
    ESP_LOGVV(TAG, "At first step. Waiting for scroll delay");
    this->display();
    return;
  }

  if (this->scroll_mode_ == ScrollMode::STOP) {
    if (static_cast<size_t>(this->stepsleft_ + get_width_internal()) == first_line_size + 1) {
      if (millis_since_last_scroll < this->scroll_dwell_) {
        ESP_LOGVV(TAG,
                  "Dwell time at end of string in case of stop at end. Step %d, since last scroll %" PRIu32
                  ", dwell %d.",
                  this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_);
        return;
      }
      ESP_LOGV(TAG, "Dwell time passed. Continue scrolling.");
    }
  }

  if (millis_since_last_scroll >= this->scroll_speed_) {
    ESP_LOGVV(TAG, "Call to scroll left action");
    this->last_scroll_ = now;
    this->scroll_left();
    this->display();
  }
}

void MAX7219Component::display() {
  uint8_t pixels[8];
  // Run this loop for every MAX CHIP (GRID OF 64 leds)
  // Run this routine for the rows of every chip 8x row 0 top to 7 bottom
  // Fill the pixel parameter with display data
  // Send the data to the chip
  for (uint8_t chip = 0; chip < this->num_chips_ / this->num_chip_lines_; chip++) {
    for (uint8_t chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) {
      for (uint8_t j = 0; j < 8; j++) {
        bool reverse =
            chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE ? !this->reverse_ : this->reverse_;
        if (reverse) {
          pixels[j] =
              this->max_displaybuffer_[chip_line][(this->num_chips_ / this->num_chip_lines_ - chip - 1) * 8 + j];
        } else {
          pixels[j] = this->max_displaybuffer_[chip_line][chip * 8 + j];
        }
      }
      if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE)
        this->orientation_ = orientation_180_();
      this->send64pixels(chip_line * this->num_chips_ / this->num_chip_lines_ + chip, pixels);
      if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE)
        this->orientation_ = orientation_180_();
    }
  }
}

uint8_t MAX7219Component::orientation_180_() {
  switch (this->orientation_) {
    case 0:
      return 2;
    case 1:
      return 3;
    case 2:
      return 0;
    case 3:
      return 1;
    default:
      return 0;
  }
}

int MAX7219Component::get_height_internal() {
  return 8 * this->num_chip_lines_;  // TO BE DONE -> CREATE Virtual size of screen and scroll
}

int MAX7219Component::get_width_internal() { return this->num_chips_ / this->num_chip_lines_ * 8; }

void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) {
  if (x + 1 > (int) this->max_displaybuffer_[0].size()) {  // Extend the display buffer in case required
    for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) {
      this->max_displaybuffer_[chip_line].resize(x + 1, this->bckgrnd_);
    }
  }

  if ((y >= this->get_height_internal()) || (y < 0) || (x < 0))  // If pixel is outside display then dont draw
    return;

  uint16_t pos = x;    // X is starting at 0 top left
  uint8_t subpos = y;  // Y is starting at 0 top left

  if (color.is_on()) {
    this->max_displaybuffer_[subpos / 8][pos] |= (1 << subpos % 8);
  } else {
    this->max_displaybuffer_[subpos / 8][pos] &= ~(1 << subpos % 8);
  }
}

void MAX7219Component::send_byte_(uint8_t a_register, uint8_t data) {
  this->write_byte(a_register);  // Write register value to MAX
  this->write_byte(data);        // Followed by actual data
}
void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) {
  this->enable();                                 // Enable SPI
  for (uint8_t i = 0; i < this->num_chips_; i++)  // Run the loop for every MAX chip in the stack
    this->send_byte_(a_register, data);           // Send the data to the chips
  this->disable();                                // Disable SPI
}
void MAX7219Component::update() {
  this->update_ = true;
  for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) {
    this->max_displaybuffer_[chip_line].clear();
    this->max_displaybuffer_[chip_line].resize(get_width_internal(), this->bckgrnd_);
  }
  if (this->writer_local_.has_value())  // insert Labda function if available
    (*this->writer_local_)(*this);
}

void MAX7219Component::invert_on_off(bool on_off) { this->invert_ = on_off; };
void MAX7219Component::invert_on_off() { this->invert_ = !this->invert_; };

void MAX7219Component::turn_on_off(bool on_off) {
  if (on_off) {
    this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1);
  } else {
    this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 0);
  }
}

void MAX7219Component::scroll(bool on_off, ScrollMode mode, uint16_t speed, uint16_t delay, uint16_t dwell) {
  this->set_scroll(on_off);
  this->set_scroll_mode(mode);
  this->set_scroll_speed(speed);
  this->set_scroll_dwell(dwell);
  this->set_scroll_delay(delay);
}

void MAX7219Component::scroll(bool on_off, ScrollMode mode) {
  this->set_scroll(on_off);
  this->set_scroll_mode(mode);
}

void MAX7219Component::intensity(uint8_t intensity) {
  this->intensity_ = intensity;
  this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_);
}

void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); }

void MAX7219Component::scroll_left() {
  for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) {
    auto scroll = [&](std::vector<uint8_t> &line, uint16_t steps) {
      std::rotate(line.begin(), std::next(line.begin(), steps), line.end());
    };
    if (this->update_) {
      this->max_displaybuffer_[chip_line].push_back(this->bckgrnd_);
      scroll(this->max_displaybuffer_[chip_line],
             (this->stepsleft_ + 1) % (this->max_displaybuffer_[chip_line].size()));
    } else {
      scroll(this->max_displaybuffer_[chip_line], 1);
    }
  }
  this->update_ = false;
  this->stepsleft_++;
  this->stepsleft_ %= this->max_displaybuffer_[0].size();
}

void MAX7219Component::send_char(uint8_t chip, uint8_t data) {
  // get this character from PROGMEM
  for (uint8_t i = 0; i < 8; i++)
    this->max_displaybuffer_[0][chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]);
}  // end of send_char

// send one character (data) to position (chip)

void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) {
  for (uint8_t col = 0; col < 8; col++) {  // RUN THIS LOOP 8 times until column is 7
    this->enable();                        // start sending by enabling SPI
    for (uint8_t i = 0; i < chip; i++) {   // send extra NOPs to push the pixels out to extra displays
      this->send_byte_(MAX7219_REGISTER_NOOP,
                       MAX7219_REGISTER_NOOP);  // run this loop unit the matching chip is reached
    }
    uint8_t b = 0;  // rotate pixels 90 degrees -- set byte to 0
    if (this->orientation_ == 0) {
      for (uint8_t i = 0; i < 8; i++) {
        // run this loop 8 times for all the pixels[8] received
        if (this->flip_x_) {
          b |= ((pixels[i] >> col) & 1) << i;  // change the column bits into row bits
        } else {
          b |= ((pixels[i] >> col) & 1) << (7 - i);  // change the column bits into row bits
        }
      }
    } else if (this->orientation_ == 1) {
      if (this->flip_x_) {
        b = pixels[7 - col];
      } else {
        b = pixels[col];
      }
    } else if (this->orientation_ == 2) {
      for (uint8_t i = 0; i < 8; i++) {
        if (this->flip_x_) {
          b |= ((pixels[i] >> (7 - col)) & 1) << (7 - i);
        } else {
          b |= ((pixels[i] >> (7 - col)) & 1) << i;
        }
      }
    } else {
      for (uint8_t i = 0; i < 8; i++) {
        if (this->flip_x_) {
          b |= ((pixels[col] >> i) & 1) << (7 - i);
        } else {
          b |= ((pixels[7 - col] >> i) & 1) << (7 - i);
        }
      }
    }
    // send this byte to display at selected chip
    if (this->invert_) {
      this->send_byte_(col + 1, ~b);
    } else {
      this->send_byte_(col + 1, b);
    }
    for (int i = 0; i < this->num_chips_ - chip - 1; i++)  // end with enough NOPs so later chips don't update
      this->send_byte_(MAX7219_REGISTER_NOOP, MAX7219_REGISTER_NOOP);
    this->disable();  // all done disable SPI
  }                   // end of for each column
}  // end of send64pixels

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

uint8_t MAX7219Component::printdigit(uint8_t start_pos, const char *s) {
  uint8_t chip = start_pos;
  for (; chip < this->num_chips_ && *s; chip++)
    send_char(chip, *s++);
  // space out rest
  while (chip < (this->num_chips_))
    send_char(chip++, ' ');
  return 0;
}  // end of sendString

uint8_t MAX7219Component::printdigitf(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->printdigit(pos, buffer);
  return 0;
}
uint8_t MAX7219Component::printdigitf(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->printdigit(buffer);
  return 0;
}

uint8_t MAX7219Component::strftimedigit(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->printdigit(pos, buffer);
  return 0;
}
uint8_t MAX7219Component::strftimedigit(const char *format, ESPTime time) {
  return this->strftimedigit(0, format, time);
}

}  // namespace esphome::max7219digit
