/*
 * Copyright (c) 2021 Arm Limited and Contributors. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 * 
 * ST7796 TFT LCD Display Driver - Implementation
 * Based on https://github.com/giemma/RapsberryPiPico/blob/main/simpleST7796/simpleST7796.c
 * 
 * ==============================================================================
 * IMPLEMENTATION NOTES
 * ==============================================================================
 * 
 * This driver uses a minimal initialization approach that was discovered
 * through extensive debugging. The ST7796 controller is very sensitive to
 * initialization sequences, and many example codes include extended commands
 * that cause compatibility issues.
 * 
 * Key Design Decisions:
 * 
 * 1. MINIMAL INITIALIZATION:
 *    We only use essential commands (SWRESET, SLPOUT, COLMOD, MADCTL, DISPON)
 *    Extended commands (CSCON, gamma, power control) caused white screen issues
 *    This approach is more reliable across different ST7796 modules
 * 
 * 2. SPI SPEED:
 *    Currently set to 100 MHz (can be adjusted in st7796_init)
 *    ST7796 datasheet specifies max 15.15 MHz for SPI write, but in practice
 *    modern displays with level shifters work reliably at much higher speeds
 *    Tested successfully at 80 MHz, attempting 100 MHz
 * 
 * 3. BUFFER OPTIMIZATION:
 *    Uses 512-byte buffers (256 pixels) for bulk transfers
 *    This balances memory usage with SPI efficiency
 *    Larger buffers don't significantly improve performance
 * 
 * 4. COLOR FORMAT:
 *    RGB565 (16-bit color) is used throughout
 *    Bytes are sent MSB first: [R4R3R2R1R0G5G4G3][G2G1G0B4B3B2B1B0]
 * 
 * 5. COORDINATE SYSTEM:
 *    Origin (0,0) is top-left corner
 *    X increases rightward (0-479 in landscape)
 *    Y increases downward (0-319 in landscape)
 *    MADCTL=0xE0 provides proper landscape orientation
 * 
 * Debugging History:
 * - Initial attempt with full initialization → white screen
 * - Removed CSCON and extended commands → display working
 * - Adjusted MADCTL for orientation → proper landscape mode
 * - Optimized SPI speed from 62.5 → 80 → 100 MHz
 * 
 * ==============================================================================
 */

#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 "st7796.h"

// ST7796 Standard LCD Commands
// These are common across most ST7796 controllers
#define ST7796_NOP        0x00  // No Operation
#define ST7796_SWRESET    0x01  // Software Reset
#define ST7796_SLPIN      0x10  // Sleep In (enter low power mode)
#define ST7796_SLPOUT     0x11  // Sleep Out (exit low power mode)
#define ST7796_PTLON      0x12  // Partial Display Mode On
#define ST7796_NORON      0x13  // Normal Display Mode On
#define ST7796_INVOFF     0x20  // Display Inversion Off
#define ST7796_INVON      0x21  // Display Inversion On
#define ST7796_DISPOFF    0x28  // Display Off (blank screen, keep framebuffer)
#define ST7796_DISPON     0x29  // Display On (show framebuffer)
#define ST7796_CASET      0x2A  // Column Address Set (X range)
#define ST7796_RASET      0x2B  // Row Address Set (Y range)
#define ST7796_RAMWR      0x2C  // Memory Write (send pixel data)
#define ST7796_RAMRD      0x2E  // Memory Read (read pixel data)
#define ST7796_MADCTL     0x36  // Memory Access Control (rotation, mirroring)
#define ST7796_COLMOD     0x3A  // Pixel Format Set (color depth)

// ST7796 Extended Commands
// These are manufacturer-specific and may vary between modules
// Currently UNUSED in our minimal initialization
#define ST7796_CSCON      0xF0  // Command Set Control (switch command sets)
#define ST7796_IFMODE     0xB0  // Interface Mode Control
#define ST7796_FRMCTR1    0xB1  // Frame Rate Control (In Normal Mode)
#define ST7796_DIC        0xB4  // Display Inversion Control
#define ST7796_BPC        0xB5  // Blanking Porch Control
#define ST7796_DFC        0xB6  // Display Function Control
#define ST7796_EM         0xB7  // Entry Mode Set
#define ST7796_PWR2       0xC2  // Power Control 2
#define ST7796_VCMPCTL    0xC5  // VCOM Control
#define ST7796_DOCA       0xE8  // Display Output Ctrl Adjust
#define ST7796_PGC        0xE0  // Positive Gamma Control
#define ST7796_NGC        0xE1  // Negative Gamma Control

