/**
 * \file cimditssd1306.h
 * \brief Declaration of the CimditSSD1306 class
 * \author GrumpyDeveloper (Sascha Nitsch)
 * \copyright 2022 Sascha Nitsch
 * Licensed under MIT license
 *
 */

// system includes
#include <avr/pgmspace.h>
#include <util/delay.h>

// our includes
#include "cimditssd1306.h"
#include "glcdfont.c"

CimditSSD1306::CimditSSD1306(uint8_t w, uint8_t h, uint8_t i2cAddr) {
  m_width = w;
  m_height = h;
  m_buffer = nullptr;
  m_cursorX = m_cursorY = 0;
  m_textsizeX = m_textsizeY = 1;
  m_state = true;
  m_i2cAddr = i2cAddr;
}

CimditSSD1306::~CimditSSD1306() {
  if (m_buffer) {
    free(m_buffer);
    m_buffer = nullptr;
  }
}

void CimditSSD1306::commandStart() {
  Wire.beginTransmission(m_i2cAddr);
  Wire.write((uint8_t)0x00);
}

void CimditSSD1306::commandEnd() {
  Wire.endTransmission();
}

void CimditSSD1306::commandListProgMem(const uint8_t *c, uint8_t n) {
  uint16_t bytesOut = 1;
  while (n--) {
    if (bytesOut >= BUFFER_LENGTH) {
      commandEnd();
      commandStart();
      bytesOut = 1;
    }
    Wire.write(pgm_read_byte(c++));
    bytesOut++;
  }
}

/// static intialization part 1
static const uint8_t PROGMEM init1[] = {
  SSD1306_SET_DISPLAY_OFF,
  SSD1306_SET_DISPLAY_CLOCK_DIVIDE_RATIO, 0x80,
  SSD1306_SET_MULTIPLEX_RATIO
};

/// static initialization part 2
static const uint8_t PROGMEM init2[] = {
  SSD1306_SET_DISPLAY_OFFSET, 0x0,
  SSD1306_SET_DISPLAY_START_LINE_0,
  SSD1306_CHARGE_PUMP_SETTING, 0x14,
  SSD1306_SET_MEMORY_ADDRESSING_MODE, 0x00,
  SSD1306_SET_SEGMENT_RE_MAP_127,
  SSD1306_SET_COM_OUTPUT_SCAN_DIRECTION_REMAPPED,
  SSD1306_SET_COM_PINS_HARDWARE_CONFIGURATION, 0x02,
  SSD1306_SET_CONTRAST_CONTROL, 0x8F,
  SSD1306_SET_PRE_CHARGE_PERIOD, 0xF1,
  SSD1306_SET_VCOM_DESELECT_LEVEL, 0x20,
  SSD1306_ENTIRE_DISPLAY_ON,
  SSD1306_SET_NORMAL_DISPLAY,
  SSD1306_DEACTIVATE_SCROLL,
  SSD1306_SET_DISPLAY_ON
};

bool CimditSSD1306::begin() {
  if ((!m_buffer) && !(m_buffer = reinterpret_cast<uint8_t *>(malloc(m_width * (m_height / 8)))))
    return false;
  clearDisplay();
  // Init sequence
  commandStart();
  commandListProgMem(init1, sizeof(init1));
  Wire.write(m_height - 1);
  commandListProgMem(init2, sizeof(init2));
  commandEnd();
  return true;  // Success
}

void CimditSSD1306::drawPixel(uint8_t x, uint8_t y, bool state) {
  if (state) {
    m_buffer[x + (y >> 3) * m_width] |= (1 << (y & 7));
  } else {
    m_buffer[x + (y >> 3) * m_width] &= ~(1 << (y & 7));
  }
}

void CimditSSD1306::clearDisplay(void) {
  memset(m_buffer, 0, m_width * ((m_height + 7) / 8));
}

void CimditSSD1306::drawFastHLine(uint8_t x, uint8_t y, uint8_t w, bool state) {
  uint8_t *pBuf = m_buffer + (y / 8) * m_width + x;
  uint8_t mask = 1 << (y & 7);
  if (state) {
    while (w--) {
      *pBuf++ |= mask;
    }
  } else {
    mask = ~mask;
    while (w--) {
      *pBuf++ &= mask;
    }
  }
}

