/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn
 * Copyright (c) 2018 by Fabrice Weinberg
 * Copyright (c) 2024 by Heltec AutoMation
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * ThingPulse invests considerable time and money to develop these open source libraries.
 * Please support us by buying our products (and not the clones) from
 * https://thingpulse.com
 *
 */

#ifndef ST7789Spi_h
#define ST7789Spi_h

#include "OLEDDisplay.h"
#include <SPI.h>


#define ST_CMD_DELAY 0x80 // special signifier for command lists

#define ST77XX_NOP 0x00
#define ST77XX_SWRESET 0x01
#define ST77XX_RDDID 0x04
#define ST77XX_RDDST 0x09

#define ST77XX_SLPIN 0x10
#define ST77XX_SLPOUT 0x11
#define ST77XX_PTLON 0x12
#define ST77XX_NORON 0x13

#define ST77XX_INVOFF 0x20
#define ST77XX_INVON 0x21
#define ST77XX_DISPOFF 0x28
#define ST77XX_DISPON 0x29
#define ST77XX_CASET 0x2A
#define ST77XX_RASET 0x2B
#define ST77XX_RAMWR 0x2C
#define ST77XX_RAMRD 0x2E

#define ST77XX_PTLAR 0x30
#define ST77XX_TEOFF 0x34
#define ST77XX_TEON 0x35
#define ST77XX_MADCTL 0x36
#define ST77XX_COLMOD 0x3A

#define ST77XX_MADCTL_MY 0x80
#define ST77XX_MADCTL_MX 0x40
#define ST77XX_MADCTL_MV 0x20
#define ST77XX_MADCTL_ML 0x10
#define ST77XX_MADCTL_RGB 0x00

#define ST77XX_RDID1 0xDA
#define ST77XX_RDID2 0xDB
#define ST77XX_RDID3 0xDC
#define ST77XX_RDID4 0xDD

// Some ready-made 16-bit ('565') color settings:
#define ST77XX_BLACK 0x0000
#define ST77XX_WHITE 0xFFFF
#define ST77XX_RED 0xF800
#define ST77XX_GREEN 0x07E0
#define ST77XX_BLUE 0x001F
#define ST77XX_CYAN 0x07FF
#define ST77XX_MAGENTA 0xF81F
#define ST77XX_YELLOW 0xFFE0
#define ST77XX_ORANGE 0xFC00

#define LED_A_ON LOW

