/*
 * Copyright (c) 2021 Arm Limited and Contributors. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 * 
 * ST7789 TFT LCD Display Driver
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "hardware/gpio.h"
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#include "st7789.h"

// 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_VSCRDEF    0x33
#define ST7789_MADCTL     0x36
#define ST7789_VSCRSADD   0x37
#define ST7789_COLMOD     0x3A

#define ST7789_MADCTL_MY  0x80
#define ST7789_MADCTL_MX  0x40
#define ST7789_MADCTL_MV  0x20
#define ST7789_MADCTL_ML  0x10
#define ST7789_MADCTL_RGB 0x00

#define ST7789_RDID1      0xDA
#define ST7789_RDID2      0xDB
#define ST7789_RDID3      0xDC
#define ST7789_RDID4      0xDD

static const struct st7789_config *config;
static uint16_t width;
static uint16_t height;
static uint16_t x_offset;
static uint16_t y_offset;

static inline void cs_select() {
    if (config->gpio_cs >= 0) {
        asm volatile("nop \n nop \n nop");
        gpio_put(config->gpio_cs, 0);
        asm volatile("nop \n nop \n nop");
    }
}

static inline void cs_deselect() {
    if (config->gpio_cs >= 0) {
        asm volatile("nop \n nop \n nop");
        gpio_put(config->gpio_cs, 1);
        asm volatile("nop \n nop \n nop");
    }
}

static inline void dc_command() {
    asm volatile("nop \n nop \n nop");
    gpio_put(config->gpio_dc, 0);
    asm volatile("nop \n nop \n nop");
}

static inline void dc_data() {
    asm volatile("nop \n nop \n nop");
    gpio_put(config->gpio_dc, 1);
    asm volatile("nop \n nop \n nop");
}

static inline void reset_pulse() {
    gpio_put(config->gpio_rst, 1);
    sleep_ms(5);
    gpio_put(config->gpio_rst, 0);
    sleep_ms(20);
    gpio_put(config->gpio_rst, 1);
    sleep_ms(150);
}

static void write_command(uint8_t cmd) {
    dc_command();
    cs_select();
    spi_write_blocking(config->spi, &cmd, 1);
    cs_deselect();
}

static void write_data(const uint8_t *data, size_t len) {
    dc_data();
    cs_select();
    spi_write_blocking(config->spi, data, len);
    cs_deselect();
}

static void write_command_with_data(uint8_t cmd, const uint8_t *data, size_t len) {
    write_command(cmd);
    write_data(data, len);
}

static void set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
    uint8_t data[4];

    // Add offsets for display positioning
    x0 += x_offset;
    x1 += x_offset;
    y0 += y_offset;
    y1 += y_offset;

    // Column address set
    data[0] = (x0 >> 8) & 0xFF;
    data[1] = x0 & 0xFF;
    data[2] = (x1 >> 8) & 0xFF;
    data[3] = x1 & 0xFF;
    write_command_with_data(ST7789_CASET, data, 4);

    // Row address set
    data[0] = (y0 >> 8) & 0xFF;
    data[1] = y0 & 0xFF;
    data[2] = (y1 >> 8) & 0xFF;
    data[3] = y1 & 0xFF;
    write_command_with_data(ST7789_RASET, data, 4);

    // Write to RAM
    write_command(ST7789_RAMWR);
}

void st7789_init(const struct st7789_config *c, uint16_t w, uint16_t h) {
    config = c;
    width = w;
    height = h;
    
    // Set offsets for 240x240 display on ST7789
    // The Adafruit 1.54" TFT needs these offsets
    x_offset = 0;
    y_offset = 80;  // 240x240 display with 80 pixel row offset

    // Initialize SPI
    spi_init(config->spi, 62500 * 1000); // 62.5 MHz
    gpio_set_function(config->gpio_din, GPIO_FUNC_SPI);
    gpio_set_function(config->gpio_clk, GPIO_FUNC_SPI);

    // Initialize CS pin
    if (config->gpio_cs >= 0) {
        gpio_init(config->gpio_cs);
        gpio_set_dir(config->gpio_cs, GPIO_OUT);
        gpio_put(config->gpio_cs, 1);
    }

    // Initialize DC pin
    gpio_init(config->gpio_dc);
    gpio_set_dir(config->gpio_dc, GPIO_OUT);

    // Initialize RST pin
    gpio_init(config->gpio_rst);
    gpio_set_dir(config->gpio_rst, GPIO_OUT);

    // Initialize backlight pin
    gpio_init(config->gpio_bl);
    gpio_set_dir(config->gpio_bl, GPIO_OUT);
    gpio_put(config->gpio_bl, 1); // Turn on backlight

    // Reset display
    reset_pulse();

    // Software reset
    write_command(ST7789_SWRESET);
    sleep_ms(150);

    // Out of sleep mode
    write_command(ST7789_SLPOUT);
    sleep_ms(10);

    // Set color mode to 16-bit (RGB565)
    uint8_t colmod_data = 0x05;
    write_command_with_data(ST7789_COLMOD, &colmod_data, 1);
    sleep_ms(10);

    // Memory data access control
    uint8_t madctl_data = ST7789_MADCTL_MX | ST7789_MADCTL_MY | ST7789_MADCTL_RGB;
    write_command_with_data(ST7789_MADCTL, &madctl_data, 1);

    // Normal display mode on
    write_command(ST7789_NORON);
    sleep_ms(10);

    // Display on
    write_command(ST7789_DISPON);
    sleep_ms(10);
}

void st7789_fill(uint16_t color) {
    set_window(0, 0, width - 1, height - 1);

    dc_data();
    cs_select();

    // Send color data in chunks for better performance
    uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
    uint32_t pixel_count = width * height;
    
    // Send pixels in batches
    uint8_t buffer[256];  // 128 pixels at a time
    for (int i = 0; i < 128; i++) {
        buffer[i * 2] = data[0];
        buffer[i * 2 + 1] = data[1];
    }
    
    uint32_t full_chunks = pixel_count / 128;
    uint32_t remaining = pixel_count % 128;
    
    for (uint32_t i = 0; i < full_chunks; i++) {
        spi_write_blocking(config->spi, buffer, 256);
    }
    
    if (remaining > 0) {
        spi_write_blocking(config->spi, buffer, remaining * 2);
    }

    cs_deselect();
}

void st7789_put(uint16_t color) {
    uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
    
    dc_data();
    cs_select();
    spi_write_blocking(config->spi, data, 2);
    cs_deselect();
}

void st7789_set_cursor(uint16_t x, uint16_t y) {
    set_window(x, y, width - 1, height - 1);
}

void st7789_write(const uint16_t *data, size_t len) {
    dc_data();
    cs_select();
    
    for (size_t i = 0; i < len; i++) {
        uint8_t bytes[2] = {(data[i] >> 8) & 0xFF, data[i] & 0xFF};
        spi_write_blocking(config->spi, bytes, 2);
    }
    
    cs_deselect();
}

void st7789_vertical_scroll(uint16_t row) {
    uint8_t data[2] = {(row >> 8) & 0xFF, row & 0xFF};
    write_command_with_data(ST7789_VSCRSADD, data, 2);
}

void st7789_draw_pixel(uint16_t x, uint16_t y, uint16_t color) {
    if (x >= width || y >= height) return;
    set_window(x, y, x, y);
    uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
    dc_data();
    cs_select();
    spi_write_blocking(config->spi, data, 2);
    cs_deselect();
}

void st7789_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
    // Top and bottom horizontal lines
    st7789_fill_rect(x, y, w, 1, color);
    st7789_fill_rect(x, y + h - 1, w, 1, color);
    // Left and right vertical lines
    st7789_fill_rect(x, y, 1, h, color);
    st7789_fill_rect(x + w - 1, y, 1, h, color);
}

void st7789_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
    if (x >= width || y >= height) return;
    if (x + w > width) w = width - x;
    if (y + h > height) h = height - y;
    
    set_window(x, y, x + w - 1, y + h - 1);
    
    dc_data();
    cs_select();
    
    uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
    uint32_t pixel_count = w * h;
    
    // Send pixels in batches
    uint8_t buffer[256];
    for (int i = 0; i < 128; i++) {
        buffer[i * 2] = data[0];
        buffer[i * 2 + 1] = data[1];
    }
    
    uint32_t full_chunks = pixel_count / 128;
    uint32_t remaining = pixel_count % 128;
    
    for (uint32_t i = 0; i < full_chunks; i++) {
        spi_write_blocking(config->spi, buffer, 256);
    }
    
    if (remaining > 0) {
        spi_write_blocking(config->spi, buffer, remaining * 2);
    }
    
    cs_deselect();
}

void st7789_draw_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color) {
    int16_t f = 1 - r;
    int16_t ddF_x = 1;
    int16_t ddF_y = -2 * r;
    int16_t x = 0;
    int16_t y = r;
    
    st7789_draw_pixel(x0, y0 + r, color);
    st7789_draw_pixel(x0, y0 - r, color);
    st7789_draw_pixel(x0 + r, y0, color);
    st7789_draw_pixel(x0 - r, y0, color);
    
    while (x < y) {
        if (f >= 0) {
            y--;
            ddF_y += 2;
            f += ddF_y;
        }
        x++;
        ddF_x += 2;
        f += ddF_x;
        
        st7789_draw_pixel(x0 + x, y0 + y, color);
        st7789_draw_pixel(x0 - x, y0 + y, color);
        st7789_draw_pixel(x0 + x, y0 - y, color);
        st7789_draw_pixel(x0 - x, y0 - y, color);
        st7789_draw_pixel(x0 + y, y0 + x, color);
        st7789_draw_pixel(x0 - y, y0 + x, color);
        st7789_draw_pixel(x0 + y, y0 - x, color);
        st7789_draw_pixel(x0 - y, y0 - x, color);
    }
}

void st7789_fill_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color) {
    int16_t f = 1 - r;
    int16_t ddF_x = 1;
    int16_t ddF_y = -2 * r;
    int16_t x = 0;
    int16_t y = r;
    
    st7789_fill_rect(x0 - r, y0, 2 * r + 1, 1, color);
    
    while (x < y) {
        if (f >= 0) {
            y--;
            ddF_y += 2;
            f += ddF_y;
        }
        x++;
        ddF_x += 2;
        f += ddF_x;
        
        st7789_fill_rect(x0 - x, y0 + y, 2 * x + 1, 1, color);
        st7789_fill_rect(x0 - x, y0 - y, 2 * x + 1, 1, color);
        st7789_fill_rect(x0 - y, y0 + x, 2 * y + 1, 1, color);
        st7789_fill_rect(x0 - y, y0 - x, 2 * y + 1, 1, color);
    }
}

void st7789_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
    int16_t dx = abs(x1 - x0);
    int16_t dy = abs(y1 - y0);
    int16_t sx = (x0 < x1) ? 1 : -1;
    int16_t sy = (y0 < y1) ? 1 : -1;
    int16_t err = dx - dy;
    
    while (1) {
        st7789_draw_pixel(x0, y0, color);
        
        if (x0 == x1 && y0 == y1) break;
        
        int16_t e2 = 2 * err;
        if (e2 > -dy) {
            err -= dy;
            x0 += sx;
        }
        if (e2 < dx) {
            err += dx;
            y0 += sy;
        }
    }
}