// MADCTL (0x36) bit definitions
// These control display orientation and color order
#define ST7796_MADCTL_MY  0x80  // Row Address Order (0=top-to-bottom, 1=bottom-to-top)
#define ST7796_MADCTL_MX  0x40  // Column Address Order (0=left-to-right, 1=right-to-left)
#define ST7796_MADCTL_MV  0x20  // Row/Column Exchange (0=normal, 1=swap X/Y)
#define ST7796_MADCTL_ML  0x10  // Vertical Refresh Order (0=top-to-bottom, 1=bottom-to-top)
#define ST7796_MADCTL_RGB 0x00  // RGB color order
#define ST7796_MADCTL_BGR 0x08  // BGR color order

// Global state variables
// These hold the current display configuration

// Global state variables
// These hold the current display configuration
static struct st7796_config config_storage;  // Static storage for config copy
static const struct st7796_config *config;   // Pin and SPI configuration
static uint16_t width;      // Display width in pixels (e.g., 480)
static uint16_t height;     // Display height in pixels (e.g., 320)
static uint16_t x_offset;   // X offset for display alignment (currently 0)
static uint16_t y_offset;   // Y offset for display alignment (currently 0)

/**
 * @brief Activate chip select (pull CS LOW)
 * 
 * The ST7796 is selected when CS is LOW. The NOP instructions provide
 * a small delay to ensure clean signal transitions at high SPI speeds.
 */
static inline void cs_select() {
    if (config->gpio_cs >= 0) {
        asm volatile("nop \n nop \n nop");  // Small delay for signal stability
        gpio_put(config->gpio_cs, 0);       // Pull CS LOW (active)
        asm volatile("nop \n nop \n nop");
    }
}

/**
 * @brief Deactivate chip select (pull CS HIGH)
 * 
 * When CS is HIGH, the ST7796 ignores SPI communications. This allows
 * multiple devices to share the same SPI bus.
 */
static inline void cs_deselect() {
    if (config->gpio_cs >= 0) {
        asm volatile("nop \n nop \n nop");
        gpio_put(config->gpio_cs, 1);       // Pull CS HIGH (inactive)
        asm volatile("nop \n nop \n nop");
    }
}

/**
 * @brief Set DC pin for COMMAND mode
 * 
 * When DC is LOW, the next byte sent over SPI is interpreted as a command.
 * Commands tell the display what operation to perform (e.g., set window,
 * write pixel data, change settings).
 */
static inline void dc_command() {
    asm volatile("nop \n nop \n nop");
    gpio_put(config->gpio_dc, 0);  // DC LOW = Command mode
    asm volatile("nop \n nop \n nop");
}

/**
 * @brief Set DC pin for DATA mode
 * 
 * When DC is HIGH, the next bytes sent over SPI are interpreted as data
 * (e.g., pixel colors, configuration parameters for the previous command).
 */
static inline void dc_data() {
    asm volatile("nop \n nop \n nop");
    gpio_put(config->gpio_dc, 1);  // DC HIGH = Data mode
    asm volatile("nop \n nop \n nop");
}

/**
 * @brief Hardware reset sequence
 * 
 * The ST7796 has a hardware reset pin (RST) that performs a full reset
 * of the controller. This is more reliable than software reset for
 * recovering from unknown states.
 * 
 * Timing: HIGH(5ms) → LOW(15ms) → HIGH(15ms)
 * After reset, the display is in a known initial state and ready for
 * initialization commands.
 */
static inline void reset_pulse() {
    gpio_put(config->gpio_rst, 1);
    sleep_ms(5);
    gpio_put(config->gpio_rst, 0);  // Hold LOW for reset
    sleep_ms(15);
    gpio_put(config->gpio_rst, 1);  // Release reset
    sleep_ms(15);                   // Wait for display to initialize
}

/**
 * @brief Send a single command byte
 * 
 * Commands are single-byte opcodes that tell the display what to do.
 * Some commands take parameters (sent via write_data after the command).
 * 
 * @param cmd Command byte (e.g., ST7796_RAMWR, ST7796_CASET)
 */