void CimditSSD1306::drawFastVLine(uint8_t x, uint8_t y, uint8_t h, bool state) {
  uint8_t *pBuf = m_buffer + (y / 8) * m_width + x;
  // do the first partial byte, if necessary - this requires some masking
  uint8_t mod = (y & 7);
  if (mod) {
    // mask off the high n bits we want to set
    mod = 8 - mod;
    // note - lookup table results in a nearly 10% performance
    // improvement in fill* functions
    // uint8_t mask = ~(0xFF >> mod);
    static const uint8_t PROGMEM premask[8] = {0x00, 0x80, 0xC0, 0xE0,
                                               0xF0, 0xF8, 0xFC, 0xFE};
    uint8_t mask = pgm_read_byte(&premask[mod]);
    // adjust the mask if we're not going to reach the end of this byte
    if (h < mod) {
      mask &= (0XFF >> (mod - h));
    }
    if (state) {
      *pBuf |= mask;
    } else {
      *pBuf &= ~mask;
    }
    pBuf += m_width;
  }

  if (h >= mod) {  // More to go?
    h -= mod;
    // Write solid bytes while we can - effectively 8 rows at a time
    if (h >= 8) {
      // store a local value to work with
      uint8_t val = state ? 255 : 0;
      do {
        *pBuf = val;      // Set byte
        pBuf += m_width;  // Advance pointer 8 rows
        h -= 8;           // Subtract 8 rows from height
      } while (h >= 8);
    }
    if (h) {  // Do the final partial byte, if necessary
      mod = h & 7;
      // this time we want to mask the low bits of the byte,
      // vs the high bits we did above
      // uint8_t mask = (1 << mod) - 1;
      // note - lookup table results in a nearly 10% performance
      // improvement in fill* functions
      static const uint8_t PROGMEM postmask[8] = {0x00, 0x01, 0x03, 0x07,
                                                  0x0F, 0x1F, 0x3F, 0x7F};
      uint8_t mask = pgm_read_byte(&postmask[mod]);
      if (state) {
        *pBuf |= mask;
      } else {
        *pBuf &= ~mask;
      }
    }
  }
}

/// statric start command for transmitting screen  content
static const uint8_t PROGMEM dlist1[] = {
  SSD1306_SET_PAGE_ADDRESS, 0, 0x3,
  SSD1306_SET_COLUMN_ADDRESS, 0
};

void CimditSSD1306::display(void) {
  commandStart();
  commandListProgMem(dlist1, sizeof(dlist1));
  Wire.write(m_width - 1);  // Column end address
  commandEnd();
  uint16_t count = m_width * (m_height >> 3);
  uint8_t *ptr = m_buffer;
  Wire.beginTransmission(m_i2cAddr);
  Wire.write((uint8_t)0x40);
  uint16_t bytesOut = 1;
  while (count--) {
    if (bytesOut >= BUFFER_LENGTH) {
      Wire.endTransmission();
      Wire.beginTransmission(m_i2cAddr);
      Wire.write((uint8_t)0x40);
      bytesOut = 1;
    }
    Wire.write(*ptr++);
    bytesOut++;
  }
  Wire.endTransmission();
}

void CimditSSD1306::displayPartial(uint8_t part) {
  commandStart();
  Wire.write(SSD1306_SET_PAGE_ADDRESS);
  Wire.write(part);             // Page start address
  Wire.write(part);             // Page end address
  Wire.write(SSD1306_SET_COLUMN_ADDRESS);
  Wire.write(0);
  Wire.write(m_width - 1);  // Column end address
  Wire.endTransmission();
  uint16_t count = m_width * (m_height >> 5);
  uint8_t *ptr = m_buffer + m_width * part;
  Wire.beginTransmission(m_i2cAddr);
  Wire.write((uint8_t)0x40);
  uint16_t bytesOut = 1;
  while (count--) {
    if (bytesOut >= BUFFER_LENGTH) {
      Wire.endTransmission();
      Wire.beginTransmission(m_i2cAddr);
      Wire.write((uint8_t)0x40);
      bytesOut = 1;
    }
    Wire.write(*ptr++);
    bytesOut++;
  }
  Wire.endTransmission();
}

void CimditSSD1306::setTextSize(uint8_t sX, uint8_t sY) {
  m_textsizeX = sX;
  m_textsizeY = sY;
}

size_t CimditSSD1306::write(uint8_t c) {
  if (c == '\n' || m_cursorX + m_textsizeX * 6) {  // a newline or text oveflow
    m_cursorX = 0;                 // set X to zero
    m_cursorY += m_textsizeY * 8;  // go down one line
  } else if (c == '\r') {          // a carriage return
    m_cursorX = 0;                 // set X to zero
  } else {                         // something to print
    drawChar(m_cursorX, m_cursorY, c, m_textsizeX, m_textsizeY);
    m_cursorX += m_textsizeX * 6;    // Advance x one char
  }
  return 1;
}

void CimditSSD1306::drawChar(uint8_t x, uint8_t y, unsigned char c, uint8_t sizeX, uint8_t sizeY) {
  for (int8_t i = 0; i < 5; i++) {  // one char is 5 pixel wide
    uint8_t line = pgm_read_byte(&font[c * 5 + i]); // get pixel from flash/program memory
    for (int8_t j = 0; j < 8; j++, line >>= 1) { // for each 8 lines
      if (line & 1) {
        if (sizeX == 1 && sizeY == 1) { // one pixel size, draw pixel direct
          drawPixel(x + i, y + j, m_state);
        } else {
          fillRect(x + i * sizeX, y + j * sizeY, sizeX, sizeY, m_state); // draw rectangles with given size multiplicator
        }
      }
    }
  }
}

void CimditSSD1306::fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, bool state) {
  if (y + h >= m_height) h = m_height - y;
  if (x + w >= m_width) w = m_width - x;
  for (int16_t i = x; i < x + w; i++) {
    drawFastVLine(i, y, h, state);
  }
}

void CimditSSD1306::setTextState(bool state) {
  m_state = state;
}

void CimditSSD1306::setCursor(uint8_t x, uint8_t y) {
  m_cursorX = x;
  m_cursorY = y;
}
