/**
 * Marlin 3D Printer Firmware
 * Copyright (c) 2021 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
 *
 * Based on Sprinter and grbl.
 * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */
#if defined(ARDUINO_ARCH_STM32) && !defined(STM32GENERIC)

#include "../../../inc/MarlinConfig.h"

#if HAS_LTDC_TFT

#include "tft_ltdc.h"
#include "pinconfig.h"

#define FRAME_BUFFER_ADDRESS 0XC0000000  // SDRAM address

#define SDRAM_TIMEOUT                            ((uint32_t)0xFFFF)
#define REFRESH_COUNT                            ((uint32_t)0x02A5)  // SDRAM refresh counter

#define SDRAM_MODEREG_BURST_LENGTH_1             ((uint16_t)0x0000)
#define SDRAM_MODEREG_BURST_LENGTH_2             ((uint16_t)0x0001)
#define SDRAM_MODEREG_BURST_LENGTH_4             ((uint16_t)0x0002)
#define SDRAM_MODEREG_BURST_LENGTH_8             ((uint16_t)0x0004)
#define SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL      ((uint16_t)0x0000)
#define SDRAM_MODEREG_BURST_TYPE_INTERLEAVED     ((uint16_t)0x0008)
#define SDRAM_MODEREG_CAS_LATENCY_2              ((uint16_t)0x0020)
#define SDRAM_MODEREG_CAS_LATENCY_3              ((uint16_t)0x0030)
#define SDRAM_MODEREG_OPERATING_MODE_STANDARD    ((uint16_t)0x0000)
#define SDRAM_MODEREG_WRITEBURST_MODE_PROGRAMMED ((uint16_t)0x0000)
#define SDRAM_MODEREG_WRITEBURST_MODE_SINGLE     ((uint16_t)0x0200)

void SDRAM_Initialization_Sequence(SDRAM_HandleTypeDef *hsdram, FMC_SDRAM_CommandTypeDef *Command) {

  __IO uint32_t tmpmrd =0;
  /* Step 1:  Configure a clock configuration enable command */
  Command->CommandMode = FMC_SDRAM_CMD_CLK_ENABLE;
  Command->CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
  Command->AutoRefreshNumber = 1;
  Command->ModeRegisterDefinition = 0;
  /* Send the command */
  HAL_SDRAM_SendCommand(hsdram, Command, SDRAM_TIMEOUT);

  /* Step 2: Insert 100 us minimum delay */
  /* Inserted delay is equal to 1 ms due to systick time base unit (ms) */
  HAL_Delay(1);

  /* Step 3: Configure a PALL (precharge all) command */
  Command->CommandMode = FMC_SDRAM_CMD_PALL;
  Command->CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
  Command->AutoRefreshNumber = 1;
  Command->ModeRegisterDefinition = 0;
  /* Send the command */
  HAL_SDRAM_SendCommand(hsdram, Command, SDRAM_TIMEOUT);

  /* Step 4 : Configure a Auto-Refresh command */
  Command->CommandMode = FMC_SDRAM_CMD_AUTOREFRESH_MODE;
  Command->CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
  Command->AutoRefreshNumber = 8;
  Command->ModeRegisterDefinition = 0;
  /* Send the command */
  HAL_SDRAM_SendCommand(hsdram, Command, SDRAM_TIMEOUT);

  /* Step 5: Program the external memory mode register */
  tmpmrd = (uint32_t)(SDRAM_MODEREG_BURST_LENGTH_1          |
                      SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL   |
                      SDRAM_MODEREG_CAS_LATENCY_2           |
                      SDRAM_MODEREG_OPERATING_MODE_STANDARD |
                      SDRAM_MODEREG_WRITEBURST_MODE_SINGLE);

  Command->CommandMode = FMC_SDRAM_CMD_LOAD_MODE;
  Command->CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
  Command->AutoRefreshNumber = 1;
  Command->ModeRegisterDefinition = tmpmrd;
  /* Send the command */
  HAL_SDRAM_SendCommand(hsdram, Command, SDRAM_TIMEOUT);

  /* Step 6: Set the refresh rate counter */
  /* Set the device refresh rate */
  HAL_SDRAM_ProgramRefreshRate(hsdram, REFRESH_COUNT);
}