static void write_command(uint8_t cmd) {
    dc_command();                           // Set DC LOW for command
    cs_select();                            // Activate display
    spi_write_blocking(config->spi, &cmd, 1);  // Send command byte
    cs_deselect();                          // Deactivate display
}

/**
 * @brief Send data bytes
 * 
 * Data bytes are parameters for the previous command, or pixel data
 * when following a RAMWR command.
 * 
 * @param data Pointer to data buffer
 * @param len Number of bytes to send
 */
static void write_data(const uint8_t *data, size_t len) {
    dc_data();                                    // Set DC HIGH for data
    cs_select();                                  // Activate display
    spi_write_blocking(config->spi, data, len);   // Send data bytes
    cs_deselect();                                // Deactivate display
}

/**
 * @brief Send a command followed by data
 * 
 * Convenience function for commands that always take parameters.
 * Example: CASET (column address set) always needs 4 bytes (x0, x1).
 * 
 * @param cmd Command byte
 * @param data Pointer to parameter data
 * @param len Number of parameter bytes
 */
static void write_command_with_data(uint8_t cmd, const uint8_t *data, size_t len) {
    write_command(cmd);
    write_data(data, len);
}

/**
 * @brief Set drawing window (active pixel region)
 * 
 * The ST7796 has a "window" concept where you define a rectangular region,
 * then all subsequent RAMWR commands write pixels sequentially within that
 * window (left-to-right, top-to-bottom, wrapping at the right edge).
 * 
 * This is how all drawing operations work:
 * 1. Set window to desired region
 * 2. Send RAMWR command
 * 3. Stream pixel data
 * 
 * @param x0 Left column (inclusive)
 * @param y0 Top row (inclusive)
 * @param x1 Right column (inclusive)
 * @param y1 Bottom row (inclusive)
 * 
 * Note: Coordinates are automatically offset if x_offset/y_offset are set.
 * This compensates for displays where the physical screen doesn't align
 * with the controller's framebuffer (common with ST7789/ST7796).
 */
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
    // (Currently 0 for ST7796, but can be adjusted if needed)
    x0 += x_offset;
    x1 += x_offset;
    y0 += y_offset;
    y1 += y_offset;

    // CASET: Column Address Set (X coordinates)
    // Format: [x0_high, x0_low, x1_high, x1_low]
    data[0] = (x0 >> 8) & 0xFF;  // X start MSB
    data[1] = x0 & 0xFF;          // X start LSB
    data[2] = (x1 >> 8) & 0xFF;  // X end MSB
    data[3] = x1 & 0xFF;          // X end LSB
    write_command_with_data(ST7796_CASET, data, 4);

    // RASET: Row Address Set (Y coordinates)
    // Format: [y0_high, y0_low, y1_high, y1_low]
    data[0] = (y0 >> 8) & 0xFF;  // Y start MSB
    data[1] = y0 & 0xFF;          // Y start LSB
    data[2] = (y1 >> 8) & 0xFF;  // Y end MSB
    data[3] = y1 & 0xFF;          // Y end LSB
    write_command_with_data(ST7796_RASET, data, 4);

    // RAMWR: Memory Write
    // After this command, all data sent is interpreted as pixel colors
    write_command(ST7796_RAMWR);
}

/**
 * @brief Initialize the ST7796 display controller
 * 
 * This function performs the complete initialization sequence. It was developed
 * through extensive trial-and-error to find a minimal set of commands that work
 * reliably across different ST7796 modules.
 * 
 * Initialization Steps:
 * 1. Configure GPIO pins and SPI interface
 * 2. Hardware reset
 * 3. Software reset (SWRESET)
 * 4. Exit sleep mode (SLPOUT)
 * 5. Set pixel format to RGB565 (COLMOD)
 * 6. Set display orientation to landscape (MADCTL)
 * 7. Disable inversion (INVOFF)
 * 8. Enable normal display mode (NORON)
 * 9. Turn on display (DISPON)
 * 
 * Notable Omissions (these caused issues during development):
 * - Extended command set control (CSCON)
 * - Gamma correction (PGC, NGC)
 * - Power control registers (PWR2, VCMPCTL)
 * - Display function control (DFC)
 * 
 * These extended commands are manufacturer-specific and vary between ST7796
 * modules from different vendors. The minimal approach is more universal.
 * 
 * @param c Pointer to configuration structure with pin assignments
 * @param w Display width (480 for landscape)
 * @param h Display height (320 for landscape)
 */
