/**
 * @file st7789.c
 * @brief ST7789 Display Driver - 270° Rotation (USB left)
 */

#include <string.h>
#include "esp_log.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "st7789.h"
#include "hardware_config.h"

static const char *TAG = "ST7789";

static spi_device_handle_t spi_handle = NULL;

// ST7789 Commands
#define ST7789_NOP      0x00
#define ST7789_SWRESET  0x01
#define ST7789_RDDID    0x04
#define ST7789_RDDST    0x09
#define ST7789_SLPIN    0x10
#define ST7789_SLPOUT   0x11
#define ST7789_PTLON    0x12
#define ST7789_NORON    0x13
#define ST7789_INVOFF   0x20
#define ST7789_INVON    0x21
#define ST7789_DISPOFF  0x28
#define ST7789_DISPON   0x29
#define ST7789_CASET    0x2A
#define ST7789_RASET    0x2B
#define ST7789_RAMWR    0x2C
#define ST7789_RAMRD    0x2E
#define ST7789_PTLAR    0x30
#define ST7789_COLMOD   0x3A
#define ST7789_MADCTL   0x36

// MADCTL bits
#define MADCTL_MY  0x80
#define MADCTL_MX  0x40
#define MADCTL_MV  0x20
#define MADCTL_ML  0x10
#define MADCTL_RGB 0x00
#define MADCTL_BGR 0x08
#define MADCTL_MH  0x04

static void st7789_send_cmd(uint8_t cmd)
{
    gpio_set_level(LCD_PIN_DC, 0);
    spi_transaction_t t = {
        .length = 8,
        .tx_buffer = &cmd,
        .flags = SPI_TRANS_USE_TXDATA
    };
    t.tx_data[0] = cmd;
    ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}

static void st7789_send_data(uint8_t data)
{
    gpio_set_level(LCD_PIN_DC, 1);
    spi_transaction_t t = {
        .length = 8,
        .tx_buffer = &data,
        .flags = SPI_TRANS_USE_TXDATA
    };
    t.tx_data[0] = data;
    ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}

static void st7789_send_data_buf(const uint8_t *data, size_t len)
{
    if (len == 0) return;
    gpio_set_level(LCD_PIN_DC, 1);
    spi_transaction_t t = {
        .length = len * 8,
        .tx_buffer = data
    };
    ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}

static void st7789_set_address_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
    st7789_send_cmd(ST7789_CASET);
    st7789_send_data(x0 >> 8);
    st7789_send_data(x0 & 0xFF);
    st7789_send_data(x1 >> 8);
    st7789_send_data(x1 & 0xFF);
    
    st7789_send_cmd(ST7789_RASET);
    st7789_send_data(y0 >> 8);
    st7789_send_data(y0 & 0xFF);
    st7789_send_data(y1 >> 8);
    st7789_send_data(y1 & 0xFF);
    
    st7789_send_cmd(ST7789_RAMWR);
}

static void st7789_backlight_init(void)
{
    ledc_timer_config_t timer_conf = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .timer_num = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_8_BIT,
        .freq_hz = 5000,
        .clk_cfg = LEDC_AUTO_CLK
    };
    ESP_ERROR_CHECK(ledc_timer_config(&timer_conf));
    
    ledc_channel_config_t channel_conf = {
        .gpio_num = LCD_PIN_BCKL,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL_0,
        .timer_sel = LEDC_TIMER_0,
        .duty = 0,
        .hpoint = 0
    };
    ESP_ERROR_CHECK(ledc_channel_config(&channel_conf));
}

esp_err_t st7789_init(void)
{
    ESP_LOGI(TAG, "Initializing ST7789 display...");
    
    gpio_config_t io_conf = {};
    io_conf.pin_bit_mask = (1ULL << LCD_PIN_DC) | (1ULL << LCD_PIN_RST);
    io_conf.mode = GPIO_MODE_OUTPUT;
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
    gpio_config(&io_conf);
    
    spi_bus_config_t buscfg = {
        .mosi_io_num = LCD_PIN_MOSI,
        .miso_io_num = LCD_PIN_MISO,
        .sclk_io_num = LCD_PIN_SCLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = LCD_WIDTH * LCD_HEIGHT * 2 + 8,
        .flags = SPICOMMON_BUSFLAG_MASTER,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
    
    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = LCD_PIXEL_CLOCK_HZ,
        .mode = 0,
        .spics_io_num = LCD_PIN_CS,
        .queue_size = 7,
        .flags = SPI_DEVICE_NO_DUMMY,
    };
    ESP_ERROR_CHECK(spi_bus_add_device(LCD_SPI_HOST, &devcfg, &spi_handle));
    
    st7789_backlight_init();
    st7789_set_backlight(0);
    
    gpio_set_level(LCD_PIN_RST, 0);
    vTaskDelay(pdMS_TO_TICKS(100));
    gpio_set_level(LCD_PIN_RST, 1);
    vTaskDelay(pdMS_TO_TICKS(100));
    
    st7789_send_cmd(ST7789_SWRESET);
    vTaskDelay(pdMS_TO_TICKS(150));
    
    st7789_send_cmd(ST7789_SLPOUT);
    vTaskDelay(pdMS_TO_TICKS(10));
    
    st7789_send_cmd(ST7789_COLMOD);
    st7789_send_data(0x55);
    vTaskDelay(pdMS_TO_TICKS(10));
    
    // MADCTL: 270° = 90° CCW = USB on LEFT side
    // MV=1 (Row/Column exchange) + MY=1 (Mirror Y)
    st7789_send_cmd(ST7789_MADCTL);
    st7789_send_data(MADCTL_MV | MADCTL_MY | MADCTL_RGB);
    
    st7789_send_cmd(ST7789_INVON);
    vTaskDelay(pdMS_TO_TICKS(10));
    
    st7789_send_cmd(ST7789_NORON);
    vTaskDelay(pdMS_TO_TICKS(10));
    
    st7789_send_cmd(ST7789_DISPON);
    vTaskDelay(pdMS_TO_TICKS(100));
    
    ESP_LOGI(TAG, "✓ ST7789 initialized (%dx%d - 270° rotated, USB left)", LCD_WIDTH, LCD_HEIGHT);
    
    st7789_set_backlight(LCD_BCKL_DEFAULT);
    st7789_fill_screen(0x0000);
    
    return ESP_OK;
}