#ifdef ESP_PLATFORM
#undef LED_A_ON
#define LED_A_ON HIGH
#define rtos_free free
#define rtos_malloc malloc
//SPIClass SPI1(HSPI);
#endif
class ST7789Spi : public OLEDDisplay {
  private:
      uint8_t             _rst;
      uint8_t             _dc;
      uint8_t             _cs;
      uint8_t             _ledA;
      int             _miso;
      int             _mosi;
      int             _clk;
      SPIClass * _spi;
      SPISettings 		    _spiSettings;
      uint16_t            _RGB=0xFFFF;
      uint8_t             _buffheight;
  public:
    /* pass _cs as -1 to indicate "do not use CS pin", for cases where it is hard wired low */
    ST7789Spi(SPIClass *spiClass,uint8_t _rst, uint8_t _dc, uint8_t _cs, OLEDDISPLAY_GEOMETRY g = GEOMETRY_RAWMODE,uint16_t width=240,uint16_t height=135,int mosi=-1,int miso=-1,int clk=-1) {
      this->_spi = spiClass;
      this->_rst = _rst;
      this->_dc  = _dc;
      this->_cs  = _cs;
      this->_mosi=mosi;
      this->_miso=miso;
      this->_clk=clk;
      //this->_ledA  = _ledA;
      _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0);
      setGeometry(g,width,height);
    }

    bool connect(){
      this->_buffheight=displayHeight / 8;
      this->_buffheight+=displayHeight % 8 ? 1:0;
      pinMode(_cs, OUTPUT);
      pinMode(_dc, OUTPUT);
      //pinMode(_ledA, OUTPUT);
      if (_cs != (uint8_t) -1) {
        pinMode(_cs, OUTPUT);
      }  
      pinMode(_rst, OUTPUT);

#ifdef ESP_PLATFORM
      _spi->begin(_clk,_miso,_mosi,-1);
#else
      _spi->begin();
#endif
      _spi->setClockDivider (SPI_CLOCK_DIV2);

      // Pulse Reset low for 10ms
      digitalWrite(_rst, HIGH);
      delay(1);
      digitalWrite(_rst, LOW);
      delay(10);
      digitalWrite(_rst, HIGH);
      _spi->begin ();
      //digitalWrite(_ledA, LED_A_ON);
      return true;
    }

    void display(void) {
    #ifdef OLEDDISPLAY_DOUBLE_BUFFER

       uint16_t minBoundY = UINT16_MAX;
       uint16_t maxBoundY = 0;

       uint16_t minBoundX = UINT16_MAX;
       uint16_t maxBoundX = 0;

       uint16_t x, y;
        
       // Calculate the Y bounding box of changes
       // and copy buffer[pos] to buffer_back[pos];
       for (y = 0; y < _buffheight; y++) {
         for (x = 0; x < displayWidth; x++) {
          //Serial.printf("x  %d y %d\r\n",x,y);
          uint16_t pos = x + y * displayWidth;
          if (buffer[pos] != buffer_back[pos]) {
            minBoundY = min(minBoundY, y);
            maxBoundY = max(maxBoundY, y);
            minBoundX = min(minBoundX, x);
            maxBoundX = max(maxBoundX, x);
          }
          buffer_back[pos] = buffer[pos];
        }
        yield();
       }

       // If the minBoundY wasn't updated
       // we can savely assume that buffer_back[pos] == buffer[pos]
       // holdes true for all values of pos
       if (minBoundY == UINT16_MAX) return;

		  set_CS(LOW);
		  _spi->beginTransaction(_spiSettings);

          for (y = minBoundY; y <= maxBoundY; y++)
          {
            for(int temp = 0; temp<8;temp++)
            {
              //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
              setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
              //setAddrWindow(y*8+temp,minBoundX,1,maxBoundX-minBoundX+1);
              uint32_t const pixbufcount = maxBoundX-minBoundX+1;
              uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount);
              for (x = minBoundX; x <= maxBoundX; x++)
              {
                pixbuf[x-minBoundX] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0;
              }
#ifdef ESP_PLATFORM
              _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount);
#else
              _spi->transfer(pixbuf, NULL, 2 * pixbufcount);
#endif
              rtos_free(pixbuf);
            }
          }
	  _spi->endTransaction();
	  set_CS(HIGH);

     #else
		  set_CS(LOW);
		  _spi->beginTransaction(_spiSettings);
		uint8_t x, y;
          for (y = 0; y < _buffheight; y++)
          {
            for(int temp = 0; temp<8;temp++)
            {
              //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
              //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
              setAddrWindow(y*8+temp,0,1,displayWidth);
              uint32_t const pixbufcount = displayWidth;
              uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount);
              for (x = 0; x < displayWidth; x++)
              {
                pixbuf[x] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0;
              }
#ifdef ESP_PLATFORM
              _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount);
#else
              _spi->transfer(pixbuf, NULL, 2 * pixbufcount);
#endif
              rtos_free(pixbuf);
            }
          }
	  _spi->endTransaction();
	  set_CS(HIGH);

     #endif
    }

 virtual void resetOrientation() {
	uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV;
	sendCommand(ST77XX_MADCTL);
	WriteData(madctl);
	delay(10);
  }
  
 virtual void flipScreenVertically() {
	uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MY;
	sendCommand(ST77XX_MADCTL);
	WriteData(madctl);
	delay(10);
  }
  
 virtual void mirrorScreen() {
	uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX|ST77XX_MADCTL_MY;
	sendCommand(ST77XX_MADCTL);
	WriteData(madctl);
	delay(10);
  }

  virtual void landscapeScreen() {
    // For landscape mode rotated 180 degrees with correct text direction
    // MV swaps rows/columns for landscape orientation
    // Adding MX (instead of MY) flips X axis and rotates 180 degrees
    uint8_t madctl = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MX;
    sendCommand(ST77XX_MADCTL);
    WriteData(madctl);
    delay(10);
    }
  

  void setRGB(uint16_t c)
  {

    this->_RGB=0x00|c>>8|c<<8&0xFF00;
  }
  
  void displayOn(void) {
  //sendCommand(DISPLAYON);
  }

  void displayOff(void) {
  //sendCommand(DISPLAYOFF);
  }
  
  void drawBitmap(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *xbm) {
    int16_t widthInXbm = (width + 7) / 8;
    uint8_t data = 0;
  
    for(int16_t y = 0; y < height; y++) {
      for(int16_t x = 0; x < width; x++ ) {
        if (x & 7) {
          data <<= 1; // Move a bit
        } else {  // Read new data every 8 bit
            data = pgm_read_byte(xbm + (x / 8) + y * widthInXbm);
        }
        // if there is a bit draw it
        if (data & 0x80) {
          setPixel(xMove + x, yMove + y);
        }
      }
    }
  }
  