void st7796_init(const struct st7796_config *c, uint16_t w, uint16_t h) {
    // Copy config to static storage to avoid dangling pointer
    memcpy(&config_storage, c, sizeof(struct st7796_config));
    config = &config_storage;
    width = w;
    height = h;
    
    // Set offsets for 480x320 display
    // ST7796 controller has a 960x480 framebuffer, but most displays
    // only use a portion of it. Adjust these if your display is misaligned.
    x_offset = 0;
    y_offset = 0;

    // Initialize SPI at maximum stable speed for ST7796
    // Datasheet says max 15.15 MHz, but modern displays work much faster
    // Successfully tested at 80 MHz, now trying 100 MHz
    // If you see corruption, reduce to 80 MHz: spi_init(config->spi, 80000 * 1000)
    spi_init(config->spi, 100000 * 1000); // 100 MHz - try this first
    gpio_set_function(config->gpio_din, GPIO_FUNC_SPI);
    gpio_set_function(config->gpio_clk, GPIO_FUNC_SPI);

    // Initialize CS pin (Chip Select)
    // CS selects which device on the SPI bus is active
    if (config->gpio_cs >= 0) {
        gpio_init(config->gpio_cs);
        gpio_set_dir(config->gpio_cs, GPIO_OUT);
        gpio_put(config->gpio_cs, 1);  // Start HIGH (inactive)
    }

    // Initialize DC pin (Data/Command)
    // DC tells the display whether we're sending commands or data
    gpio_init(config->gpio_dc);
    gpio_set_dir(config->gpio_dc, GPIO_OUT);

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

    // Initialize backlight pin
    // Most TFT displays have LED backlights that need power
    if (config->gpio_bl >= 0) {
        gpio_init(config->gpio_bl);
        gpio_set_dir(config->gpio_bl, GPIO_OUT);
        gpio_put(config->gpio_bl, 1); // Turn on backlight immediately
    }

    // Hardware reset sequence
    // This ensures the display starts from a clean state
    reset_pulse();

    // === BEGIN MINIMAL ST7796 INITIALIZATION SEQUENCE ===
    // This is the result of extensive debugging. Adding more commands
    // often causes compatibility issues. Only modify if necessary.
    
    uint8_t data;
    
    // Software Reset - clears all registers to default values
    write_command(ST7796_SWRESET);
    sleep_ms(150);  // Must wait for reset to complete
    
    // Sleep Out - exits low power mode
    // Display is in sleep mode after reset to save power
    write_command(ST7796_SLPOUT);
    sleep_ms(120);  // Must wait for oscillator to stabilize
    
    // Interface Pixel Format - set color depth
    // 0x55 = 16 bits per pixel for both RGB and MCU interface
    // Format: 5 bits Red, 6 bits Green, 5 bits Blue (RGB565)
    data = 0x55;
    write_command_with_data(ST7796_COLMOD, &data, 1);
    sleep_ms(10);
    
    // Memory Data Access Control - set display orientation
    // This is CRITICAL for proper display orientation
    // 0xE0 = MY(1) + MX(1) + MV(1) for landscape mode
    //   MY = Row Address Order (flip vertical)
    //   MX = Column Address Order (flip horizontal)
    //   MV = Row/Column Exchange (rotate 90°)
    // Result: 480x320 landscape orientation with correct pixel mapping
    data = 0xE0;
    write_command_with_data(ST7796_MADCTL, &data, 1);
    sleep_ms(10);
    
    // Display Inversion - try ON for displays that need it
    // Some displays need INVON, others need INVOFF
    // If this doesn't work, try: write_command(ST7796_INVOFF);
    write_command(ST7796_INVON);
    sleep_ms(10);
    
    // Normal Display Mode On
    // Exits partial mode and idle mode (if active)
    write_command(ST7796_NORON);
    sleep_ms(10);
    
    // Display ON - start showing framebuffer contents
    // After this, display is active and ready for drawing
    write_command(ST7796_DISPON);
    sleep_ms(120);  // Wait for display to stabilize
    
    // === END INITIALIZATION ===
    // Display is now ready. Framebuffer contains random data, so you should
    // call st7796_fill() to clear it or start drawing immediately.
}