void SDRAM_Config() {

  __HAL_RCC_SYSCFG_CLK_ENABLE();
  __HAL_RCC_FMC_CLK_ENABLE();

  SDRAM_HandleTypeDef      hsdram;
  FMC_SDRAM_TimingTypeDef  SDRAM_Timing;
  FMC_SDRAM_CommandTypeDef command;

  /* Configure the SDRAM device */
  hsdram.Instance = FMC_SDRAM_DEVICE;
  hsdram.Init.SDBank             = FMC_SDRAM_BANK1;
  hsdram.Init.ColumnBitsNumber   = FMC_SDRAM_COLUMN_BITS_NUM_9;
  hsdram.Init.RowBitsNumber      = FMC_SDRAM_ROW_BITS_NUM_13;
  hsdram.Init.MemoryDataWidth    = FMC_SDRAM_MEM_BUS_WIDTH_16;
  hsdram.Init.InternalBankNumber = FMC_SDRAM_INTERN_BANKS_NUM_4;
  hsdram.Init.CASLatency         = FMC_SDRAM_CAS_LATENCY_2;
  hsdram.Init.WriteProtection    = FMC_SDRAM_WRITE_PROTECTION_DISABLE;
  hsdram.Init.SDClockPeriod      = FMC_SDRAM_CLOCK_PERIOD_2;
  hsdram.Init.ReadBurst          = FMC_SDRAM_RBURST_ENABLE;
  hsdram.Init.ReadPipeDelay      = FMC_SDRAM_RPIPE_DELAY_0;

  /* Timing configuration for 100Mhz as SDRAM clock frequency (System clock is up to 200Mhz) */
  SDRAM_Timing.LoadToActiveDelay    = 2;
  SDRAM_Timing.ExitSelfRefreshDelay = 8;
  SDRAM_Timing.SelfRefreshTime      = 6;
  SDRAM_Timing.RowCycleDelay        = 6;
  SDRAM_Timing.WriteRecoveryTime    = 2;
  SDRAM_Timing.RPDelay              = 2;
  SDRAM_Timing.RCDDelay             = 2;

  /* Initialize the SDRAM controller */
  if (HAL_SDRAM_Init(&hsdram, &SDRAM_Timing) != HAL_OK)
  {
    /* Initialization Error */
  }

  /* Program the SDRAM external device */
  SDRAM_Initialization_Sequence(&hsdram, &command);
}