void st7789_set_backlight(uint8_t brightness)
{
    if (brightness > 100) brightness = 100;
    uint32_t duty = (brightness * 255) / 100;
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}

void st7789_fill_screen(uint16_t color)
{
    st7789_set_address_window(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);
    
    uint16_t line_buf[LCD_WIDTH];
    for (int i = 0; i < LCD_WIDTH; i++) {
        line_buf[i] = (color >> 8) | (color << 8);
    }
    
    gpio_set_level(LCD_PIN_DC, 1);
    for (int y = 0; y < LCD_HEIGHT; y++) {
        spi_transaction_t t = {
            .length = LCD_WIDTH * 16,
            .tx_buffer = line_buf
        };
        ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
    }
}

void st7789_draw_pixel(int16_t x, int16_t y, uint16_t color)
{
    if (x < 0 || x >= LCD_WIDTH || y < 0 || y >= LCD_HEIGHT) return;
    
    st7789_set_address_window(x, y, x, y);
    
    uint8_t data[2] = {color >> 8, color & 0xFF};
    
    gpio_set_level(LCD_PIN_DC, 1);
    spi_transaction_t t = {
        .length = 16,
        .tx_buffer = data
    };
    ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}

void st7789_draw_buffer(const uint16_t *buffer, int16_t x, int16_t y, 
                        int16_t width, int16_t height)
{
    if (buffer == NULL || width <= 0 || height <= 0) return;
    
    if (x < 0 || y < 0 || x + width > LCD_WIDTH || y + height > LCD_HEIGHT) {
        ESP_LOGW(TAG, "Buffer out of bounds: (%d,%d) %dx%d", x, y, width, height);
        return;
    }
    
    st7789_set_address_window(x, y, x + width - 1, y + height - 1);
    
    size_t buf_size = width * height;
    uint16_t *swap_buf = heap_caps_malloc(buf_size * 2, MALLOC_CAP_DMA);
    if (swap_buf == NULL) {
        ESP_LOGE(TAG, "Failed to allocate swap buffer!");
        return;
    }
    
    for (size_t i = 0; i < buf_size; i++) {
        swap_buf[i] = (buffer[i] >> 8) | (buffer[i] << 8);
    }
    
    gpio_set_level(LCD_PIN_DC, 1);
    spi_transaction_t t = {
        .length = buf_size * 16,
        .tx_buffer = swap_buf
    };
    ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
    
    heap_caps_free(swap_buf);
}

void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y,
                                    int16_t width, int16_t height)
{
    if (buffer == NULL || width <= 0 || height <= 0) return;

    if (x < 0 || y < 0 || x + width > LCD_WIDTH || y + height > LCD_HEIGHT) {
        ESP_LOGW(TAG, "Buffer out of bounds: (%d,%d) %dx%d", x, y, width, height);
        return;
    }

    // Set address window once for entire region
    st7789_set_address_window(x, y, x + width - 1, y + height - 1);

    gpio_set_level(LCD_PIN_DC, 1);

    // Split into DMA-safe chunks (max 32KB per transfer to be safe)
    // SPI will auto-split into smaller DMA descriptors internally
    const size_t max_chunk_size = 32768;  // 32KB in bytes
    size_t total_bytes = width * height * 2;
    size_t offset = 0;

    while (offset < total_bytes) {
        size_t chunk_size = (total_bytes - offset > max_chunk_size) ?
                            max_chunk_size : (total_bytes - offset);

        spi_transaction_t t = {
            .length = chunk_size * 8,  // in bits
            .tx_buffer = (uint8_t*)buffer + offset
        };
        ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));

        offset += chunk_size;
    }
}