/**
 * @brief Fill entire screen with a single color
 * 
 * This is the most efficient way to clear or set background color.
 * 
 * How it works:
 * 1. Set window to entire display (0,0) to (width-1, height-1)
 * 2. Send RAMWR command
 * 3. Stream 480*320 = 153,600 pixels as RGB565 values
 * 4. Use 512-byte buffer (256 pixels) for batch writes
 * 
 * Performance optimization:
 * - Pre-fills buffer with color
 * - Writes in 512-byte chunks to minimize SPI overhead
 * - At 100 MHz SPI: 153,600 pixels * 2 bytes = 307,200 bytes
 *   Transfer time: ~3ms + overhead = ~5-10ms total
 * 
 * @param color RGB565 color value (0x0000=black, 0xFFFF=white)
 */
void st7796_fill(uint16_t color) {
    set_window(0, 0, width - 1, height - 1);

    dc_data();
    cs_select();

    // Convert RGB565 to two bytes (MSB first)
    uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
    uint32_t pixel_count = width * height;  // 153,600 for 480x320
    
    // Create 512-byte buffer (256 pixels worth)
    // This is the sweet spot for performance vs memory usage
    uint8_t buffer[512];
    for (int i = 0; i < 256; i++) {
        buffer[i * 2] = data[0];      // MSB
        buffer[i * 2 + 1] = data[1];  // LSB
    }
    
    // Send full 512-byte chunks
    uint32_t full_chunks = pixel_count / 256;  // 600 chunks
    uint32_t remaining = pixel_count % 256;     // 0 pixels (evenly divisible)
    
    for (uint32_t i = 0; i < full_chunks; i++) {
        spi_write_blocking(config->spi, buffer, 512);
    }
    
    // Send remaining pixels (if any)
    if (remaining > 0) {
        spi_write_blocking(config->spi, buffer, remaining * 2);
    }

    cs_deselect();
}

/**
 * @brief Write single pixel at current cursor position
 * 
 * Writes one RGB565 pixel at the current window position. The window
 * position auto-advances after each write. Rarely used directly.
 * 
 * @param color RGB565 color value
 */
void st7796_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();
}

/**
 * @brief Set cursor for subsequent pixel writes
 * 
 * Sets the window starting position for st7796_put() or st7796_write().
 * The window extends from (x,y) to the bottom-right corner of the display.
 * 
 * @param x Starting X coordinate
 * @param y Starting Y coordinate
 */
void st7796_set_cursor(uint16_t x, uint16_t y) {
    set_window(x, y, width - 1, height - 1);
}

/**
 * @brief Write array of pixels at current cursor
 * 
 * Writes multiple RGB565 pixels starting at the current window position.
 * Less efficient than fill_rect for solid colors due to per-pixel conversion.
 * 
 * Use case: Drawing images, sprites, or multi-color patterns
 * 
 * @param data Array of RGB565 color values
 * @param len Number of pixels to write
 */
void st7796_write(const uint16_t *data, size_t len) {
    dc_data();
    cs_select();
    
    // Convert each RGB565 value to two bytes
    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();
}

/**
 * @brief Draw single pixel at specific coordinates
 * 
 * This is the slowest drawing operation because it requires:
 * 1. Set 1x1 window (4-byte CASET, 4-byte RASET, RAMWR command)
 * 2. Write 2-byte color
 * 3. CS select/deselect overhead
 * 
 * Total: ~15-20 bytes of SPI traffic per pixel
 * Performance: ~100-200 pixels/second
 * 
 * Use sparingly. For multiple pixels, use line/rect functions.
 * 
 * @param x X coordinate (0 to width-1)
 * @param y Y coordinate (0 to height-1)
 * @param color RGB565 color value
 */
void st7796_draw_pixel(uint16_t x, uint16_t y, uint16_t color) {
    if (x >= width || y >= height) return;  // Bounds check
    
    set_window(x, y, x, y);  // 1x1 window
    
    uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
    dc_data();
    cs_select();
    spi_write_blocking(config->spi, data, 2);
    cs_deselect();
}