void LTDC_Config() {

  __HAL_RCC_LTDC_CLK_ENABLE();
  __HAL_RCC_DMA2D_CLK_ENABLE();

  RCC_PeriphCLKInitTypeDef  PeriphClkInitStruct;

  /* The PLL3R is configured to provide the LTDC PCLK clock */
  /* PLL3_VCO Input = HSE_VALUE / PLL3M = 25Mhz / 5 = 5 Mhz */
  /* PLL3_VCO Output = PLL3_VCO Input * PLL3N = 5Mhz * 160 = 800 Mhz */
  /* PLLLCDCLK = PLL3_VCO Output/PLL3R = 800Mhz / 16 = 50Mhz */
  /* LTDC clock frequency = PLLLCDCLK = 50 Mhz */
  PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_LTDC;
  PeriphClkInitStruct.PLL3.PLL3M = 5;
  PeriphClkInitStruct.PLL3.PLL3N = 160;
  PeriphClkInitStruct.PLL3.PLL3FRACN = 0;
  PeriphClkInitStruct.PLL3.PLL3P = 2;
  PeriphClkInitStruct.PLL3.PLL3Q = 2;
  PeriphClkInitStruct.PLL3.PLL3R = (800 / LTDC_LCD_CLK);
  PeriphClkInitStruct.PLL3.PLL3VCOSEL = RCC_PLL3VCOWIDE;
  PeriphClkInitStruct.PLL3.PLL3RGE = RCC_PLL3VCIRANGE_2;
  HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct);

  LTDC_HandleTypeDef hltdc_F;
  LTDC_LayerCfgTypeDef pLayerCfg;

  /* LTDC Initialization -------------------------------------------------------*/

  /* Polarity configuration */
  /* Initialize the horizontal synchronization polarity as active low */
  hltdc_F.Init.HSPolarity = LTDC_HSPOLARITY_AL;
  /* Initialize the vertical synchronization polarity as active low */
  hltdc_F.Init.VSPolarity = LTDC_VSPOLARITY_AL;
  /* Initialize the data enable polarity as active low */
  hltdc_F.Init.DEPolarity = LTDC_DEPOLARITY_AL;
  /* Initialize the pixel clock polarity as input pixel clock */
  hltdc_F.Init.PCPolarity = LTDC_PCPOLARITY_IPC;

  /* Timing configuration */
  hltdc_F.Init.HorizontalSync = (LTDC_LCD_HSYNC - 1);
  hltdc_F.Init.VerticalSync = (LTDC_LCD_VSYNC - 1);
  hltdc_F.Init.AccumulatedHBP = (LTDC_LCD_HSYNC + LTDC_LCD_HBP - 1);
  hltdc_F.Init.AccumulatedVBP = (LTDC_LCD_VSYNC + LTDC_LCD_VBP - 1);
  hltdc_F.Init.AccumulatedActiveH = (TFT_HEIGHT + LTDC_LCD_VSYNC + LTDC_LCD_VBP - 1);
  hltdc_F.Init.AccumulatedActiveW = (TFT_WIDTH + LTDC_LCD_HSYNC + LTDC_LCD_HBP - 1);
  hltdc_F.Init.TotalHeigh = (TFT_HEIGHT + LTDC_LCD_VSYNC + LTDC_LCD_VBP + LTDC_LCD_VFP - 1);
  hltdc_F.Init.TotalWidth = (TFT_WIDTH + LTDC_LCD_HSYNC + LTDC_LCD_HBP + LTDC_LCD_HFP - 1);

  /* Configure R,G,B component values for LCD background color : all black background */
  hltdc_F.Init.Backcolor.Blue = 0;
  hltdc_F.Init.Backcolor.Green = 0;
  hltdc_F.Init.Backcolor.Red = 0;

  hltdc_F.Instance = LTDC;

  /* Layer0 Configuration ------------------------------------------------------*/

  /* Windowing configuration */
  pLayerCfg.WindowX0 = 0;
  pLayerCfg.WindowX1 = TFT_WIDTH;
  pLayerCfg.WindowY0 = 0;
  pLayerCfg.WindowY1 = TFT_HEIGHT;

  /* Pixel Format configuration*/
  pLayerCfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;

  /* Start Address configuration : frame buffer is located at SDRAM memory */
  pLayerCfg.FBStartAdress = (uint32_t)(FRAME_BUFFER_ADDRESS);

  /* Alpha constant (255 == totally opaque) */
  pLayerCfg.Alpha = 255;

  /* Default Color configuration (configure A,R,G,B component values) : no background color */
  pLayerCfg.Alpha0 = 0; /* fully transparent */
  pLayerCfg.Backcolor.Blue = 0;
  pLayerCfg.Backcolor.Green = 0;
  pLayerCfg.Backcolor.Red = 0;

  /* Configure blending factors */
  pLayerCfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_CA;
  pLayerCfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_CA;

  /* Configure the number of lines and number of pixels per line */
  pLayerCfg.ImageWidth  = TFT_WIDTH;
  pLayerCfg.ImageHeight = TFT_HEIGHT;

  /* Configure the LTDC */
  if (HAL_LTDC_Init(&hltdc_F) != HAL_OK)
  {
    /* Initialization Error */
  }

  /* Configure the Layer*/
  if (HAL_LTDC_ConfigLayer(&hltdc_F, &pLayerCfg, 0) != HAL_OK)
  {
    /* Initialization Error */
  }
}

uint16_t TFT_LTDC::x_min = 0;
uint16_t TFT_LTDC::x_max = 0;
uint16_t TFT_LTDC::y_min = 0;
uint16_t TFT_LTDC::y_max = 0;
uint16_t TFT_LTDC::x_cur = 0;
uint16_t TFT_LTDC::y_cur = 0;
uint8_t TFT_LTDC::reg = 0;
volatile uint16_t* TFT_LTDC::framebuffer = (volatile uint16_t* )FRAME_BUFFER_ADDRESS;

void TFT_LTDC::Init() {

  // SDRAM pins init
  for (uint16_t i = 0; PinMap_SDRAM[i].pin != NC; i++)
    pinmap_pinout(PinMap_SDRAM[i].pin, PinMap_SDRAM);

  // SDRAM peripheral config
  SDRAM_Config();

  // LTDC pins init
  for (uint16_t i = 0; PinMap_LTDC[i].pin != NC; i++)
    pinmap_pinout(PinMap_LTDC[i].pin, PinMap_LTDC);

  // LTDC peripheral config
  LTDC_Config();
}