//#define ST77XX_MADCTL_MY 0x80
//#define ST77XX_MADCTL_MX 0x40
//#define ST77XX_MADCTL_MV 0x20
//#define ST77XX_MADCTL_ML 0x10
  protected:
    // Send all the init commands
    virtual void sendInitCommands()
    {
        sendCommand(ST77XX_SWRESET); //  1: Software reset, no args, w/delay
        delay(150);

        sendCommand(ST77XX_SLPOUT); //  2: Out of sleep mode, no args, w/delay
        delay(10);

        sendCommand(ST77XX_COLMOD); //  3: Set color mode, 16-bit color
        WriteData(0x55); 
        delay(10);
        
        // Initialize with landscape orientation rotated 180 degrees
        uint8_t madctl = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MX;
        sendCommand(ST77XX_MADCTL); //  4: Mem access ctrl (directions)
        WriteData(madctl);
        delay(10);
        
        // Set column address range for landscape orientation (240 pixels wide)
        sendCommand(ST77XX_CASET); //   5: Column addr set
        WriteData(0x00); 
        WriteData(0x00);         //    XSTART = 0
        WriteData(0x00); 
        WriteData(240);          //     XEND = 240
        
        // Set row address range for landscape orientation (135 pixels tall)
        sendCommand(ST77XX_RASET); //   6: Row addr set
        WriteData(0x00); 
        WriteData(0x00);         //    YSTART = 0
        WriteData(0x00);
        WriteData(135);          //    YEND = 135
        
        sendCommand(ST77XX_SLPOUT); //  7: hack
        delay(10);
        
        sendCommand(ST77XX_NORON); //  8: Normal display on, no args, w/delay
        delay(10);
        
        sendCommand(ST77XX_DISPON); //  9: Main screen turn on, no args, delay
        delay(10);

        sendCommand(ST77XX_INVON); //  10: invert
        delay(10);

        // Use landscape mode instead of portrait
        landscapeScreen();
        setRGB(ST77XX_GREEN);
    }


  private:

   void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
    x += (320-displayWidth)/2;
    y += (240-displayHeight)/2;
    
    uint32_t xa = ((uint32_t)x << 16) | (x + w - 1);
    uint32_t ya = ((uint32_t)y << 16) | (y + h - 1);

    writeCommand(ST77XX_CASET); // Column addr set
    SPI_WRITE32(xa);

    writeCommand(ST77XX_RASET); // Row addr set
    SPI_WRITE32(ya);

    writeCommand(ST77XX_RAMWR); // write to RAM
  }
	int getBufferOffset(void) {
		return 0;
	}
    inline void set_CS(bool level) {
      if (_cs != (uint8_t) -1) {
        digitalWrite(_cs, level);
      }
    };
    inline void sendCommand(uint8_t com) __attribute__((always_inline)){
      set_CS(HIGH);
      digitalWrite(_dc, LOW);
      set_CS(LOW);
      _spi->beginTransaction(_spiSettings);
      _spi->transfer(com);
      _spi->endTransaction();
      set_CS(HIGH);
      digitalWrite(_dc, HIGH);
    }
    
    inline void WriteData(uint8_t data) __attribute__((always_inline)){
        digitalWrite(_cs, LOW);
        _spi->beginTransaction(_spiSettings);
        _spi->transfer(data);
        _spi->endTransaction();
        digitalWrite(_cs, HIGH);
    }
   void SPI_WRITE32(uint32_t l)
   {
      _spi->transfer(l >> 24);
      _spi->transfer(l >> 16);
      _spi->transfer(l >> 8);
      _spi->transfer(l);
   }
  void writeCommand(uint8_t cmd) {
    digitalWrite(_dc, LOW);
    _spi->transfer(cmd);
    digitalWrite(_dc, HIGH);
  }

// Private functions
  void setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width, uint16_t height) {
    this->geometry = g;

    switch (g) {
      case GEOMETRY_128_128:
        this->displayWidth = 128;
        this->displayHeight = 128;
        break;
      case GEOMETRY_128_64:
        this->displayWidth = 128;
        this->displayHeight = 64;
        break;
      case GEOMETRY_128_32:
        this->displayWidth = 128;
        this->displayHeight = 32;
        break;
      case GEOMETRY_64_48:
        this->displayWidth = 64;
        this->displayHeight = 48;
        break;
      case GEOMETRY_64_32:
        this->displayWidth = 64;
        this->displayHeight = 32;
        break;
      case GEOMETRY_RAWMODE:
        this->displayWidth = width > 0 ? width : 128;
        this->displayHeight = height > 0 ? height : 64;
        break;
    }
    uint8_t tmp=displayHeight % 8;
    uint8_t _buffheight=displayHeight / 8;

    if(tmp!=0)
      _buffheight++;
    this->displayBufferSize = displayWidth * _buffheight ;
  }
  


};

#endif