/**
 * @brief Draw rectangle outline
 * 
 * Draws a hollow rectangle by drawing 4 lines:
 * - Top edge: from (x,y) to (x+w-1, y)
 * - Bottom edge: from (x, y+h-1) to (x+w-1, y+h-1)
 * - Left edge: from (x,y) to (x, y+h-1)
 * - Right edge: from (x+w-1, y) to (x+w-1, y+h-1)
 * 
 * Implemented as fill_rect calls for efficiency (faster than pixel-by-pixel).
 * 
 * @param x Top-left X coordinate
 * @param y Top-left Y coordinate
 * @param w Width in pixels
 * @param h Height in pixels
 * @param color RGB565 color value
 */
void st7796_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
    // Top and bottom horizontal lines
    st7796_fill_rect(x, y, w, 1, color);           // Top
    st7796_fill_rect(x, y + h - 1, w, 1, color);   // Bottom
    
    // Left and right vertical lines
    st7796_fill_rect(x, y, 1, h, color);           // Left
    st7796_fill_rect(x + w - 1, y, 1, h, color);   // Right
}

/**
 * @brief Draw filled rectangle
 * 
 * This is one of the fastest drawing operations after fill(). Uses the
 * same buffered approach as fill() but for a smaller region.
 * 
 * How it works:
 * 1. Set window to rectangle bounds
 * 2. Stream w*h pixels using 512-byte buffer
 * 
 * Performance: Depends on size
 * - Small rects (< 256 pixels): ~1-2ms
 * - Large rects (> 10,000 pixels): ~5-10ms
 * 
 * @param x Top-left X coordinate
 * @param y Top-left Y coordinate
 * @param w Width in pixels
 * @param h Height in pixels
 * @param color RGB565 color value
 */
void st7796_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
    // Bounds checking and clipping
    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;
    
    // Use 512-byte buffer for faster transfers
    uint8_t buffer[512];
    for (int i = 0; i < 256; i++) {
        buffer[i * 2] = data[0];
        buffer[i * 2 + 1] = data[1];
    }
    
    uint32_t full_chunks = pixel_count / 256;
    uint32_t remaining = pixel_count % 256;
    
    for (uint32_t i = 0; i < full_chunks; i++) {
        spi_write_blocking(config->spi, buffer, 512);
    }
    
    if (remaining > 0) {
        spi_write_blocking(config->spi, buffer, remaining * 2);
    }
    
    cs_deselect();
}

/**
 * @brief Draw circle outline
 * 
 * Uses the Midpoint Circle Algorithm (Bresenham's circle algorithm).
 * This algorithm draws circles by calculating 8 symmetric points per iteration,
 * taking advantage of 8-way symmetry in a circle.
 * 
 * How it works:
 * 1. Start at (0, r) relative to center
 * 2. For each iteration, decide whether to move down (y--) based on error term
 * 3. Always move right (x++)
 * 4. Draw 8 symmetric points for each (x,y) calculated
 * 
 * The 8 symmetry points for center (x0, y0) and offset (x, y):
 * - (x0+x, y0+y), (x0-x, y0+y)  - Upper half
 * - (x0+x, y0-y), (x0-x, y0-y)  - Lower half
 * - (x0+y, y0+x), (x0-y, y0+x)  - Right side
 * - (x0+y, y0-x), (x0-y, y0-x)  - Left side
 * 
 * Performance: O(r) - proportional to radius
 * Draws 8 pixels per iteration, so ~r/8 iterations
 * At ~150 pixels/second, a circle with r=50 takes ~0.4 seconds
 * 
 * @param x0 Center X coordinate
 * @param y0 Center Y coordinate
 * @param r Radius in pixels
 * @param color RGB565 color value
 */