uint32_t TFT_LTDC::GetID() {
  return 0xABAB;
}

uint32_t TFT_LTDC::ReadID(tft_data_t Reg) {
  return 0xABAB;
}

bool TFT_LTDC::isBusy() {
  return false;
}

uint16_t TFT_LTDC::ReadPoint(uint16_t x, uint16_t y) {
  return framebuffer[(TFT_WIDTH * y) + x];
}

void TFT_LTDC::DrawPoint(uint16_t x, uint16_t y, uint16_t color) {
  framebuffer[(TFT_WIDTH * y) + x] = color;
}

void TFT_LTDC::DrawRect(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint16_t color) {

  if (sx == ex || sy == ey) return;

  uint16_t offline = TFT_WIDTH - (ex - sx);
  uint32_t addr = (uint32_t)&framebuffer[(TFT_WIDTH * sy) + sx];

  CBI(DMA2D->CR, 0);
  DMA2D->CR = 3 << 16;
  DMA2D->OPFCCR = 0X02;
  DMA2D->OOR = offline;
  DMA2D->OMAR = addr;
  DMA2D->NLR = (ey - sy) | ((ex - sx) << 16);
  DMA2D->OCOLR = color;
  SBI(DMA2D->CR, 0);

  uint32_t timeout = 0;
  while (!TEST(DMA2D->ISR, 1)) {
    timeout++;
    if (timeout > 0x1FFFFF) break;
  }
  SBI(DMA2D->IFCR, 1);
}

void TFT_LTDC::DrawImage(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint16_t *colors) {

  if (sx == ex || sy == ey) return;

  uint16_t offline = TFT_WIDTH - (ex - sx);
  uint32_t addr = (uint32_t)&framebuffer[(TFT_WIDTH * sy) + sx];

  CBI(DMA2D->CR, 0);
  DMA2D->CR = 0 << 16;
  DMA2D->FGPFCCR = 0X02;
  DMA2D->FGOR = 0;
  DMA2D->OOR = offline;
  DMA2D->FGMAR = (uint32_t)colors;
  DMA2D->OMAR = addr;
  DMA2D->NLR = (ey - sy) | ((ex - sx) << 16);
  SBI(DMA2D->CR, 0);

  uint32_t timeout = 0;
  while (!TEST(DMA2D->ISR, 1)) {
    timeout++;
    if (timeout > 0x1FFFFF) break;
  }
  SBI(DMA2D->IFCR, 1);
}

void TFT_LTDC::WriteData(uint16_t data) {
  switch (reg) {
    case 0x01: x_cur = x_min = data; return;
    case 0x02: x_max = data; return;
    case 0x03: y_cur = y_min = data; return;
    case 0x04: y_max = data; return;
  }
  Transmit(data);
}

void TFT_LTDC::Transmit(tft_data_t Data) {
  DrawPoint(x_cur, y_cur, Data);
  x_cur++;
  if (x_cur > x_max) {
    x_cur = x_min;
    y_cur++;
    if (y_cur > y_max) y_cur = y_min;
  }
}

void TFT_LTDC::WriteReg(uint16_t Reg) {
  reg = Reg;
}

void TFT_LTDC::TransmitDMA(uint32_t MemoryIncrease, uint16_t *Data, uint16_t Count) {

  while (x_cur != x_min && Count) {
    Transmit(*Data);
    if (MemoryIncrease == DMA_PINC_ENABLE) Data++;
    Count--;
  }

  uint16_t width = x_max - x_min + 1;
  uint16_t height = Count / width;
  uint16_t x_end_cnt = Count - (width * height);

  if (height) {
    if (MemoryIncrease == DMA_PINC_ENABLE) {
      DrawImage(x_min, y_cur, x_min + width, y_cur + height, Data);
      Data += width * height;
    } else {
      DrawRect(x_min, y_cur, x_min + width, y_cur + height, *Data);
    }
    y_cur += height;
  }

  while (x_end_cnt) {
    Transmit(*Data);
    if (MemoryIncrease == DMA_PINC_ENABLE) Data++;
    x_end_cnt--;
  }
}

#endif // HAS_LTDC_TFT
#endif // ARDUINO_ARCH_STM32 && !STM32GENERIC