void st7796_draw_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color) {
    int16_t f = 1 - r;      // Decision variable
    int16_t ddF_x = 1;      // Delta decision variable for X
    int16_t ddF_y = -2 * r; // Delta decision variable for Y
    int16_t x = 0;
    int16_t y = r;
    
    // Draw initial 4 points (cardinal directions)
    st7796_draw_pixel(x0, y0 + r, color);  // Bottom
    st7796_draw_pixel(x0, y0 - r, color);  // Top
    st7796_draw_pixel(x0 + r, y0, color);  // Right
    st7796_draw_pixel(x0 - r, y0, color);  // Left
    
    // Draw remaining points using 8-way symmetry
    while (x < y) {
        if (f >= 0) {
            y--;
            ddF_y += 2;
            f += ddF_y;
        }
        x++;
        ddF_x += 2;
        f += ddF_x;
        
        // Draw 8 symmetric points
        st7796_draw_pixel(x0 + x, y0 + y, color);
        st7796_draw_pixel(x0 - x, y0 + y, color);
        st7796_draw_pixel(x0 + x, y0 - y, color);
        st7796_draw_pixel(x0 - x, y0 - y, color);
        st7796_draw_pixel(x0 + y, y0 + x, color);
        st7796_draw_pixel(x0 - y, y0 + x, color);
        st7796_draw_pixel(x0 + y, y0 - x, color);
        st7796_draw_pixel(x0 - y, y0 - x, color);
    }
}

/**
 * @brief Draw filled circle
 * 
 * Uses the same Midpoint Circle Algorithm but fills horizontal spans
 * instead of drawing individual pixels. This is much faster than
 * draw_circle because fill_rect uses optimized buffering.
 * 
 * For each Y level calculated by the algorithm, we draw a horizontal
 * line spanning from -x to +x. The 8-way symmetry means we draw 4
 * horizontal lines per iteration.
 * 
 * Performance: Much better than draw_circle
 * - Uses fill_rect for each scan line
 * - O(r) scan lines, each taking ~0.5-2ms depending on width
 * - A circle with r=50 takes ~0.1-0.2 seconds
 * 
 * @param x0 Center X coordinate
 * @param y0 Center Y coordinate
 * @param r Radius in pixels
 * @param color RGB565 color value
 */
void st7796_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;
    
    // Draw initial horizontal line through center
    st7796_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;
        
        // Draw 4 horizontal lines (using 8-way symmetry)
        st7796_fill_rect(x0 - x, y0 + y, 2 * x + 1, 1, color);  // Bottom outer
        st7796_fill_rect(x0 - x, y0 - y, 2 * x + 1, 1, color);  // Top outer
        st7796_fill_rect(x0 - y, y0 + x, 2 * y + 1, 1, color);  // Bottom inner
        st7796_fill_rect(x0 - y, y0 - x, 2 * y + 1, 1, color);  // Top inner
    }
}

/**
 * @brief Draw line between two points
 * 
 * Uses Bresenham's Line Algorithm - a classic computer graphics algorithm
 * that draws straight lines using only integer arithmetic.
 * 
 * How it works:
 * 1. Calculate delta X and delta Y (dx, dy)
 * 2. Determine step direction (sx, sy) - positive or negative
 * 3. Initialize error term = dx - dy
 * 4. Loop:
 *    - Draw pixel at current position
 *    - Update error term
 *    - Step in X or Y direction based on error
 * 
 * The algorithm ensures the line stays as close as possible to the true
 * mathematical line without using floating point math.
 * 
 * Performance: O(max(dx, dy)) - proportional to line length
 * Each pixel requires a draw_pixel call (~150 pixels/second)
 * A 100-pixel line takes ~0.66 seconds
 * 
 * Note: Could be optimized by batching pixels or using fill_rect for
 * horizontal/vertical lines.
 * 
 * @param x0 Start point X coordinate
 * @param y0 Start point Y coordinate
 * @param x1 End point X coordinate
 * @param y1 End point Y coordinate
 * @param color RGB565 color value
 */
void st7796_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
    int16_t dx = abs(x1 - x0);  // Horizontal distance
    int16_t dy = abs(y1 - y0);  // Vertical distance
    int16_t sx = (x0 < x1) ? 1 : -1;  // Step direction in X
    int16_t sy = (y0 < y1) ? 1 : -1;  // Step direction in Y
    int16_t err = dx - dy;  // Error term
    
    while (1) {
        st7796_draw_pixel(x0, y0, color);
        
        if (x0 == x1 && y0 == y1) break;  // Reached end point
        
        // Update position based on error term
        int16_t e2 = 2 * err;
        if (e2 > -dy) {
            err -= dy;
            x0 += sx;  // Step in X direction
        }
        if (e2 < dx) {
            err += dx;
            y0 += sy;  // Step in Y direction
        }
    }
}

/**
 * @brief Draw triangle outline
 * 
 * Simply draws three lines connecting the three vertices.
 * 
 * @param x0, y0 First vertex
 * @param x1, y1 Second vertex
 * @param x2, y2 Third vertex
 * @param color RGB565 color value
 */
void st7796_draw_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, 
                          uint16_t x2, uint16_t y2, uint16_t color) {
    st7796_draw_line(x0, y0, x1, y1, color);  // Side 1
    st7796_draw_line(x1, y1, x2, y2, color);  // Side 2
    st7796_draw_line(x2, y2, x0, y0, color);  // Side 3
}

/**
 * @brief Draw filled triangle
 * 
 * Uses a scanline fill algorithm. The triangle is filled by drawing
 * horizontal lines (scanlines) from top to bottom.
 * 
 * Algorithm:
 * 1. Sort vertices by Y coordinate (y0 <= y1 <= y2)
 * 2. Handle degenerate case (all points on same line)
 * 3. Split triangle into two parts at the middle Y coordinate:
 *    - Upper part: from y0 to y1
 *    - Lower part: from y1 to y2
 * 4. For each Y level, calculate left and right X boundaries
 * 5. Draw horizontal line between left and right boundaries
 * 
 * The algorithm uses linear interpolation to find X coordinates along
 * each edge of the triangle.
 * 
 * Performance: O(height) scanlines
 * Each scanline is a fill_rect, so relatively fast
 * A 100-pixel tall triangle takes ~0.1-0.2 seconds
 * 
 * @param x0, y0 First vertex
 * @param x1, y1 Second vertex
 * @param x2, y2 Third vertex
 * @param color RGB565 color value
 */
void st7796_fill_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
                          uint16_t x2, uint16_t y2, uint16_t color) {
    int16_t a, b, y, last;
    
    // Sort coordinates by Y order (y2 >= y1 >= y0)
    // Using bubble sort for 3 elements
    if (y0 > y1) {
        int16_t temp;
        temp = y0; y0 = y1; y1 = temp;
        temp = x0; x0 = x1; x1 = temp;
    }
    if (y1 > y2) {
        int16_t temp;
        temp = y2; y2 = y1; y1 = temp;
        temp = x2; x2 = x1; x1 = temp;
    }
    if (y0 > y1) {
        int16_t temp;
        temp = y0; y0 = y1; y1 = temp;
        temp = x0; x0 = x1; x1 = temp;
    }
    
    // Handle degenerate case: all vertices on same horizontal line
    if (y0 == y2) {
        a = b = x0;
        if (x1 < a)      a = x1;
        else if (x1 > b) b = x1;
        if (x2 < a)      a = x2;
        else if (x2 > b) b = x2;
        st7796_fill_rect(a, y0, b - a + 1, 1, color);
        return;
    }
    
    // Calculate edge slopes (as fixed-point)
    int32_t dx01 = x1 - x0;  // X delta from v0 to v1
    int32_t dy01 = y1 - y0;  // Y delta from v0 to v1
    int32_t dx02 = x2 - x0;  // X delta from v0 to v2 (long edge)
    int32_t dy02 = y2 - y0;
    int32_t dx12 = x2 - x1;  // X delta from v1 to v2
    int32_t dy12 = y2 - y1;
    int32_t sa = 0;  // Accumulated X for short edge
    int32_t sb = 0;  // Accumulated X for long edge
    
    // Upper part of triangle (from y0 to y1)
    last = (y1 == y2) ? y1 : y1 - 1;
    
    for (y = y0; y <= last; y++) {
        a = x0 + sa / dy01;  // Left boundary
        b = x0 + sb / dy02;  // Right boundary
        sa += dx01;
        sb += dx02;
        
        if (a > b) {  // Swap if needed
            int16_t temp = a;
            a = b;
            b = temp;
        }
        st7796_fill_rect(a, y, b - a + 1, 1, color);
    }
    
    // Lower part of triangle (from y1 to y2)
    sa = dx12 * (y - y1);
    sb = dx02 * (y - y0);
    
    for (; y <= y2; y++) {
        a = x1 + sa / dy12;  // Left boundary
        b = x0 + sb / dy02;  // Right boundary
        sa += dx12;
        sb += dx02;
        
        if (a > b) {
            int16_t temp = a;
            a = b;
            b = temp;
        }
        st7796_fill_rect(a, y, b - a + 1, 1, color);
    }
}
