scan.c
/* Scan Example
 
 This example code is in the Public Domain (or CC0 licensed, at your option.)
 
 Unless required by applicable law or agreed to in writing, this
 software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 CONDITIONS OF ANY KIND, either express or implied.
 */

/*
 This example shows how to scan for available set of APs.
 */


//Main ESP System Imports
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_heap_caps.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "esp_sleep.h"
#include "nvs_flash.h"
#include "esp_adc/adc_oneshot.h"
#include "driver/ledc.h"
#include "regex.h"

//ESP Flash File System
#include <stdio.h>
#include "esp_log.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "wear_levelling.h"
#include "esp_heap_caps.h"

//SDIO File System
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"

//I2S Audio
#include "driver/i2s_std.h"
#include "es8311.h"

//ESP SPI LCD
#include "driver/spi_master.h"
#include "driver/gpio.h"

#include "lvgl.h"
#include "esp_lvgl_port.h"

#include "rom/tjpgd.h"

// ESP-DSP library (must be added as a component)
#include "dsps_fft2r.h"
#include "dsps_fft_tables.h"
#include "dsps_view.h" // For windowing function
#include "dsps_wind.h"

// ESP-ADC
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"

//C Standard Library
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <limits.h>
#include <unistd.h>
#include <math.h>
#include <setjmp.h>
#include <ctype.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <libgen.h>
#include <regex.h>
#include <pthread.h>
//#include <curl.h>
#include <sys/termios.h>
#include <fcntl.h>
#include <sys/select.h>
#include <dirent.h>

//Objective CC
#include <ObjectiveCC.h>
#include <CPUGraphics.h>
#include <cJSON.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_CACHE_H
#include <png.h>
#include <zlib.h>
#include "jpeg_decoder.h"
#define MINIMP3_IMPLEMENTATION
#include "minimp3.h"

// -- ESP H264 Components --
#include "esp_h264_dec.h"
#include "esp_h264_dec_sw.h"
#include "esp_h264_types.h"

// -- ESP-IDF LCD Components --
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_dev.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include <esp_lcd_panel_vendor.h>
#include "esp_lcd_panel_st7789.h" // Specific driver for your controller
#include "esp_lcd_ili9488.h"

#include "driver/i2c_master.h"
#include "esp_lcd_touch.h"
#include "esp_lcd_touch_ft6x36.h"
#include "FT6336U.h"

static const char *TAG = "scan";
void checkAvailableMemory(void);

FT_Library ft_library;
FT_Face    ft_face;
uint8_t* font_buffer = NULL; // We must keep the font in memory

static const char *MOUNT_POINT = "/spiflash";
static const char *PARTITION_LABEL = "storage"; // MUST match partitions.csv entry

// Global handle for the Wear Leveling driver
wl_handle_t wl_handle = WL_INVALID_HANDLE;

// In the global scope of scan.c
Framebuffer fb;

// Define the commands
typedef enum {
    CMD_DRAW_RECT,       // NEW: Draw a solid rectangle
    CMD_DRAW_TEXT,       // Draws text
    CMD_DRAW_TEXT_BOX,
    CMD_DRAW_TEXT_BOX_CACHED,
    CMD_UPDATE_AREA,      // NEW: Send a part of the PSRAM to the LCD
    CMD_CURSOR_SETUP,      // NEW: Initializes cursor position and saves background
    CMD_CURSOR_DRAW,       // NEW: Draws the cursor (blink ON)
    CMD_CURSOR_RESTORE,   // NEW: Restores background (blink OFF)
    CMD_SCROLL_CONTENT,
    CMD_LOAD_PAGE_1,
    CMD_DRAW_STAR,
    CMD_DRAW_ROUNDED_RECT,
    CMD_DRAW_GRADIENT_RECT,
    CMD_DRAW_GRADIENT_ROUNDED_RECT,
    CMD_DRAW_IMAGE_FILE,
    CMD_DRAW_POLYGON,
    CMD_DRAW_PIXEL_BUFFER,
    CMD_ANIM_SAVE_BG,    // Copy Framebuffer -> Backup Buffer
    CMD_ANIM_RESTORE_BG,
    CMD_UI_SAVE_TO_A,      // Copy Screen -> Buffer A
    CMD_UI_RESTORE_FROM_A, // Copy Buffer A -> Screen
    CMD_UI_SAVE_TO_B,      // Copy Screen -> Buffer B
    CMD_UI_COPY_B_TO_A,     // Copy Buffer B -> Buffer A (To promote new icon to "Active")
    CMD_DRAW_ROUNDED_HAND,
    CMD_DRAW_DAY_NIGHT_OVERLAY
} GraphicsCommandType;

// Define the message struct
typedef struct {
    GraphicsCommandType cmd;
    int x, y, w, h;
    int clipX, clipY, clipW, clipH;
    int radius;
    
    // Text Specifics
    char *text; // Increased size for multi-line text
    ColorRGBA color;
    int fontSize;
    TextFormat textFormat; // Stores alignment, wrap, spacing
    
    // --- NEW POLYGON FIELDS ---
    Vector3* vertices;   // Pointer to array of Vector3 (allocated in Bridge, freed in Task)
    int numVertices;     // Number of points
    
    // Image/Gradient Specifics
    char imagePath[64]; // Path to the file (e.g., "/spiflash/icon.png")
    void* pixelBuffer;
    Gradient* gradientData;
    
    // --- TRANSFORM FIELD (NEW) ---
    Matrix3x3 transform; // Fixed 3x3 matrix (9 floats)
    bool hasTransform;   // Flag to trigger the transform draw function
    
    // General
    bool fill;
    int shadowSize;
} GraphicsCommand;

// The global queue handle
QueueHandle_t g_graphics_queue;

bool setupui = false;
bool touchEnabled = true;

void drawViewHierarchy(void* object, int parentX, int parentY, CCRect currentClip, bool notHierarchy);
CCView* find_subview_at_point(CCView* container, int globalX, int globalY);
// Global Root View
CCView* mainWindowView = NULL;
CCScrollView* g_active_scrollview = NULL;
int g_touch_last_y = 0;
int lastOffY = 0;
CCTextView* myDemoTextView = NULL;
// --- Global Scroll State ---
int g_text_scroll_y = 0;      // How far down we have scrolled
int g_text_total_height = 0;  // Total height of the text (calculated once)
int g_text_view_h = 300;      // Height of the visible box
const char* g_long_text_ptr = NULL; // Pointer to the long string
bool notFirstTouch = false;

//
// =================== UPDATED: FreeType Initialization ===================
//
esp_err_t initialize_freetype()
{
    ESP_LOGI(TAG, "Initializing FreeType...");
    FT_Error error = FT_Init_FreeType(&ft_library);
    if (error) {
        ESP_LOGE(TAG, "Failed to initialize FreeType library!");
        return ESP_FAIL;
    }
    
    // --- THIS IS THE NEW PART ---
    // Read the font file from the mounted /storage partition
    const char* font_path = "/spiflash/proximaNovaRegular.ttf";
    ESP_LOGI(TAG, "Loading font from %s", font_path);
    
    FILE* file = fopen(font_path, "rb");
    if (!file) {
        ESP_LOGE(TAG, "Failed to open font file. Make sure it's in your 'storage' folder.");
        return ESP_FAIL;
    }
    
    // Get file size
    fseek(file, 0, SEEK_END);
    long font_size = ftell(file);
    fseek(file, 0, SEEK_SET);
    
    // Allocate a buffer for the font.
    // FreeType needs this buffer to exist for as long as the font is used.
    //font_buffer = (uint8_t*)cc_safe_alloc(1, font_size);
    font_buffer = (uint8_t*)heap_caps_malloc(font_size, MALLOC_CAP_SPIRAM);
    if (!font_buffer) {
        ESP_LOGE(TAG, "Failed to allocate memory for font buffer (%ld bytes)", font_size);
        fclose(file);
        return ESP_FAIL;
    }
    
    // Read the file into the buffer
    fread(font_buffer, 1, font_size, file);
    fclose(file);
    
    // Load the font face from the memory buffer
    error = FT_New_Memory_Face(ft_library,
                               font_buffer, // Font buffer
                               font_size,   // Font size
                               0,           // Face index (0 for first face)
                               &ft_face);   // Output handle
    
    if (error) {
        ESP_LOGE(TAG, "Failed to load font face! FreeType error: 0x%X", error);
        // Free the buffer if we failed
        free(font_buffer);
        font_buffer = NULL;
        return ESP_FAIL;
    }
    
    ESP_LOGI(TAG, "FreeType initialized and font loaded.");
    return ESP_OK;
}

// --- FreeType Cache Globals ---
FTC_Manager     g_ftc_manager = NULL;
FTC_ImageCache  g_ftc_image_cache = NULL;
FTC_CMapCache   g_ftc_cmap_cache = NULL;

// --- Face Requester Callback ---
// The Cache Manager calls this automatically when it needs to load a font file.
// We use the file path string itself as the 'face_id'.
FT_Error face_requester(FTC_FaceID face_id, FT_Library library, FT_Pointer req_data, FT_Face* aface) {
    const char* font_path = (const char*)face_id;
    // printf("FTC: Loading font from flash: %s\n", font_path);
    return FT_New_Face(library, font_path, 0, aface);
}

// --- Initialize and Preload ---
void init_freetype_cache_system(void) {
    if (g_ftc_manager != NULL) return; // Prevent double init

    printf("Initializing FreeType Cache...\n");

    // 1. Create the Cache Manager
    // Max Faces: 2 (Keep low for embedded)
    // Max Sizes: 4 (e.g., 12, 18, 24, 30)
    // Max Bytes: 200KB (Adjust based on your available SPIRAM)
    FT_Error error = FTC_Manager_New(
                                     ft_library,
        2,              // max_faces
        4,              // max_sizes
        200 * 1024,     // max_bytes (200KB cache)
        face_requester, // Callback to load files
        NULL,           // req_data
        &g_ftc_manager
    );

    if (error) {
        printf("CRITICAL: Failed to create FTC Manager! Error: %d\n", error);
        return;
    }

    // 2. Create the Sub-Caches (Image = Glyphs, CMap = Char-to-Index)
    FTC_ImageCache_New(g_ftc_manager, &g_ftc_image_cache);
    FTC_CMapCache_New(g_ftc_manager, &g_ftc_cmap_cache);
    
    // --- 3. Preload a Specific Size (Warm Up) ---
    // This forces the I/O and parsing to happen NOW.
    const char* font_path = "/spiflash/proximaNovaRegular.ttf";
    int preload_size = 12;

    printf("Preloading Font: %s @ %dpx\n", font_path, preload_size);
    
    FTC_ScalerRec scaler;
    scaler.face_id = (FTC_FaceID)font_path; // Cast path to ID
    scaler.width = 0;
    scaler.height = preload_size;
    scaler.pixel = 1; // 1 = Size is pixels
    scaler.x_res = 0;
    scaler.y_res = 0;

    FT_Size size;
    // Looking up the size forces the manager to load the file and set the scale
    if (FTC_Manager_LookupSize(g_ftc_manager, &scaler, &size) != 0) {
        printf("WARNING: Failed to preload font size.\n");
    } else {
        printf("Font preloaded successfully. Ready for fast rendering.\n");
    }
}


#define MAX_VIEW_STACK 5

// A simple stack to hold pointers to previous views
static CCView* viewStack[MAX_VIEW_STACK];
static int viewStackPointer = 0;

void push_view(CCView* currentView) {
    if (viewStackPointer < MAX_VIEW_STACK) {
        viewStack[viewStackPointer++] = currentView;
        printf("View pushed to stack. Stack size: %d\n", viewStackPointer);
    } else {
        printf("Error: View stack full!\n");
    }
}

CCView* pop_view() {
    if (viewStackPointer > 0) {
        viewStackPointer--;
        printf("View popped from stack. Remaining: %d\n", viewStackPointer);
        return viewStack[viewStackPointer];
    }
    return NULL; // Stack is empty
}

typedef enum {
    CurrentViewHome = 0,
    CurrentViewFiles,    // Default (starts at X)
    CurrentViewSettings,      // Centers text within clipWidth
    CurrentViewText,        // Aligns text to the right edge of clipWidth
    CurrentViewMessages,
    CurrentViewPaint,
    CurrentViewClock,
    CurrentViewPhotos,
    CurrentViewMusic,
    CurrentViewCalculator,
    CurrentViewSearch,
    CurrentViewMaps,
    CurrentViewNetTools,
    CurrentViewAbout,
    CurrentViewLocale,
    CurrentViewCalendarClock,
    CurrentViewWifi,
    CurrentViewBluetooth
} CurrentView;

CurrentView currentView = CurrentViewHome;

void init_anim_buffer(void);
void update_full_ui(void);

bool openedApp = false;
CCArray* files = NULL;
CCArray* settings = NULL;

typedef struct {
    CCView* container;
    CCImageView* icon;
    CCLabel* label;
} CCIconView;

// --- Keyboard Globals ---
CCView* uiKeyboardView = NULL;   // The container for the keyboard
CCLabel* uiTargetLabel = NULL;   // The label currently receiving text
CCString* uiInputBuffer = NULL;  // The actual text string being edited

void teardown_keyboard_data(void) {
    // 1. Free the Input String Buffer (Prevents Memory Leak)
    if (uiInputBuffer) {
        freeCCString(uiInputBuffer);
        uiInputBuffer = NULL;
    }

    // 2. Clear the View Pointer (Prevents "Dangling Pointer" Crashes)
    // Note: We do NOT free(uiKeyboardView) here because freeViewHierarchy
    // will handle the actual memory when it frees the parent window.
    uiKeyboardView = NULL;

    // 3. Clear the Target (Prevents "Ghost Updates")
    uiTargetLabel = NULL;
    
    // 4. Reset Touch State (Prevents Stuck Keys)
    //reset_keyboard_touch_state();
}

void close_current_app(void) {
    // 1. Check if we have anywhere to go back to
    CCView* previousView = pop_view();
    
    if (previousView != NULL) {
        // 2. Clean up the current app (The Files App)
        // We created it with malloc/viewWithFrame, so we should free it
        // to prevent memory leaks now that we are done with it.
        // Assuming you have a function like freeViewHierarchy(CCView* v)
        teardown_keyboard_data();
        
        freeViewHierarchy(mainWindowView);
        
        CurrentView newCurrentView = -999;
        
        if (currentView == CurrentViewFiles) {
            freeElement(files);
        }
        else if (currentView == CurrentViewSettings) {
            freeElement(settings);
        }
        else if (currentView == CurrentViewWifi) {
            newCurrentView = CurrentViewSettings;
        }
        
        if (newCurrentView != -999) {
            currentView = newCurrentView;
        }
        else {
            currentView = CurrentViewHome;
        }
        
        openedApp = false;
        
        // 3. Restore the old view
        mainWindowView = previousView;
        
        // 4. Refresh
        printf("Restored previous view.\n");
        update_full_ui();
        
        
    } else {
        printf("Can't go back, stack empty.\n");
    }
}

/**
 * @brief Returns a CCArray of CCDictionaries containing file metadata.
 * Keys: "Name" (String), "Path" (String), "DateModified" (CCDate), "Size" (Number)
 */
CCArray* get_directory_files_as_array(const char *mount_point) {
    CCArray* fileList = array();
    
    DIR *dir = NULL;
    struct dirent *ent;
    char full_path[256];
    struct stat st;
    
    dir = opendir(mount_point);
    if (!dir) {
        ESP_LOGE(TAG, "Failed to open directory: %s", mount_point);
        return fileList;
    }
    
    while ((ent = readdir(dir)) != NULL) {
        if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
            continue;
        }
        
        if (mount_point[strlen(mount_point) - 1] == '/') {
            snprintf(full_path, sizeof(full_path), "%s%s", mount_point, ent->d_name);
        } else {
            snprintf(full_path, sizeof(full_path), "%s/%s", mount_point, ent->d_name);
        }
        
        if (stat(full_path, &st) == 0) {
            CCDictionary* fileDict = dictionary();
            
            // 1. Name & Path
            // Correct Order: (Dict, Value, Key)
            dictionarySetObjectForKey(fileDict, ccs(ent->d_name), ccs("Name"));
            dictionarySetObjectForKey(fileDict, ccs(full_path), ccs("Path"));
            
            // 2. Size
            // Correct Order: (Dict, Value, Key)
            dictionarySetObjectForKey(fileDict, numberWithInt((int)st.st_size), ccs("Size"));
            
            // 3. Date Modified
            // Correct Order: (Dict, Value, Key)
            CCDate* fileDate = dateWithTimeInterval((double)st.st_mtime);
            dictionarySetObjectForKey(fileDict, fileDate, ccs("DateModified"));
            
            // 4. Is Directory
            // Correct Order: (Dict, Value, Key)
            bool isDir = S_ISDIR(st.st_mode);
            dictionarySetObjectForKey(fileDict, numberWithInt(isDir ? 1 : 0), ccs("IsDirectory"));
            
            arrayAddObject(fileList, fileDict);
        } else {
            ESP_LOGW(TAG, "Failed to stat: %s", full_path);
        }
    }
    
    closedir(dir);
    return fileList;
}

void showTriangleAnimation(void);
void showRotatingImageAnimation(void);
void hideRotatingImageAnimation(void);
void rotating_image_task(void *pvParameter);
void setup_wifi_ui(void);

// --- WiFi Data Structures ---
#define MAX_WIFI_RESULTS 20

typedef struct {
    char ssid[33]; // 32 chars + null terminator
    int rssi;
    int channel;
    int auth_mode; // Useful later for lock icons
} WifiNetwork;

// Global access to scan results
extern WifiNetwork g_wifi_scan_results[MAX_WIFI_RESULTS];
extern int g_wifi_scan_count;

// Functions
void init_wifi_stack_once(void);
void trigger_wifi_scan(void);

#include <math.h>
// --- Global Animation State ---
// 200x200 pixels * 3 bytes = 120 KB (Must use PSRAM)
#define ANIM_W 200
#define ANIM_H 200
#define ANIM_X 60  // Center X (160) - Half Width (100)
#define ANIM_Y 140 // Center Y (240) - Half Height (100)

uint8_t* g_anim_backup_buffer = NULL;

// In scan.c (Global Scope)
#define MAX_INTERACT_W 80
#define MAX_INTERACT_H 100
#define BUFFER_SIZE (MAX_INTERACT_W * MAX_INTERACT_H * 3)

uint8_t* g_ui_backup_buffer_A = NULL; // Stores background of g_last_touched_icon
uint8_t* g_ui_backup_buffer_B = NULL; // Temp buffer for the new icon being touched

void init_ui_buffers() {
    g_ui_backup_buffer_A = (uint8_t*)heap_caps_malloc(BUFFER_SIZE, MALLOC_CAP_SPIRAM);
    g_ui_backup_buffer_B = (uint8_t*)heap_caps_malloc(BUFFER_SIZE, MALLOC_CAP_SPIRAM);
    
    if (!g_ui_backup_buffer_A || !g_ui_backup_buffer_B) {
        ESP_LOGE(TAG, "Failed to allocate UI backup buffers!");
    }
}



CCPoint getAbsoluteOrigin(CCView* view) {
    float x = 0;
    float y = 0;
    
    CCView* current = view;
    while (current != NULL) {
        x += current->frame->origin->x;
        y += current->frame->origin->y;
        
        // --- FIX: Cast the pointer ---
        current = (CCView*)current->superview;
        // -----------------------------
    }
    return (CCPoint){ .x = x, .y = y };
}

// Calculates the actual visible rectangle of a view on screen,
// accounting for all parent offsets AND parent clipping masks.
CCRect getAbsoluteVisibleRect(CCView* view) {
    if (!view) return *ccRect(0,0,0,0);
    
    // 1. Start with the view's own bounds relative to itself
    float x = 0;
    float y = 0;
    float w = view->frame->size->width;
    float h = view->frame->size->height;
    
    // 2. Walk up the tree to transform coordinates and apply clips
    CCView* current = view;
    
    // Accumulate offsets to get to screen space
    // We have to do this carefully:
    // The clipping rects of parents are in THEIR coordinate space,
    // so we need to track where 'current' is relative to screen.
    
    // Actually, it's easier to calculate the Absolute Rect of the view first,
    // then walk up and intersect with Absolute Rects of parents.
    
    CCPoint absOrigin = getAbsoluteOrigin(view); // Use your existing helper
    
    float finalX = absOrigin.x;
    float finalY = absOrigin.y;
    float finalW = w;
    float finalH = h;
    
    // Now walk up to check clipping
    current = (CCView*)view->superview;
    while (current != NULL) {
        if (current->layer->masksToBounds) {
            CCPoint parentOrigin = getAbsoluteOrigin(current);
            
            // Parent's Visible Absolute Rect
            float pX = parentOrigin.x;
            float pY = parentOrigin.y;
            float pW = current->frame->size->width;
            float pH = current->frame->size->height;
            
            // Intersect (Math logic expanded here for clarity)
            float interLeft = (finalX > pX) ? finalX : pX;
            float interTop  = (finalY > pY) ? finalY : pY;
            float interRight = (finalX + finalW < pX + pW) ? (finalX + finalW) : (pX + pW);
            float interBottom = (finalY + finalH < pY + pH) ? (finalY + finalH) : (pY + pH);
            
            if (interLeft < interRight && interTop < interBottom) {
                finalX = interLeft;
                finalY = interTop;
                finalW = interRight - interLeft;
                finalH = interBottom - interTop;
            } else {
                // Completely hidden
                return *ccRect(0,0,0,0);
            }
        }
        current = (CCView*)current->superview;
    }
    
    return *ccRect(finalX, finalY, finalW, finalH);
}

// Add this to scan.c

void update_label_safe(CCLabel* label) {
    if (!label || !label->view) return;

    // 1. Get Coordinates
    CCPoint origin = getAbsoluteOrigin(label->view);
    int x = (int)origin.x;
    int y = (int)origin.y;
    int w = (int)label->view->frame->size->width;
    int h = (int)label->view->frame->size->height;
    
    printf("\n  update_label_safe %d %d  \n", w , h);

    // 2. Erase Old Text (Draw Background)
    // We assume the label is opaque (backgroundColor is set).
    GraphicsCommand cmd_bg = {
        .cmd = CMD_DRAW_RECT,
        .x = x, .y = y, .w = w, .h = h,
        .color = convert_cc_color(label->view->backgroundColor),
        // No clipping needed for self-update usually, or clip to self
        .clipX = 0, .clipY = 0, .clipW = 320, .clipH = 480
    };
    xQueueSend(g_graphics_queue, &cmd_bg, portMAX_DELAY);

    // 3. Draw New Text
    //CCLabel* label = (CCLabel*)object;
    ColorRGBA textCol = convert_cc_color(label->textColor);
    
    TextFormat fmt = { 0 };
    if (label->textAlignment == CCTextAlignmentCenter) fmt.alignment = TEXT_ALIGN_CENTER;
    else if (label->textAlignment == CCTextAlignmentRight) fmt.alignment = TEXT_ALIGN_RIGHT;
    else fmt.alignment = TEXT_ALIGN_LEFT;
    
    if (label->textVerticalAlignment == CCTextVerticalAlignmentCenter) fmt.valignment = TEXT_VALIGN_CENTER;
    else if (label->textVerticalAlignment == CCTextVerticalAlignmentTop) fmt.valignment = TEXT_VALIGN_TOP;
    else fmt.valignment = TEXT_VALIGN_BOTTOM;
    
    //ESP_LOGI(TAG, "drawViewHierarchy5 CMD_DRAW_TEXT_BOX valignment %d", fmt.valignment);
    
    if (label->lineBreakMode == CCLineBreakWordWrapping) fmt.wrapMode = TEXT_WRAP_MODE_WHOLE_WORD;
    else fmt.wrapMode = TEXT_WRAP_MODE_TRUNCATE;
    
    fmt.lineSpacing = (int)label->lineSpacing;
    fmt.glyphSpacing = (int)label->glyphSpacing;
    
    GraphicsCommand cmd_text = {
        .cmd = CMD_DRAW_TEXT_BOX_CACHED,
        .x = x, .y = y, .w = w, .h = h,
        .color = textCol,
        .fontSize = (int)label->fontSize,
        .textFormat = fmt,
        .clipX = 0,
        .clipY = 0,
        .clipW = 320,
        .clipH = 480
    };
    if (label->text && label->text->string) {
        // strdup allocates memory on heap and copies the string
        cmd_text.text = strdup(label->text->string);
    } else {
        cmd_text.text = NULL;
    }
    xQueueSend(g_graphics_queue, &cmd_text, portMAX_DELAY);
    
    // 4. Force Flush of ONLY this area (Optimization)
    // If you have a specific flush command, use it.
    // Otherwise, the Graphics Task usually flushes after processing.
    
    GraphicsCommand cmd_flush = {
        .cmd = CMD_UPDATE_AREA,
        .x = 0, .y = 0, .w = 320, .h = 480
    };
    xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
    
}

void update_view_area_via_parent(CCView* view) {
    if (!view || !view->superview) return;
    
    // 1. Calculate Child's Absolute Position (Target for Update)
    CCPoint childAbs = getAbsoluteOrigin(view);
    int targetX = (int)childAbs.x;
    int targetY = (int)childAbs.y;
    int targetW = (int)view->frame->size->width;
    int targetH = (int)view->frame->size->height;
    
    // --- EXPANSION (Optional, keep 0 for debugging) ---
    int expand = 0;
    int drawX = targetX - expand;
    int drawY = targetY - expand;
    int drawW = targetW + (expand * 2);
    int drawH = targetH + (expand * 2);
    
    // 2. Create Clip Rect
    CCRect* clipRectPtr = ccRect(drawX, drawY, drawW, drawH);
    if (!clipRectPtr) return;
    
    // 3. Find the Parent (We draw starting here)
    CCView* parent = (CCView*)view->superview;
    
    // 4. Calculate Parent's Absolute Position
    // We use the SAME helper function to guarantee coordinates match 'targetX/Y'
    CCPoint parentAbs = getAbsoluteOrigin(parent);
    
    // 5. Draw from Parent
    // We pass the Parent's absolute screen coordinates as the starting point.
    // drawViewHierarchy will take this (X,Y) and add the Child's relative (x,y)
    // resulting in exactly (targetX, targetY).
    drawViewHierarchy(parent, (int)parentAbs.x, (int)parentAbs.y, *clipRectPtr, true);
    
    freeCCRect(clipRectPtr);
    
    // 6. Safety Clamp for LCD
    if (drawX < 0) drawX = 0;
    if (drawY < 0) drawY = 0;
    if (drawX + drawW > 320) drawW = 320 - drawX;
    if (drawY + drawH > 480) drawH = 480 - drawY;
    
    // 7. Push Update
    /*GraphicsCommand cmd_flush = {
     .cmd = CMD_UPDATE_AREA,
     .x = drawX, .y = drawY, .w = drawW, .h = drawH
     };*/
    GraphicsCommand cmd_flush = {
        .cmd = CMD_UPDATE_AREA,
        .x = 0, .y = 0, .w = 320, .h = 480
    };
    xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
}


void update_view_only(CCView* view) {
    if (!view) return;
    
    // --- 1. Use the FULL Frame, not the "Visible/Clipped" Rect ---
    // The drawing logic likely generates the full view regardless of clipping.
    // We must ensure the hardware window is large enough to accept ALL that data.
    
    CCPoint absOrigin = getAbsoluteOrigin(view);
    int rawX = (int)absOrigin.x;
    int rawY = (int)absOrigin.y;
    int rawW = (int)view->frame->size->width;
    int rawH = (int)view->frame->size->height;
    
    // --- 5. Define the Clip Rect for the Renderer ---
    // We give the renderer the full padded area so it draws the borders.
    CCRect* clipRectPtr = ccRect(rawX, rawY, rawW, rawH);
    if (!clipRectPtr) return;
    
    // --- 6. Draw ---
    // Calculate parent-relative coordinates for the draw function
    int parentAbsX = rawX - (int)view->frame->origin->x;
    int parentAbsY = rawY - (int)view->frame->origin->y;
    
    // Use the PADDED size for the clip
    drawViewHierarchy(view, parentAbsX, parentAbsY, *clipRectPtr, true);
    
    freeCCRect(clipRectPtr);
    
    // --- 7. Flush the PADDED, ALIGNED Area ---
    GraphicsCommand cmd_flush = {
        .cmd = CMD_UPDATE_AREA,
        .x = 0,
        .y = 0,
        .w = 320,
        .h = 480
    };
    /*GraphicsCommand cmd_flush = {
     .cmd = CMD_UPDATE_AREA,
     .x = rawX,
     .y = rawY,
     .w = rawW,
     .h = rawH
     };*/
    xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
}

void update_full_ui(void) {
    if (!mainWindowView) return;
    touchEnabled = false;
    
    ESP_LOGI(TAG, "Starting UI Update...");
    
    // 1. Clear the screen first (Optional, but prevents ghosting)
    // We use the main window's background color for this
    ColorRGBA bgCol = convert_cc_color(mainWindowView->backgroundColor);
    GraphicsCommand cmd_clear = {
        .cmd = CMD_DRAW_RECT,
        .x = 0, .y = 0,
        .w = 320, .h = 480,
        .color = bgCol,
        .fill = true
    };
    // Use portMAX_DELAY to ensure we don't drop the clear command
    xQueueSend(g_graphics_queue, &cmd_clear, portMAX_DELAY);
    
    CCRect* screenRect = ccRect(0, 0, 320, 480);
    // 2. Walk the tree and generate all draw commands (Shadows, Borders, Views)
    // This calls the recursive function we wrote earlier
    drawViewHierarchy(mainWindowView, 0, 0, *screenRect, false);
    freeCCRect(screenRect);
    
    // 3. Push the pixels to the LCD (The Chunked Update)
    GraphicsCommand cmd_flush = {
        .cmd = CMD_UPDATE_AREA,
        .x = 0, .y = 0,
        .w = 320, .h = 480
    };
    xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
    
    ESP_LOGI(TAG, "UI Update Commands Sent.");
}

void update_full_ui1(void) {
    if (!mainWindowView) return;
    touchEnabled = false;
    
    ESP_LOGI(TAG, "Starting UI Update...");
    
    // 1. Clear the screen first (Optional, but prevents ghosting)
    // We use the main window's background color for this
    ColorRGBA bgCol = convert_cc_color(mainWindowView->backgroundColor);
    GraphicsCommand cmd_clear = {
        .cmd = CMD_DRAW_RECT,
        .x = 0, .y = 0,
        .w = 320, .h = 480,
        .color = bgCol,
        .fill = true
    };
    // Use portMAX_DELAY to ensure we don't drop the clear command
    xQueueSend(g_graphics_queue, &cmd_clear, portMAX_DELAY);
    
    CCRect* screenRect = ccRect(0, 0, 320, 480);
    // 2. Walk the tree and generate all draw commands (Shadows, Borders, Views)
    // This calls the recursive function we wrote earlier
    drawViewHierarchy(mainWindowView, 0, 0, *screenRect, true);
    freeCCRect(screenRect);
    
    // 3. Push the pixels to the LCD (The Chunked Update)
    GraphicsCommand cmd_flush = {
        .cmd = CMD_UPDATE_AREA,
        .x = 0, .y = 0,
        .w = 320, .h = 480
    };
    xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
    
    ESP_LOGI(TAG, "UI Update Commands Sent.");
}

void drawShapeLayer(CCShapeLayer* shapeLayer, int absX, int absY) {
    // 1. Basic Null Checks
    if (!shapeLayer) return;
    if (!shapeLayer->pointPath) return;
    if (!shapeLayer->pointPath->points) return;
    
    CCArray* points = shapeLayer->pointPath->points;
    int count = points->count;
    
    // 2. Minimum Vertex Check
    if (count < 3) return;
    
    // 3. Allocate Vertices
    Vector3* rawVertices = (Vector3*)cc_safe_alloc(1, sizeof(Vector3) * count);
    if (!rawVertices) {
        ESP_LOGE(TAG, "Failed to allocate vertices for shape layer");
        return;
    }
    
    // 4. Convert Coordinates with SAFETY CHECK
    for (int i = 0; i < count; i++) {
        CCPoint* pt = (CCPoint*)arrayObjectAtIndex(points, i);
        
        // --- SAFETY CHECK START ---
        if (pt == NULL) {
            ESP_LOGE(TAG, "Point at index %d is NULL! Aborting shape draw.", i);
            free(rawVertices); // Prevent memory leak
            return;
        }
        // --- SAFETY CHECK END ---
        
        rawVertices[i].x = (float)(absX + pt->x);
        rawVertices[i].y = (float)(absY + pt->y);
        rawVertices[i].z = 0.0f;
    }
    
    // 5. Handle Gradient or Solid Fill
    Gradient* lowLevelGrad = NULL;
    
    if (shapeLayer->gradient) {
        lowLevelGrad = create_low_level_gradient(shapeLayer->gradient);
    } else {
        // Create "Fake" Gradient for Solid Color
        // Ensure shapeLayer->fillColor is valid
        ColorRGBA solid;
        if (shapeLayer->fillColor) {
            solid = convert_cc_color(shapeLayer->fillColor);
        } else {
            // Default to magenta so you can see the error visibly
            solid = (ColorRGBA){255, 0, 255, 255};
        }
        
        lowLevelGrad = (Gradient*)cc_safe_alloc(1, sizeof(Gradient));
        if (lowLevelGrad) {
            lowLevelGrad->type = GRADIENT_TYPE_LINEAR;
            lowLevelGrad->angle = 0;
            lowLevelGrad->numStops = 2;
            lowLevelGrad->stops = (ColorStop*)cc_safe_alloc(1, sizeof(ColorStop) * 2);
            if (lowLevelGrad->stops) {
                lowLevelGrad->stops[0].color = solid;
                lowLevelGrad->stops[0].position = 0.0f;
                lowLevelGrad->stops[1].color = solid;
                lowLevelGrad->stops[1].position = 1.0f;
            }
        }
    }
    
    // 6. Send Command
    if (lowLevelGrad && lowLevelGrad->stops) {
        GraphicsCommand cmd_poly = {
            .cmd = CMD_DRAW_POLYGON,
            .vertices = rawVertices,
            .numVertices = count,
            .gradientData = lowLevelGrad
        };
        if (xQueueSend(g_graphics_queue, &cmd_poly, portMAX_DELAY) != pdTRUE) {
            ESP_LOGE(TAG, "Failed to send polygon command");
            free(rawVertices);
            if(lowLevelGrad->stops) free(lowLevelGrad->stops);
            free(lowLevelGrad);
        }
    } else {
        // Cleanup if gradient creation failed
        free(rawVertices);
        if (lowLevelGrad) free(lowLevelGrad);
    }
}

// Helper to find intersection of two rects (for clipping)
CCRect intersectRects(CCRect r1, CCRect r2) {
    float r1_min_x = r1.origin->x;
    float r1_min_y = r1.origin->y;
    float r1_max_x = r1_min_x + r1.size->width;
    float r1_max_y = r1_min_y + r1.size->height;
    
    float r2_min_x = r2.origin->x;
    float r2_min_y = r2.origin->y;
    float r2_max_x = r2_min_x + r2.size->width;
    float r2_max_y = r2_min_y + r2.size->height;
    
    float inter_min_x = (r1_min_x > r2_min_x) ? r1_min_x : r2_min_x;
    float inter_min_y = (r1_min_y > r2_min_y) ? r1_min_y : r2_min_y;
    float inter_max_x = (r1_max_x < r2_max_x) ? r1_max_x : r2_max_x;
    float inter_max_y = (r1_max_y < r2_max_y) ? r1_max_y : r2_max_y;
    
    if (inter_min_x < inter_max_x && inter_min_y < inter_max_y) {
        return *ccRect(inter_min_x, inter_min_y, inter_max_x - inter_min_x, inter_max_y - inter_min_y);
    }
    // No intersection (empty rect)
    return *ccRect(0, 0, 0, 0);
}

/**
 * @brief Recursively traverses a CCView tree and generates graphics commands.
 * * @param view The current view to render.
 * @param parentX The accumulated X coordinate of the parent view.
 * @param parentY The accumulated Y coordinate of the parent view.
 */
// Function Signature: Accepts void* to handle CCView, CCLabel, and CCImageView
void drawViewHierarchy(void* object, int parentX, int parentY, CCRect currentClip, bool notHierarchy) {
    if (!object) return;
    
    //ESP_LOGI(TAG, "CMD_DRAW_GRADIENT_ROUNDED_RECT %d %d %d %d", (int)currentClip.origin->x, (int)currentClip.origin->y, (int)currentClip.size->width, (int)currentClip.size->height);
    
    // 1. Identify Type
    // We safely cast to CCType* first because 'type' is the first member of ALL your structs.
    CCType type = *((CCType*)object);
    
    // 2. Unwrap the "Base View"
    // This is the common structure shared by all UI elements
    CCView* baseView = NULL;
    
    if (type == CCType_View) {
        baseView = (CCView*)object;
    }
    else if (type == CCType_ImageView) {
        baseView = ((CCImageView*)object)->view;
    }
    else if (type == CCType_Label) {
        baseView = ((CCLabel*)object)->view;
    }
    else {
        return; // Unknown type
    }
    
    // Safety check
    if (!baseView) return;
    
    //ESP_LOGI(TAG, "drawViewHierarchy");
    
    // 3. Calculate Absolute Position
    int absX = parentX + (int)baseView->frame->origin->x;
    int absY = parentY + (int)baseView->frame->origin->y;
    int w = (int)baseView->frame->size->width;
    int h = (int)baseView->frame->size->height;
    
    CCRect myEffectiveClip = currentClip;
    
    if (notHierarchy) {
        // ============================================================
        // USE 'baseView' FOR GEOMETRY, LAYERS, AND BACKGROUNDS
        // ============================================================
        
        
        
        if (baseView->frame->size->width == 80 && baseView->frame->size->height == 100) {
            //printf("DEBUG: drawViewHierarchy | View Abs Pos: X=%d Y=%d | Parent Passed: X=%d Y=%d\n",
            //       absX, absY, parentX, parentY);
        }
        
        // ============================================================
        // FIXED VISIBILITY CULLING (Include Shadows!)
        // ============================================================
        
        // 1. Calculate the "Visual Padding" needed
        int padLeft = 0, padRight = 0, padTop = 0, padBottom = 0;
        
        if (!baseView->layer->masksToBounds) {
            if (baseView->layer->shadowOpacity > 0.0f) {
                int r = (int)baseView->layer->shadowRadius;
                int offX = (int)baseView->layer->shadowOffset->x;
                int offY = (int)baseView->layer->shadowOffset->y;
                
                // Expand to cover the shadow direction + radius
                padLeft   = (offX < 0) ? r + abs(offX) : r;
                padRight  = (offX > 0) ? r + abs(offX) : r;
                padTop    = (offY < 0) ? r + abs(offY) : r;
                padBottom = (offY > 0) ? r + abs(offY) : r;
            }
            if (baseView->layer->borderWidth > 0) {
                int b = (int)baseView->layer->borderWidth;
                padLeft += b; padRight += b; padTop += b; padBottom += b;
            }
        }
        
        // 2. Define Clipping Bounds
        int clipLeft = (int)currentClip.origin->x;
        int clipTop = (int)currentClip.origin->y;
        int clipRight = clipLeft + (int)currentClip.size->width;
        int clipBottom = clipTop + (int)currentClip.size->height;
        
        // 3. Define Visual Bounds
        int visualLeft   = absX - padLeft;
        int visualTop    = absY - padTop;
        int visualRight  = absX + w + padRight;
        int visualBottom = absY + h + padBottom;
        
        // 4. Check Intersection
        bool intersects = (visualLeft < clipRight && visualRight > clipLeft &&
                           visualTop < clipBottom && visualBottom > clipTop);
        
        if (!intersects) {
            // printf("Skipping view outside dirty rect\n");
            return;
        }
        
        // 4. Clipping Logic
        // A. Calculate Clip for THIS VIEW (Background + Shadows)
        // -----------------------------------------------------
        // Start with the PARENT'S clip. This allows shadows to spill outside
        // the view's frame, provided they are still inside the parent.
        CCRect myEffectiveClip = currentClip;
        
        // Only restrict 'myself' to 'my frame' if masksToBounds is strictly TRUE.
        if (baseView->layer->masksToBounds) {
            CCRect* myFrameAbs = ccRect(absX, absY, w, h);
            CCRect intersected = intersectRects(myEffectiveClip, *myFrameAbs);
            myEffectiveClip = intersected;
            freeCCRect(myFrameAbs);
        }
        
        // Optimization: If we can't see the view or its shadow, stop here.
        if (myEffectiveClip.size->width <= 0 || myEffectiveClip.size->height <= 0) return;
        
        //ESP_LOGI(TAG, "drawViewHierarchy1");
    }
    else {
        
    }
    
    
    // 5. Geometry Properties
    int radius = (int)baseView->layer->cornerRadius;
    CCLayer* layer = baseView->layer;
    
    // --- STEP A: SHADOWS (Use baseView/layer) ---
    if (layer->shadowOpacity > 0.0f) {
        int blur = (int)layer->shadowRadius;
        int sh_x = absX + (int)layer->shadowOffset->x - blur;
        int sh_y = absY + (int)layer->shadowOffset->y - blur;
        int sh_w = w + (blur * 2);
        int sh_h = h + (blur * 2);
        int sh_radius = radius + blur;
        
        ColorRGBA baseColor = convert_cc_color(layer->shadowColor);
        ColorRGBA startColor = baseColor;
        startColor.a = (uint8_t)(layer->shadowOpacity * 255.0f);
        ColorRGBA endColor = baseColor;
        endColor.a = 0;
        
        Gradient* shadowGrad = (Gradient*)cc_safe_alloc(1, sizeof(Gradient));
        if (shadowGrad) {
            shadowGrad->type = GRADIENT_TYPE_BOX;
            shadowGrad->angle = 0.0f;
            shadowGrad->numStops = 2;
            shadowGrad->stops = (ColorStop*)cc_safe_alloc(1, sizeof(ColorStop) * 2);
            if (shadowGrad->stops) {
                shadowGrad->stops[0].color = startColor;
                shadowGrad->stops[0].position = 0.0f;
                shadowGrad->stops[1].color = endColor;
                shadowGrad->stops[1].position = 1.0f;
                
                if ((int)sh_radius == 0) {
                    sh_radius = 20.0;
                }
                
                GraphicsCommand cmd_shadow = {
                    .cmd = CMD_DRAW_GRADIENT_ROUNDED_RECT,
                    .x = sh_x, .y = sh_y, .w = sh_w, .h = sh_h,
                    .radius = sh_radius,
                    .gradientData = shadowGrad,
                    .fill = true,
                    .clipX = (int)myEffectiveClip.origin->x,
                    .clipY = (int)myEffectiveClip.origin->y,
                    .clipW = (int)myEffectiveClip.size->width,
                    .clipH = (int)myEffectiveClip.size->height
                };
                
                //ESP_LOGI(TAG, "CMD_DRAW_GRADIENT_ROUNDED_RECT %d %d %d %d %d %d %d %d ", (int)myEffectiveClip.origin->x, (int)myEffectiveClip.origin->y, (int)myEffectiveClip.size->width, (int)myEffectiveClip.size->height, absX, absY, w, h);
                
                xQueueSend(g_graphics_queue, &cmd_shadow, portMAX_DELAY);
            } else { free(shadowGrad); }
        }
    }
    
    //ESP_LOGI(TAG, "drawViewHierarchy2");
    
    // --- STEP B: BORDERS (Use baseView/layer) ---
    if (layer->borderWidth > 0.0f) {
        int b_width = (int)layer->borderWidth;
        ColorRGBA borderCol = convert_cc_color(layer->borderColor);
        
        GraphicsCommand cmd_border = {
            .cmd = CMD_DRAW_ROUNDED_RECT,
            .x = absX - b_width,
            .y = absY - b_width,
            .w = w + (b_width * 2),
            .h = h + (b_width * 2),
            .radius = radius + b_width,
            .color = borderCol,
            .fill = true,
            .clipX = (int)myEffectiveClip.origin->x,
            .clipY = (int)myEffectiveClip.origin->y,
            .clipW = (int)myEffectiveClip.size->width,
            .clipH = (int)myEffectiveClip.size->height
        };
        xQueueSend(g_graphics_queue, &cmd_border, portMAX_DELAY);
    }
    
    //ESP_LOGI(TAG, "drawViewHierarchy3");
    
    // --- STEP C: BACKGROUNDS (Use baseView) ---
    // FIX: Changed 'view->backgroundColor' to 'baseView->backgroundColor'
    if (layer->gradient != NULL) {
        Gradient* lowLevelGrad = create_low_level_gradient(layer->gradient);
        
        // Optimization: Use fast rect if radius is 0 and gradient is linear
        if (radius <= 0 && lowLevelGrad->type == GRADIENT_TYPE_LINEAR) {
            printf("draw gradient rect command %d %d %d %d", absX, absY, w, h);
            GraphicsCommand cmd_grad = {
                .cmd = CMD_DRAW_GRADIENT_RECT,
                .x = absX, .y = absY, .w = w, .h = h,
                .gradientData = lowLevelGrad,
                .fill = true,
                .clipX = (int)myEffectiveClip.origin->x,
                .clipY = (int)myEffectiveClip.origin->y,
                .clipW = (int)myEffectiveClip.size->width,
                .clipH = (int)myEffectiveClip.size->height
            };
            xQueueSend(g_graphics_queue, &cmd_grad, portMAX_DELAY);
        } else {
            printf("draw gradient rounded rect command %d %d %d %d", absX, absY, w, h);
            GraphicsCommand cmd_grad = {
                .cmd = CMD_DRAW_GRADIENT_ROUNDED_RECT,
                .x = absX, .y = absY, .w = w, .h = h,
                .radius = radius,
                .gradientData = lowLevelGrad,
                .fill = true,
                .clipX = (int)myEffectiveClip.origin->x,
                .clipY = (int)myEffectiveClip.origin->y,
                .clipW = (int)myEffectiveClip.size->width,
                .clipH = (int)myEffectiveClip.size->height
            };
            xQueueSend(g_graphics_queue, &cmd_grad, portMAX_DELAY);
        }
    } else {
        ColorRGBA bgCol = convert_cc_color(baseView->backgroundColor);
        if (bgCol.a > 0) {
            GraphicsCommand cmd_bg = {
                .cmd = CMD_DRAW_ROUNDED_RECT,
                .x = absX, .y = absY, .w = w, .h = h,
                .radius = radius,
                .color = bgCol,
                .fill = true,
                .clipX = (int)myEffectiveClip.origin->x,
                .clipY = (int)myEffectiveClip.origin->y,
                .clipW = (int)myEffectiveClip.size->width,
                .clipH = (int)myEffectiveClip.size->height
            };
            xQueueSend(g_graphics_queue, &cmd_bg, portMAX_DELAY);
        }
    }
    
    //ESP_LOGI(TAG, "drawViewHierarchy4");
    
    
    // ============================================================
    // USE 'object' and 'type' FOR CONTENT
    // ============================================================
    
    // --- STEP D: HANDLE CONTENT ---
    
    // 1. Handle Shape Layer (Attached to baseView)
    // FIX: Changed 'view->shapeLayer' to 'baseView->shapeLayer'
    if (baseView->shapeLayer != NULL) {
        drawShapeLayer(baseView->shapeLayer, absX, absY);
    }
    
    //ESP_LOGI(TAG, "drawViewHierarchy5");
    
    // 2. Handle Label Content
    // FIX: Changed 'view->type' to 'type'
    if (type == CCType_Label) {
        CCLabel* label = (CCLabel*)object;
        ColorRGBA textCol = convert_cc_color(label->textColor);
        
        TextFormat fmt = { 0 };
        if (label->textAlignment == CCTextAlignmentCenter) fmt.alignment = TEXT_ALIGN_CENTER;
        else if (label->textAlignment == CCTextAlignmentRight) fmt.alignment = TEXT_ALIGN_RIGHT;
        else fmt.alignment = TEXT_ALIGN_LEFT;
        
        if (label->textVerticalAlignment == CCTextVerticalAlignmentCenter) fmt.valignment = TEXT_VALIGN_CENTER;
        else if (label->textVerticalAlignment == CCTextVerticalAlignmentTop) fmt.valignment = TEXT_VALIGN_TOP;
        else fmt.valignment = TEXT_VALIGN_BOTTOM;
        
        //ESP_LOGI(TAG, "drawViewHierarchy5 CMD_DRAW_TEXT_BOX valignment %d", fmt.valignment);
        
        if (label->lineBreakMode == CCLineBreakWordWrapping) fmt.wrapMode = TEXT_WRAP_MODE_WHOLE_WORD;
        else fmt.wrapMode = TEXT_WRAP_MODE_TRUNCATE;
        
        fmt.lineSpacing = (int)label->lineSpacing;
        fmt.glyphSpacing = (int)label->glyphSpacing;
        
        GraphicsCommand cmd_text = {
            .cmd = CMD_DRAW_TEXT_BOX_CACHED,
            .x = absX, .y = absY, .w = w, .h = h,
            .color = textCol,
            .fontSize = (int)label->fontSize,
            .textFormat = fmt,
            .clipX = (int)myEffectiveClip.origin->x,
            .clipY = (int)myEffectiveClip.origin->y,
            .clipW = (int)myEffectiveClip.size->width,
            .clipH = (int)myEffectiveClip.size->height
        };
        if (label->text && label->text->string) {
            // strdup allocates memory on heap and copies the string
            cmd_text.text = strdup(label->text->string);
        } else {
            cmd_text.text = NULL;
        }
        xQueueSend(g_graphics_queue, &cmd_text, portMAX_DELAY);
        //ESP_LOGI(TAG, "drawViewHierarchy5 CMD_DRAW_TEXT_BOX %d", myEffectiveClip.size->width);
    }
    
    
    
    // 3. Handle Image Content
    // FIX: Changed 'view->type' to 'type'
    // Inside drawViewHierarchy...

    // Inside drawViewHierarchy...

    else if (type == CCType_ImageView) {
        CCImageView* imgView = (CCImageView*)object;
        
        // SAFETY CHAIN:
        // 1. Check if Image exists
        // 2. Check if FilePath object exists (This was the NULL culprit)
        // 3. Check if the actual character buffer exists
        if (imgView->image != NULL &&
            imgView->image->filePath != NULL &&
            imgView->image->filePath->string != NULL) {
            
            GraphicsCommand cmd_img = {
                .cmd = CMD_DRAW_IMAGE_FILE,
                .x = absX, .y = absY, .w = w, .h = h,
                .clipX = (int)myEffectiveClip.origin->x,
                .clipY = (int)myEffectiveClip.origin->y,
                .clipW = (int)myEffectiveClip.size->width,
                .clipH = (int)myEffectiveClip.size->height
            };
            
            // Now it is 100% safe to access the string
            strncpy(cmd_img.imagePath, imgView->image->filePath->string, 63);
            
            xQueueSend(g_graphics_queue, &cmd_img, portMAX_DELAY);
        }
        
        if (imgView->image->imageData) {
            GraphicsCommand cmd = {
                .cmd = CMD_DRAW_PIXEL_BUFFER, // Use the new buffer command
                .x = absX,
                .y = absY,
                .w = imgView->image->size->width,  // Use image size
                .h = imgView->image->size->height,
                .pixelBuffer = imgView->image->imageData // Pass the raw pointer
            };
            xQueueSend(g_graphics_queue, &cmd, 0);
        }
    }
    
    //ESP_LOGI(TAG, "drawViewHierarchy6");
    
    
    // ============================================================
    // RECURSE SUBVIEWS
    // ============================================================
    
    if (notHierarchy) {
        CCRect* myExactFrame = ccRect(absX, absY, w, h);
        CCRect childClip = intersectRects(currentClip, *myExactFrame);
        freeCCRect(myExactFrame);
        
        // Only recurse if the child clip is visible
        if (childClip.size->width > 0 && childClip.size->height > 0) {
            if (baseView->subviews) {
                for (int i = 0; i < baseView->subviews->count; i++) {
                    drawViewHierarchy(baseView->subviews->array[i], absX, absY, childClip, notHierarchy);
                }
            }
        }
    }
    else {
        if (baseView->subviews) {
            for (int i = 0; i < baseView->subviews->count; i++) {
                void* subview = arrayObjectAtIndex(baseView->subviews, i);
                drawViewHierarchy(subview, absX, absY, myEffectiveClip, notHierarchy);
            }
        }
    }
    
}

// Factory function to create one grid item
CCIconView* create_icon_view(CCRect* frame, const char* imgPath, const char* title) {
    CCIconView* item = (CCIconView*)cc_safe_alloc(1, sizeof(CCIconView));
    
    // 1. Container View (80x100)
    item->container = viewWithFrame(frame);
    item->container->backgroundColor = color(0, 0, 0, 0.0); // Transparent default
    layerSetCornerRadius(item->container->layer, 10.0);     // Rounded corners
    // Optional: Add a subtle border or just rely on the click effect
    
    // 2. Icon Image (Top, e.g., 60x60)
    // Center X = (80 - 60) / 2 = 10. Top Y = 10.
    CCRect* imgFrame = ccRect(5, 5, frame->size->width-10, frame->size->width-10);
    item->icon = imageViewWithFrame(imgFrame);
    item->icon->image = imageWithFile(ccs(imgPath));
    viewAddSubview(item->container, item->icon); // Add generic wrapper
    
    // 3. Text Label (Bottom)
    // Y = 75 to leave space under image. Height = 20.
    CCRect* lblFrame = ccRect(0, imgFrame->origin->y+imgFrame->size->height+3, frame->size->width, frame->size->height - imgFrame->origin->y);
    item->label = labelWithFrame(lblFrame);
    labelSetText(item->label, ccs(title));
    item->label->fontSize = 14;
    item->label->textAlignment = CCTextAlignmentCenter;
    item->label->textVerticalAlignment = CCTextVerticalAlignmentTop;
    item->label->textColor = color(1, 1, 1, 1); // White text
    
    // CRITICAL: Add the label object, not the view wrapper
    viewAddSubview(item->container, item->label);
    
    return item;
}

CCArray* g_grid_items_registry = NULL;
// Tracks the specific grid item currently holding the highlight state
CCView* g_last_touched_icon = NULL;

CCArray* create_grid_data_source(void) {
    CCArray* gridData = array();
    
    // Define raw data for the 12 items
    const char* labels[12] = {
        "Files", "Settings", "Text",
        "Message", "Paint", "Clock",
        "Photos", "Music", "Calculator",
        "Search", "Maps", "Net Tools"
    };
    
    // We'll use a few generic icon paths for demonstration
    const char* icons[12] = {
        "/spiflash/files.png", "/spiflash/settings.png", "/spiflash/text.png",
        "/spiflash/messages.png",    "/spiflash/paint.png",  "/spiflash/clock.png",
        "/spiflash/photos.png",    "/spiflash/music.png",  "/spiflash/calculator.png",
        "/spiflash/search.png",     "/spiflash/maps.png",  "/spiflash/net tools.png"
    };
    
    for (int i = 0; i < 12; i++) {
        CCDictionary* itemDict = dictionary();
        dictionarySetObjectForKey(itemDict, ccs(labels[i]), ccs("label"));
        dictionarySetObjectForKey(itemDict, ccs(icons[i]), ccs("image"));
        arrayAddObject(gridData, itemDict);
    }
    
    return gridData;
}

void drawHomeMenu(void) {
    int cols = 3;
    int rows = 4;
    int itemW = 80;
    int itemH = 100;
    int gapX = 10; // Spacing
    int gapY = 10;
    int startX = 30;
    int startY = 40;
    
    if (g_grid_items_registry == NULL) {
        g_grid_items_registry = array();
    }
    
    CCArray* dataItems = create_grid_data_source();
    
    for (int i = 0; i < dataItems->count; i++) {
        // Calculate Grid Position (Row/Col)
        int col = i % cols;
        int row = i / cols;
        
        int x = startX + (col * (itemW + gapX));
        int y = startY + (row * (itemH + gapY));
        
        // Retrieve Data Model
        CCDictionary* itemData = (CCDictionary*)arrayObjectAtIndex(dataItems, i);
        CCString* labelStr = (CCString*)dictionaryObjectForKey(itemData, ccs("label"));
        CCString* imgStr = (CCString*)dictionaryObjectForKey(itemData, ccs("image"));
        
        // Create View using Data
        CCIconView* iconItem = create_icon_view(
                                                ccRect(x, y, itemW, itemH), // Pass by value as expected by your fix
                                                imgStr->string,
                                                labelStr->string
                                                );
        
        // Add to Main Window
        viewAddSubview(mainWindowView, iconItem->container);
        
        arrayAddObject(g_grid_items_registry, iconItem->container);
        
        // Cleanup helper struct
        free(iconItem);
    }
    
}

void drawSampleViews(void) {
    // --- View 1: The "Card" (White, Rounded, Shadow) ---
    CCView* cardView = viewWithFrame(ccRect(40, 100, 240, 150));
    cardView->backgroundColor = color(1.0, 1.0, 1.0, 1.0); // White
    
    // Configure Layer Properties
    layerSetCornerRadius(cardView->layer, 20.0);
    
    // Add Shadow
    cardView->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
    cardView->layer->shadowOpacity = 0.5; // 50% transparent shadow
    cardView->layer->shadowOffset = ccPoint(10, 10); // Offset down-right
    
    viewAddSubview(mainWindowView, cardView);
    
    // Create a View to hold the shape
    CCView* triangleView = viewWithFrame(ccRect(100, 100, 100, 100));
    // Make the view transparent so only the shape shows
    triangleView->backgroundColor = color(0,0,0,0);
    
    // Create the Shape Layer
    CCShapeLayer* triangle = shapeLayer();
    
    // Add Points (Relative to the view's 0,0)
    pointPathAddPoint(triangle->pointPath, ccPoint(50, 0));   // Top Middle
    pointPathAddPoint(triangle->pointPath, ccPoint(100, 100)); // Bottom Right
    pointPathAddPoint(triangle->pointPath, ccPoint(0, 100));   // Bottom Left
    
    // Add a Gradient
    CCArray* colors = array();
    arrayAddObject(colors, color(1, 0, 0, 1)); // Red
    arrayAddObject(colors, color(0, 0, 1, 1)); // Blue
    CCArray* locs = array();
    arrayAddObject(locs, numberWithDouble(0.0));
    arrayAddObject(locs, numberWithDouble(1.0));
    
    triangle->gradient = gradientWithColors(colors, locs, M_PI_2); // Vertical gradient
    triangleView->shapeLayer = triangle;
    viewAddSubview(mainWindowView, triangleView);
    
    // Assign to view (You might need to modify CCView struct to hold a ShapeLayer specifically,
    // or just assign it to a generic 'void* content' field if you have one.
    // For now, let's assume you cast it into the 'layer' field if you modify CCLayer to hold shapes).
    
    // Or, simpler: Just call drawShapeLayer directly inside your render loop for testing.
    
    
    
    // Example Image
    CCImageView* myIcon = imageViewWithFrame(ccRect(50, 150, 64, 64));
    // Note: Ensure you have a valid VFS path string in your CCImage
    myIcon->image = imageWithFile(ccs("/spiflash/test.png"));
    viewAddSubview(mainWindowView, myIcon);
    
    
    
    
    // --- View 2: The "Button" (Gradient, Border) ---
    CCView* buttonView = viewWithFrame(ccRect(60, 300, 200, 60));
    
    // Create Gradient: Chrome/Metal style
    CCArray* colors1 = array();
    arrayAddObject(colors1, color(0.8, 0.8, 0.9, 1.0)); // Light Blue-ish Gray
    arrayAddObject(colors1, color(0.2, 0.2, 0.3, 1.0)); // Dark Blue-ish Gray
    
    CCArray* locs1 = array();
    arrayAddObject(locs1, numberWithDouble(0.0));
    arrayAddObject(locs1, numberWithDouble(1.0));
    
    // Assign Gradient to Layer
    buttonView->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2); // Vertical
    
    // Add Border
    buttonView->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
    buttonView->layer->shadowOpacity = 0.5; // 50% transparent shadow
    buttonView->layer->shadowOffset = ccPoint(10, 10); // Offset down-right
    //buttonView->layer->borderWidth = 4.0;
    //buttonView->layer->borderColor = color(1.0, 1.0, 1.0, 1.0); // White border
    layerSetCornerRadius(buttonView->layer, 30.0); // Fully rounded caps
    
    viewAddSubview(mainWindowView, buttonView);
    
    // Example Label
    CCLabel* myLabel = labelWithFrame(ccRect(15, 0, buttonView->frame->size->width-30, buttonView->frame->size->height));
    myLabel->text = ccs("This is a long string that will now auto-wrap inside the box!");
    myLabel->fontSize = 12.0;
    myLabel->textAlignment = CCTextAlignmentCenter; // It will center!
    myLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter; // It will center!
    myLabel->lineBreakMode = CCLineBreakWordWrapping; // It will wrap!
    myLabel->textColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(buttonView, myLabel);
    
    // 1. Create Text View
    // Position: x=20, y=80, w=280, h=300 (Visible Window)
    myDemoTextView = textViewWithFrame(ccRect(20, 80, 280, 300));
    /*
     FTC_Manager     g_ftc_manager = NULL;
     FTC_ImageCache  g_ftc_image_cache = NULL;
     FTC_CMapCache   g_ftc_cmap_cache = NULL;
     */
    myDemoTextView->ftManager    = g_ftc_manager;
    myDemoTextView->ftImageCache = g_ftc_image_cache;
    myDemoTextView->ftCMapCache  = g_ftc_cmap_cache;
    
    // Style the container
    myDemoTextView->scrollView->view->backgroundColor = color(0.95, 0.95, 0.95, 1.0);
    myDemoTextView->scrollView->view->layer->masksToBounds = true;
    layerSetCornerRadius(myDemoTextView->scrollView->view->layer, 10);
    
    
    
    // 2. Set Long Text
    // We assume ccs() creates a CCString
    CCString* longStr = ccs(
                            "Here is a very long string to demonstrate the new CCTextView.\n\n"
                            "This view automatically wraps text using the renderTextBox logic we built earlier.\n\n"
                            "More importantly, it sits inside a CCScrollView. When you drag your finger, "
                            "the touch loop calculates the delta and updates the contentOffset.\n\n"
                            "The 'drawViewHierarchy' function then clips this content to the parent frame, "
                            "creating a seamless scrolling effect.\n\n"
                            "The 'drawViewHierarchy' function then clips this content to the parent frame, "
                            "creating a seamless scrolling effect.\n\n"
                            "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\n"
                            "End of text."
                            );
    
    // This helper calculates height and resizes the content view automatically
    myDemoTextView->label->fontName = ccs("/spiflash/proximaNovaRegular.ttf");
    textViewSetText(myDemoTextView, longStr);
    scrollViewSetContentSize(myDemoTextView->scrollView, ccSize(280, 1000));
    
    // Style the text
    myDemoTextView->label->fontSize = 12;
    myDemoTextView->label->textColor = color(0.1, 0.1, 0.1, 1.0);
    myDemoTextView->label->lineSpacing = 5;
    
    
    // 3. Add to Main Window
    // Important: Add the scrollView->view, NOT the textView itself
    viewAddSubview(mainWindowView, myDemoTextView->scrollView->view);
    
    CCImageView* myIcon2 = imageViewWithFrame(ccRect(0, 0, 320, 480));
    // Note: Ensure you have a valid VFS path string in your CCImage
    myIcon2->image = imageWithFile(ccs("/spiflash/test2.jpg"));
    //viewAddSubview(mainWindowView, myIcon2);
}

void setup_ui_demo(void) {
    ESP_LOGI(TAG, "setup_ui_demo");
    int64_t start_time = esp_timer_get_time();
    
    if (mainWindowView == NULL) {
        // Initialize the root view filling the screen
        mainWindowView = viewWithFrame(ccRect(0, 0, 320, 480));
        mainWindowView->backgroundColor = color(0.2, 0.2, 0.2, 1.0); // Dark gray background
    }
    
    // A. Create the Color Array
    CCArray* gradColors = array();
    // Deep Blue (0, 50, 150) -> Normalized to 0.0 - 1.0
    arrayAddObject(gradColors, color(0.0f, 50.0f/255.0f, 150.0f/255.0f, 1.0f));
    // Vibrant Aqua (0, 200, 255) -> Normalized
    arrayAddObject(gradColors, color(0.0f, 200.0f/255.0f, 1.0f, 1.0f));
    
    // B. Create the Locations Array
    CCArray* gradLocs = array();
    arrayAddObject(gradLocs, numberWithDouble(0.0));
    arrayAddObject(gradLocs, numberWithDouble(1.0));
    
    // C. Create the Gradient Object (Vertical / 90 degrees)
    // M_PI_2 is 90 degrees (Top to Bottom)
    CCGradient* aquaGradient = gradientWithColors(gradColors, gradLocs, M_PI_2);
    
    // D. Assign it to the layer
    mainWindowView->layer->gradient = aquaGradient;
    
    
    //drawSampleViews();
    
    drawHomeMenu();
    
    checkAvailableMemory();
    
    // 2. Capture End Time
    int64_t end_time = esp_timer_get_time();
    
    // 3. Calculate Duration
    int64_t time_diff = end_time - start_time;
    
    // 4. Log result (Use %lld for int64_t)
    // We log both Microseconds (us) and Milliseconds (ms)
    ESP_LOGI("PROFILE", "Code execution took: %lld us (%lld ms)", time_diff, time_diff / 1000);
    
    // --- Trigger Render ---
    // Assuming you have a function to start the render pass:
    // update_full_ui();
    //update_full_ui();
    update_full_ui1();
}

// --- Global Cursor State ---
#define CURSOR_W 2   // Width of the cursor (e.g., 10 pixels wide)
#define CURSOR_H 18   // Height of the cursor (matching 24pt font)
#define CURSOR_BPP 3  // Bytes per pixel (RGB888)
// Buffer to store the background pixels (Allocated in Internal SRAM)
uint8_t* g_cursor_backup_buffer = NULL;
static bool addedCursor = false;
// Global cursor state (from the background restoration section)
static int g_cursor_x;
static int g_cursor_y;
// Global state to track cursor visibility
static bool g_cursor_visible = false;
TaskHandle_t g_cursor_blink_handle = NULL;

// Pin Definitions SPI LCD

#define PIN_NUM_MOSI        11
#define PIN_NUM_CLK         12
#define PIN_NUM_CS          10
#define PIN_NUM_DC           9
#define PIN_NUM_RST          8
#define PIN_NUM_BK_LIGHT     -1 // Use a real GPIO if you have a backlight control pin

// --- NEW, CORRECTED PIN CONFIGURATION ---
#define I2C_HOST           I2C_NUM_0
#define PIN_NUM_TOUCH_RST  4   // Your RST pin
#define PIN_NUM_TOUCH_INT  5   // Your INT pin
#define PIN_NUM_I2C_SDA    6   // Your SDA pin
#define PIN_NUM_I2C_SCL    7   // Your SCL pin

/*
 * DISPLAY CONFIGURATION
 * Uncomment one of the following lines to select your display.
 */
#define USE_ILI9488
// #define USE_ST7789


// --- Configuration for ILI9488 ---
#if defined(USE_ILI9488)
#define LCD_PIXEL_CLOCK_HZ (30 * 1000 * 1000)
#define LCD_H_RES          320
#define LCD_V_RES          480
#define LCD_COLOR_SPACE    ESP_LCD_COLOR_SPACE_RGB // ILI9488 is typically RGB
#define LCD_DRIVER_FN()    esp_lcd_new_panel_ili9488(io_handle, &panel_config, &panel_handle)

//RGB Display Pinout

#define LCD_COLOR_BYTES_PER_PIXEL 2 // Using 16-bit RGB565 for simpler pinout
#define LCD_INIT_BUFFER_SIZE 2048 // 2KB is usually more than enough for init commands
#define FRAME_BUFFER_SIZE (LCD_H_RES * LCD_V_RES * 2)

// --- GPIO PIN DEFINITIONS ---

// 1. Pins Freed for I2S/SDIO (DO NOT USE THESE BELOW)
// I2S: GPIO 40, 41, 42 | SDIO: GPIO 1, 5, 6, 7, 8, 9
// We will define them later, but they are reserved here.

// --- 1. I2S Audio Pin Definitions (FINAL MOVE) ---
#define I2S_PIN_NUM_BCLK      1
#define I2S_PIN_NUM_LRCK      2
#define I2S_PIN_NUM_DOUT      46      // Moved to 46 to free up 3 and 4

// --- 2. SD Card (SDIO 4-bit) Pin Definitions ---
#define SDIO_PIN_NUM_CLK      5
#define SDIO_PIN_NUM_CMD      6
#define SDIO_PIN_NUM_D0       7
#define SDIO_PIN_NUM_D1       8
#define SDIO_PIN_NUM_D2       9
#define SDIO_PIN_NUM_D3       10

// --- 3. SPI Pins for ILI9488 Initialization ---
#define LCD_PIN_NUM_SPI_CS    11
#define LCD_PIN_NUM_SPI_SCLK  12
#define LCD_PIN_NUM_SPI_MOSI  13
#define LCD_PIN_NUM_SPI_DC    10      // Safest choice for Data/Command

// --- 4. General Control Pin Definitions ---
#define LCD_PIN_NUM_RST       14
#define LCD_PIN_NUM_BK_LIGHT  15

// --- 5. RGB Interface Pins for DISPLAY (SYNC LINES) ---
#define LCD_PIN_NUM_PCLK      1
#define LCD_PIN_NUM_HSYNC     0
#define LCD_PIN_NUM_VSYNC     45
#define LCD_PIN_NUM_DE        47

// --- 6. RGB Data Bus (D0-D15) - FINAL, VERIFIED PINS ---
// Low Byte (D0-D7)
#define LCD_PIN_NUM_D0        20
#define LCD_PIN_NUM_D1        21
#define LCD_PIN_NUM_D2        16      // Remapped from 23
#define LCD_PIN_NUM_D3        17      // Remapped from 25
#define LCD_PIN_NUM_D4        18      // Remapped from 26
#define LCD_PIN_NUM_D5        19      // Remapped from 27
#define LCD_PIN_NUM_D6        4       // Remapped from 33 (New location!)
#define LCD_PIN_NUM_D7        3       // Remapped from 34 (New location!)

// High Byte (D8-D15)
#define LCD_PIN_NUM_D8        7
#define LCD_PIN_NUM_D9        8
#define LCD_PIN_NUM_D10       9
#define LCD_PIN_NUM_D11       38
#define LCD_PIN_NUM_D12       39
#define LCD_PIN_NUM_D13       40
#define LCD_PIN_NUM_D14       41
#define LCD_PIN_NUM_D15       42
// --- 6. RGB Data Bus (D16, D17) - NEW 18-BIT LINES ---
#define LCD_PIN_NUM_D16       5       // LCD Pin: D16
#define LCD_PIN_NUM_D17       6       // LCD Pin: D17

// NOTE: Please carefully check your specific ESP32-S3 module's pinout.
// GPIOs 22, 24, and 28-32 are often not available. The example above is
// adjusted to skip these unavailable pins, creating the 16-pin bus.

// IMPORTANT: LCD timing parameters for ILI9488
// These are typical values, but you MUST verify them with your datasheet.
#define LCD_PCLK_HZ          (6 * 1000 * 1000) // 6MHz for 480x320
// Increase the timing margins (Porches)
#define LCD_HSYNC_PULSE_WIDTH       5
#define LCD_HSYNC_BACK_PORCH        20 // Increased from 8
#define LCD_HSYNC_FRONT_PORCH       20 // Increased from 10

#define LCD_VSYNC_PULSE_WIDTH       5
#define LCD_VSYNC_BACK_PORCH        20 // Increased from 8
#define LCD_VSYNC_FRONT_PORCH       20 // Increased from 10


// Place this outside of app_main
void lcd_cmd(spi_device_handle_t spi, const uint8_t cmd)
{
    // Ensure DC is LOW for command (0)
    gpio_set_level(LCD_PIN_NUM_SPI_DC, 0);
    
    // Create the transaction configuration
    spi_transaction_t t = {
        .length = 8, // 8 bits long
        .tx_buffer = &cmd,
    };
    // Send the command
    spi_device_transmit(spi, &t);
}

void lcd_data(spi_device_handle_t spi, const uint8_t *data, int len)
{
    if (len == 0) return;
    // Ensure DC is HIGH for data (1)
    gpio_set_level(LCD_PIN_NUM_SPI_DC, 1);
    
    // Create the transaction configuration
    spi_transaction_t t = {
        .length = len * 8, // length is in bits
        .tx_buffer = data,
    };
    // Send the data
    spi_device_transmit(spi, &t);
}

// Function to send a single command followed by a single parameter
void ili9488_set_colmod(spi_device_handle_t spi)
{
    const uint8_t cmd_colmod = 0x3A; // Command to set pixel format
    const uint8_t param_rgb565 = 0x55; // Parameter for 16-bit (RGB565)

    // 1. Send the command (DC=0)
    lcd_cmd(spi, cmd_colmod);
    
    // 2. Send the parameter (DC=1)
    lcd_data(spi, &param_rgb565, 1);
}

// --- ILI9488 Command Definitions (MUST be visible to the array) ---

#define ILI9488_SWRESET      0x01
#define ILI9488_SLPOUT       0x11
#define ILI9488_DISPON       0x29
#define ILI9488_PIXFMT       0x3A

#define ILI9488_FRMCTR1      0xB1
#define ILI9488_INVCTR       0xB4
#define ILI9488_DFUNCTR      0xB6

#define ILI9488_PWCTR1       0xC0
#define ILI9488_PWCTR2       0xC1
#define ILI9488_VMCTR1       0xC5

#define ILI9488_GMCTRP1      0xE0
#define ILI9488_GMCTRN1      0xE1
#define ILI9488_IMGFUNCT     0xE9

#define ILI9488_ADJCTR3      0xF7

// Commands used in your array (from the STM32 source):
#define ILI9488_GMCTRP1      0xE0
#define ILI9488_GMCTRN1      0xE1
#define ILI9488_PWCTR1       0xC0
#define ILI9488_PWCTR2       0xC1
#define ILI9488_VMCTR1       0xC5
#define ILI9488_PIXFMT       0x3A
#define ILI9488_FRMCTR1      0xB1
#define ILI9488_INVCTR       0xB4
#define ILI9488_DFUNCTR      0xB6
#define ILI9488_IMGFUNCT     0xE9
#define ILI9488_ADJCTR3      0xF7
#define ILI9488_SLPOUT       0x11
#define ILI9488_DISPON       0x29
#define ILI9488_SWRESET      0x01

// Adapt the command list from the STM32 driver (ili9488_Init function)
static const uint8_t ili9488_rgb_init_sequence[][20] = {
    // Command, Parameter1, Parameter2, ...
    { ILI9488_SWRESET, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x01: SWRESET (No params, but needs delay)
    { ILI9488_GMCTRP1, 0x00, 0x01, 0x02, 0x04, 0x14, 0x09, 0x3F, 0x57, 0x4D, 0x05, 0x0B, 0x09, 0x1A, 0x1D, 0x0F }, // 0xE0: Positive Gamma Control (15 params)
    { ILI9488_GMCTRN1, 0x00, 0x1D, 0x20, 0x02, 0x0E, 0x03, 0x35, 0x12, 0x47, 0x02, 0x0D, 0x0C, 0x38, 0x39, 0x0F }, // 0xE1: Negative Gamma Control (15 params)
    { ILI9488_PWCTR1, 0x17, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xC0: Power Control 1 (2 params)
    { ILI9488_PWCTR2, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xC1: Power Control 2 (1 param)
    { ILI9488_VMCTR1, 0x00, 0x12, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xC5: VCOM Control (3 params)
    
    // *** CRITICAL CHANGE ***
    // 0x3A: PIXFMT. Must be 0x55 for 16-bit RGB565.
    { ILI9488_PIXFMT, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x3A: Interface Pixel Format (1 param)

    // 0xB0: Interface Mode Control. We need to set the parallel mode bits, which is NOT 0x80/0x00.
    // The ILI9488 requires different commands for DPI (Parallel RGB) vs. MCU (8080/DPI) mode.
    // We will use 0x02 (which enables the DPI) or 0x03 (for 16-bit DPI). This depends on hardware. Let's try 0x00 (the standard reset state)
    // and rely on the RGB panel to set the mode, but it might need 0x02.
    // Since the driver failed to set this, let's skip it and focus on the power-up sequence.

    { ILI9488_FRMCTR1, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xB1: Frame rate (1 param)
    { ILI9488_INVCTR, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xB4: Display Inversion Control (1 param)
    { ILI9488_DFUNCTR, 0x02, 0x02, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xB6: Display Function Control (3 params)
    { ILI9488_IMGFUNCT, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xE9: Set Image Function (1 param)
    { ILI9488_ADJCTR3, 0xA9, 0x51, 0x2C, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xF7: Adjust Control (4 params)
    
    // **Final Wake-up**
    { ILI9488_SLPOUT, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x11: Exit Sleep (Needs 120ms delay)
    { ILI9488_DISPON, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x29: Display ON (Needs 5ms delay)
};



void initRgbDisplay(void) {
    // --- Part 1: Initialize Backlight ---
        ESP_LOGI(TAG, "Initializing LCD backlight on GPIO %d...", LCD_PIN_NUM_BK_LIGHT);
        // ... (Backlight setup code remains the same)
        gpio_config_t bk_gpio_conf = {
            .pin_bit_mask = 1ULL << LCD_PIN_NUM_BK_LIGHT,
            .mode = GPIO_MODE_OUTPUT,
        };
        ESP_ERROR_CHECK(gpio_config(&bk_gpio_conf));
        gpio_set_level(LCD_PIN_NUM_BK_LIGHT, 1);

        // --- Part 2: Initialize ILI9488 via temporary SPI bus ---
        ESP_LOGI(TAG, "Installing temporary SPI bus for ILI9488 init...");
        spi_bus_config_t buscfg = {
            .mosi_io_num = LCD_PIN_NUM_SPI_MOSI,
            .sclk_io_num = LCD_PIN_NUM_SPI_SCLK,
            .miso_io_num = -1,
            .quadwp_io_num = -1,
            .quadhd_io_num = -1,
            .max_transfer_sz = LCD_H_RES * 10, // Small transfer size for commands
        };
        ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));

        esp_lcd_panel_io_handle_t io_handle = NULL;
        esp_lcd_panel_io_spi_config_t io_config = {
            .cs_gpio_num = LCD_PIN_NUM_SPI_CS,
            .dc_gpio_num = LCD_PIN_NUM_SPI_DC,
            .spi_mode = 0,
            .pclk_hz = 10 * 1000 * 1000,
            .trans_queue_depth = 10,
            .lcd_cmd_bits = 8,
            .lcd_param_bits = 8,
        };
        ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &io_handle));

    ESP_LOGI(TAG, "Installing ILI9488 driver over SPI...");
        esp_lcd_panel_handle_t init_panel_handle = NULL;
        esp_lcd_panel_dev_config_t panel_config = {
            .reset_gpio_num = LCD_PIN_NUM_RST,
            .bits_per_pixel = 16,
        };

        ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(io_handle,
                                                  &panel_config,
                                                  LCD_INIT_BUFFER_SIZE, // <--- NEW ARGUMENT
                                                  &init_panel_handle));

        ESP_LOGI(TAG, "Resetting and Initializing ILI9488...");
        ESP_ERROR_CHECK(esp_lcd_panel_reset(init_panel_handle));
        ESP_ERROR_CHECK(esp_lcd_panel_init(init_panel_handle));

        // Optional: Set the ILI9488 to accept RGB565 (16-bit) data
        // Command 0x3A is COLMOD (Pixel Format). 0x55 is 16-bit (RGB565).
        esp_lcd_panel_io_tx_param(io_handle, 0x3A, (uint8_t[]){0x55}, 1);

        // Disable SPI interface
        ESP_LOGI(TAG, "Initialization complete. Deleting SPI handles...");
        ESP_ERROR_CHECK(esp_lcd_panel_del(init_panel_handle));
        ESP_ERROR_CHECK(esp_lcd_panel_io_del(io_handle));
        ESP_ERROR_CHECK(spi_bus_free(SPI2_HOST));


        // --- Part 3: Install the permanent RGB interface ---
        ESP_LOGI(TAG, "Installing RGB panel driver for ILI9488...");
        esp_lcd_rgb_panel_config_t rgb_config = {
            .clk_src = LCD_CLK_SRC_DEFAULT,
            .disp_gpio_num = -1,
            .psram_trans_align = 64,
            .data_width = 16, // Use 16-bit bus
            .bits_per_pixel = 16,
            .de_gpio_num = LCD_PIN_NUM_DE,
            .pclk_gpio_num = LCD_PIN_NUM_PCLK,
            .vsync_gpio_num = LCD_PIN_NUM_VSYNC,
            .hsync_gpio_num = LCD_PIN_NUM_HSYNC,
            .data_gpio_nums = {
                LCD_PIN_NUM_D0, LCD_PIN_NUM_D1, LCD_PIN_NUM_D2, LCD_PIN_NUM_D3,
                LCD_PIN_NUM_D4, LCD_PIN_NUM_D5, LCD_PIN_NUM_D6, LCD_PIN_NUM_D7,
                LCD_PIN_NUM_D8, LCD_PIN_NUM_D9, LCD_PIN_NUM_D10, LCD_PIN_NUM_D11,
                LCD_PIN_NUM_D12, LCD_PIN_NUM_D13, LCD_PIN_NUM_D14, LCD_PIN_NUM_D15,
            },
            .timings = {
                .pclk_hz = LCD_PCLK_HZ,
                .h_res = LCD_H_RES,
                .v_res = LCD_V_RES,
                .flags.pclk_active_neg = true,  // TRY THIS AGAIN! Inverting the clock edge
                .flags.vsync_idle_low = false,  // Standard for VSYNC (try swapping if needed)
                .flags.hsync_idle_low = false,  // Standard for HSYNC (try swapping if needed)
                .hsync_pulse_width = LCD_HSYNC_PULSE_WIDTH,
                .hsync_back_porch = LCD_HSYNC_BACK_PORCH,
                .hsync_front_porch = LCD_HSYNC_FRONT_PORCH,
                .vsync_pulse_width = LCD_VSYNC_PULSE_WIDTH,
                .vsync_back_porch = LCD_VSYNC_BACK_PORCH,
                .vsync_front_porch = LCD_VSYNC_FRONT_PORCH,
            },
            .flags.fb_in_psram = true,
        };
        esp_lcd_panel_handle_t rgb_panel_handle = NULL;
        ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&rgb_config, &rgb_panel_handle));
        ESP_ERROR_CHECK(esp_lcd_panel_init(rgb_panel_handle));
        // Set the ILI9488 to accept RGB565 (16-bit) data
        // Command 0x3A is COLMOD (Pixel Format). 0x55 is 16-bit (RGB565).
        //esp_lcd_panel_io_tx_param(io_handle, 0x3A, (uint8_t[]){0x55}, 1);
        //ESP_ERROR_CHECK(esp_lcd_panel_io_tx_param(io_handle, 0x29, NULL, 0));
    
    // Inside app_main, before calling esp_lcd_new_panel_ili9488:


    /// --- Part 2: Initialize ILI9488 via temporary SPI bus (Corrected Order) ---
    
    // 1. Define the physical bus GPIOs
    spi_bus_config_t buscfg1 = {
        .mosi_io_num = LCD_PIN_NUM_SPI_MOSI,
        .sclk_io_num = LCD_PIN_NUM_SPI_SCLK,
        .miso_io_num = -1, // Not needed for write-only
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 1024,
    };
    
    // Line 1: Initialize the physical bus and pins (This was missing or misplaced!)
    ESP_LOGI(TAG, "Initializing SPI bus host...");
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg1, SPI_DMA_CH_AUTO));

    // 2. Define the device configuration
    spi_device_handle_t raw_spi_handle = NULL;
    spi_device_interface_config_t devcfg={
        .clock_speed_hz = 10 * 1000 * 1000,
        .mode = 0,
        .spics_io_num = LCD_PIN_NUM_SPI_CS,
        .queue_size = 7,
        .pre_cb = NULL,
    };
    
    // Line 2: Add the device to the now-initialized bus
    ESP_LOGI(TAG, "Adding ILI9488 device to bus...");
    ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &raw_spi_handle)); // Your crash line (now fixed)
    
    // 3. Manually send the required commands
    // Ensure the DC pin (GPIO 8) is configured as output before this line!
    gpio_set_direction(LCD_PIN_NUM_SPI_DC, GPIO_MODE_OUTPUT);
    
    ESP_LOGI(TAG, "Manually setting 16-bit color format...");
    ili9488_set_colmod(raw_spi_handle); // This command should now execute!

    // ... continue with esp_lcd_new_panel_ili9488 call, etc.

        // Manually run the hardware reset (already working, but cleaner here)
        // Assuming GPIO 14 is your reset pin
        gpio_set_level(LCD_PIN_NUM_RST, 0);
        vTaskDelay(pdMS_TO_TICKS(10));
        gpio_set_level(LCD_PIN_NUM_RST, 1);
        vTaskDelay(pdMS_TO_TICKS(120));

        ESP_LOGI(TAG, "Starting full ILI9488 init sequence...");

        for (int i = 0; i < sizeof(ili9488_rgb_init_sequence) / sizeof(ili9488_rgb_init_sequence[0]); i++) {
            uint8_t cmd = ili9488_rgb_init_sequence[i][0];
            const uint8_t *data = &ili9488_rgb_init_sequence[i][1];
            // Calculate length based on known commands or hardcoding
            int len = 0;
            if (cmd == ILI9488_GMCTRP1 || cmd == ILI9488_GMCTRN1) len = 15;
            else if (cmd == ILI9488_PWCTR1) len = 2;
            else if (cmd == ILI9488_VMCTR1) len = 3;
            else if (cmd == ILI9488_ADJCTR3) len = 4;
            else if (cmd == ILI9488_DFUNCTR) len = 2; // Adjusted from 3 to 2 based on pattern
            else if (cmd == ILI9488_PWCTR2 || cmd == ILI9488_PIXFMT || cmd == ILI9488_FRMCTR1 || cmd == ILI9488_INVCTR || cmd == ILI9488_IMGFUNCT) len = 1;

            if (cmd == ILI9488_SWRESET) {
                lcd_cmd(raw_spi_handle, cmd);
                vTaskDelay(pdMS_TO_TICKS(5));
            } else if (cmd == ILI9488_SLPOUT) {
                lcd_cmd(raw_spi_handle, cmd);
                vTaskDelay(pdMS_TO_TICKS(120));
            } else if (cmd == ILI9488_DISPON) {
                lcd_cmd(raw_spi_handle, cmd);
                vTaskDelay(pdMS_TO_TICKS(5));
            } else {
                lcd_cmd(raw_spi_handle, cmd);
                if (len > 0) {
                    lcd_data(raw_spi_handle, data, len);
                }
            }
        }

        ESP_LOGI(TAG, "ILI9488 is now configured.");

    // ... (Proceed to delete SPI handles and initialize esp_lcd_new_panel_rgb) ...
    
        // --- Part 4: Draw to the "live" framebuffer ---
        uint16_t *framebuffer1 = NULL;
        ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(rgb_panel_handle, 1, (void **)&framebuffer1));

        ESP_LOGI(TAG, "Drawing solid green to the %dx%d framebuffer...", LCD_H_RES, LCD_V_RES);
        uint16_t green_color = 0x07E0; // RGB565: 00000 111111 00000 (Pure Green)
        for (int i = 0; i < LCD_H_RES * LCD_V_RES; i++) {
            framebuffer1[i] = green_color;
        }
        ESP_LOGI(TAG, "Display should be green and refreshed by hardware.");
}

static bool cpuGraphics = true;

// Add this near the top of your file
static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map);

static const int DISPLAY_HORIZONTAL_PIXELS = 320;
static const int DISPLAY_VERTICAL_PIXELS = 480;
static const int DISPLAY_COMMAND_BITS = 8;
static const int DISPLAY_PARAMETER_BITS = 8;
static const unsigned int DISPLAY_REFRESH_HZ = 40000000;
static const int DISPLAY_SPI_QUEUE_LEN = 10;
static const int SPI_MAX_TRANSFER_SIZE = 32768;

static const lcd_rgb_element_order_t TFT_COLOR_MODE = COLOR_RGB_ELEMENT_ORDER_RGB;
//if !cpuGraphics
//static const lcd_rgb_element_order_t TFT_COLOR_MODE = COLOR_RGB_ELEMENT_ORDER_BGR;

esp_err_t setup_cursor_buffers() {
    size_t size = CURSOR_W * CURSOR_H * CURSOR_BPP;
    // Allocate buffer in internal, DMA-capable memory (very fast)
    g_cursor_backup_buffer = (uint8_t*) heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
    if (!g_cursor_backup_buffer) {
        ESP_LOGE(TAG, "Failed to allocate %d bytes for cursor backup!", size);
        return ESP_ERR_NO_MEM;
    }
    ESP_LOGI(TAG, "setup_cursor_buffers");
    return ESP_OK;
}

void save_cursor_background(Framebuffer *fb, int x, int y) {
    if (!g_cursor_backup_buffer) {
        ESP_LOGI(TAG, "!g_cursor_backup_buffer");
        return;
    }
    
    // Set the new cursor position
    g_cursor_x = x;
    g_cursor_y = y;
    
    uint8_t* psram_ptr;
    uint8_t* backup_ptr = g_cursor_backup_buffer;
    
    ESP_LOGI(TAG, "save_cursor_background");
    
    // Loop through each row of the cursor rect and copy from the PSRAM framebuffer
    for (int i = 0; i < CURSOR_H; i++) {
        // Calculate the starting position of the row in the PSRAM framebuffer
        psram_ptr = &((uint8_t*)fb->pixelData)[((y + i) * fb->displayWidth + x) * CURSOR_BPP];
        // Copy the entire row (CURSOR_W * 3 bytes) to the internal backup buffer
        memcpy(backup_ptr, psram_ptr, CURSOR_W * CURSOR_BPP);
        backup_ptr += CURSOR_W * CURSOR_BPP;
    }
}

void restore_cursor_background(Framebuffer *fb) {
    if (!g_cursor_backup_buffer) return;
    
    // 1. Write the background back into the PSRAM framebuffer
    uint8_t* psram_ptr;
    uint8_t* backup_ptr = g_cursor_backup_buffer;
    
    for (int i = 0; i < CURSOR_H; i++) {
        psram_ptr = &((uint8_t*)fb->pixelData)[((g_cursor_y + i) * fb->displayWidth + g_cursor_x) * CURSOR_BPP];
        // Copy the row from the internal backup buffer back to the PSRAM framebuffer
        memcpy(psram_ptr, backup_ptr, CURSOR_W * CURSOR_BPP);
        backup_ptr += CURSOR_W * CURSOR_BPP;
    }
    
    ESP_LOGI(TAG, "restore_cursor_background");
    
    // 2. Send the restore command to the graphics queue
    GraphicsCommand cmd_update;
    cmd_update.cmd = CMD_UPDATE_AREA;
    cmd_update.x = g_cursor_x;
    cmd_update.y = g_cursor_y;
    cmd_update.w = CURSOR_W;
    cmd_update.h = CURSOR_H;
    
    // Use the queue send for the update
    if (xQueueSend(g_graphics_queue, &cmd_update, 0) != pdTRUE) {
        ESP_LOGE(TAG, "Cursor restore command failed.");
    }
}

void cursor_blink_task(void *pvParameter)
{
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    
    //ESP_LOGI(TAG, "cursor_blink_task");
    
    while (1) {
        GraphicsCommand cmd_blink;
        
        if (g_cursor_visible) {
            // If visible, send restore command (blink OFF)
            cmd_blink.cmd = CMD_CURSOR_RESTORE;
            g_cursor_visible = false;
        } else {
            // If hidden, send draw command (blink ON)
            cmd_blink.cmd = CMD_CURSOR_DRAW;
            g_cursor_visible = true;
        }
        
        // Send the command with zero wait time
        xQueueSend(g_graphics_queue, &cmd_blink, 0);
        
        // Wait for the blink interval (500ms)
        vTaskDelay(pdMS_TO_TICKS(500));
        
        
    }
}

// Note: To draw the cursor, you would then call drawRectangleCFramebuffer
// to draw a solid block over the area (x, y, CURSOR_W, CURSOR_H).

// Default to 25 lines of color data
static const size_t LV_BUFFER_SIZE = DISPLAY_HORIZONTAL_PIXELS * 25;
static const int LVGL_UPDATE_PERIOD_MS = 5;

/*static const ledc_mode_t BACKLIGHT_LEDC_MODE = LEDC_LOW_SPEED_MODE;
 static const ledc_channel_t BACKLIGHT_LEDC_CHANNEL = LEDC_CHANNEL_0;
 static const ledc_timer_t BACKLIGHT_LEDC_TIMER = LEDC_TIMER_1;
 static const ledc_timer_bit_t BACKLIGHT_LEDC_TIMER_RESOLUTION = LEDC_TIMER_10_BIT;
 static const uint32_t BACKLIGHT_LEDC_FRQUENCY = 5000;*/

static esp_lcd_panel_io_handle_t lcd_io_handle = NULL;
static esp_lcd_panel_handle_t lcd_handle = NULL;
static lv_draw_buf_t lv_disp_buf;
static lv_display_t *lv_display = NULL;
static lv_color_t *lv_buf_1 = NULL;
static lv_color_t *lv_buf_2 = NULL;
static lv_obj_t *meter = NULL;
static lv_style_t style_screen;

/**
 * @brief DMA callback for esp_lcd.
 * This function is called from an ISR when the SPI transfer is done.
 */
static bool notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io,
                                    esp_lcd_panel_io_event_data_t *edata,
                                    void *user_ctx)
{
    // user_ctx is the address of the global lv_display pointer (&lv_display)
    lv_display_t **disp_ptr = (lv_display_t **)user_ctx;
    lv_display_t *disp = *disp_ptr;
    
    // Check if LVGL has created the display object yet
    if (disp) {
        // Tell LVGL that the buffer flushing is complete
        lv_display_flush_ready(disp);
    }
    
    // Return false, indicating no high-priority task was woken.
    return false;
}


static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
    esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)lv_display_get_user_data(disp);
    
    int offsetx1 = area->x1;
    int offsetx2 = area->x2;
    int offsety1 = area->y1;
    int offsety2 = area->y2;
    // Change line 165 to this:
    esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map);
    // At the very end, tell LVGL you are done
    lv_display_flush_ready(disp);
}

static void IRAM_ATTR lvgl_tick_cb(void *param)
{
    lv_tick_inc(LVGL_UPDATE_PERIOD_MS);
}

/*static void display_brightness_init(void)
 {
 const ledc_channel_config_t LCD_backlight_channel =
 {
 .gpio_num = GPIO_NUM_NC,
 .speed_mode = BACKLIGHT_LEDC_MODE,
 .channel = BACKLIGHT_LEDC_CHANNEL,
 .intr_type = LEDC_INTR_DISABLE,
 .timer_sel = BACKLIGHT_LEDC_TIMER,
 .duty = 0,
 .hpoint = 0,
 .flags =
 {
 .output_invert = 0
 }
 };
 const ledc_timer_config_t LCD_backlight_timer =
 {
 .speed_mode = BACKLIGHT_LEDC_MODE,
 .duty_resolution = BACKLIGHT_LEDC_TIMER_RESOLUTION,
 .timer_num = BACKLIGHT_LEDC_TIMER,
 .freq_hz = BACKLIGHT_LEDC_FRQUENCY,
 .clk_cfg = LEDC_AUTO_CLK
 };
 ESP_LOGI(TAG, "Initializing LEDC for backlight pin: %d", GPIO_NUM_NC);
 
 ESP_ERROR_CHECK(ledc_timer_config(&LCD_backlight_timer));
 ESP_ERROR_CHECK(ledc_channel_config(&LCD_backlight_channel));
 }
 
 void display_brightness_set(int brightness_percentage)
 {
 if (brightness_percentage > 100)
 {
 brightness_percentage = 100;
 }
 else if (brightness_percentage < 0)
 {
 brightness_percentage = 0;
 }
 ESP_LOGI(TAG, "Setting backlight to %d%%", brightness_percentage);
 
 // LEDC resolution set to 10bits, thus: 100% = 1023
 uint32_t duty_cycle = (1023 * brightness_percentage) / 100;
 ESP_ERROR_CHECK(ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty_cycle));
 ESP_ERROR_CHECK(ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL));
 }*/

void initialize_spi()
{
    ESP_LOGI(TAG, "Initializing SPI bus (MOSI:%d, MISO:%d, CLK:%d)",
             PIN_NUM_MOSI, GPIO_NUM_NC, PIN_NUM_CLK);
    spi_bus_config_t bus =
    {
        .mosi_io_num = PIN_NUM_MOSI,
        .miso_io_num = GPIO_NUM_NC,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = GPIO_NUM_NC,
        .quadhd_io_num = GPIO_NUM_NC,
        .data4_io_num = GPIO_NUM_NC,
        .data5_io_num = GPIO_NUM_NC,
        .data6_io_num = GPIO_NUM_NC,
        .data7_io_num = GPIO_NUM_NC,
        .max_transfer_sz = SPI_MAX_TRANSFER_SIZE,
        .flags = SPICOMMON_BUSFLAG_SCLK |
        SPICOMMON_BUSFLAG_MOSI | SPICOMMON_BUSFLAG_MASTER,
            .intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM
    };
    
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_CH_AUTO));
}

void initialize_display()
{
    const esp_lcd_panel_io_spi_config_t io_config =
    {
        .cs_gpio_num = PIN_NUM_CS,
        .dc_gpio_num = PIN_NUM_DC,
        .spi_mode = 0,
        .pclk_hz = DISPLAY_REFRESH_HZ,
        .trans_queue_depth = DISPLAY_SPI_QUEUE_LEN,
        .on_color_trans_done = notify_lvgl_flush_ready,
        .user_ctx = &lv_display,
        .lcd_cmd_bits = DISPLAY_COMMAND_BITS,
        .lcd_param_bits = DISPLAY_PARAMETER_BITS,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,4,0)
        .cs_ena_pretrans = 0,
        .cs_ena_posttrans = 0,
#endif
        .flags =
        {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
            .dc_as_cmd_phase = 0,
            .dc_low_on_data = 0,
            .octal_mode = 0,
            .lsb_first = 0
#else
                .dc_low_on_data = 0,
                .octal_mode = 0,
                .sio_mode = 0,
                .lsb_first = 0,
                .cs_high_active = 0
#endif
        }
    };
    
    const esp_lcd_panel_dev_config_t lcd_config =
    {
        .reset_gpio_num = PIN_NUM_RST,
        .color_space = TFT_COLOR_MODE,
        .bits_per_pixel = 18,
        .flags =
        {
            .reset_active_high = 0
        },
            .vendor_config = NULL
    };
    
    ESP_ERROR_CHECK(
                    esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &lcd_io_handle));
    
    ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(lcd_io_handle, &lcd_config, LV_BUFFER_SIZE, &lcd_handle));
    
    ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_handle));
    ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_handle));
    ESP_ERROR_CHECK(esp_lcd_panel_invert_color(lcd_handle, true));
    ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(lcd_handle, false));
    ESP_ERROR_CHECK(esp_lcd_panel_mirror(lcd_handle, false, false));
    ESP_ERROR_CHECK(esp_lcd_panel_set_gap(lcd_handle, 0, 0));
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
    ESP_ERROR_CHECK(esp_lcd_panel_disp_off(lcd_handle, false));
#else
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(lcd_handle, true));
#endif
}

void initialize_lvgl()
{
    ESP_LOGI(TAG, "Initializing LVGL");
    lv_init();
    
    ESP_LOGI(TAG, "Allocating LVGL draw buffers");
    lv_buf_1 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
    assert(lv_buf_1);
#if USE_DOUBLE_BUFFERING
    lv_buf_2 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
    assert(lv_buf_2);
#endif
    
    ESP_LOGI(TAG, "Creating LVGL display");
    // --- THIS IS THE NEW V9 DRIVER SETUP ---
    
    // 1. Create a display
    lv_display = lv_display_create(DISPLAY_HORIZONTAL_PIXELS, DISPLAY_VERTICAL_PIXELS);
    
    lv_display_set_buffers(lv_display,
                           lv_buf_1,
                           lv_buf_2,
                           LV_BUFFER_SIZE,
                           LV_DISPLAY_RENDER_MODE_PARTIAL);
    
    // 4. Set the flush callback
    lv_display_set_flush_cb(lv_display, lvgl_flush_cb);
    
    // 5. Pass your 'lcd_handle' (panel handle) to the display's user data
    lv_display_set_user_data(lv_display, lcd_handle);
    
    // --- END OF NEW V9 SETUP ---
    
    
    ESP_LOGI(TAG, "Creating LVGL tick timer");
    const esp_timer_create_args_t lvgl_tick_timer_args =
    {
        .callback = &lvgl_tick_cb,
        .name = "lvgl_tick"
    };
    esp_timer_handle_t lvgl_tick_timer = NULL;
    ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, LVGL_UPDATE_PERIOD_MS * 1000));
}

static void lvgl_task_handler(void *pvParameters)
{
    ESP_LOGI(TAG, "Starting LVGL task handler");
    while (1)
    {
        // Run the LVGL timer handler
        // This function will block for the appropriate amount of time
        // based on the timers you have set up in LVGL.
        lv_timer_handler();
        
        // Delay for a short period to yield to other tasks.
        // 10ms is a common value.
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// --- Configuration for ST7789 ---
#elif defined(USE_ST7789)
#define LCD_PIXEL_CLOCK_HZ (40 * 1000 * 1000) // You can use 40MHz for ST7789 too
#define LCD_H_RES          320
#define LCD_V_RES          240
#define LCD_COLOR_SPACE    ESP_LCD_COLOR_SPACE_BGR // ST7789 is often BGR
#define LCD_DRIVER_FN()    esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle)

// --- Error if no display is selected ---
#else
#error "No display driver selected. Please define USE_ILI9488 or USE_ST7789."
#endif


#define LCD_COLOR_RED       0xF800 // RGB565 code for Red

// Global panel handle (needed for the drawing function)
esp_lcd_panel_handle_t panel_handle = NULL;

static bool usingSt7789 = false;

//Touch

static esp_lcd_touch_handle_t tp_handle = NULL; // Touch panel handle
static lv_indev_t *lvgl_touch_indev = NULL;     // LVGL input device

/**
 * @brief LVGL input device "read" callback.
 * This function is called by LVGL periodically to get the current touch state.
 */
/*static void lvgl_touch_cb(lv_indev_t *indev, lv_indev_data_t *data)
 {
 // Get the touch handle stored in the input device's user_data
 esp_lcd_touch_handle_t tp = (esp_lcd_touch_handle_t)lv_indev_get_user_data(indev);
 
 // --- This is your code, now inside the callback ---
 uint16_t touch_x[1];
 uint16_t touch_y[1];
 uint16_t touch_strength[1];
 uint8_t touch_cnt = 0;
 
 // Poll the touch controller
 bool touchpad_pressed = esp_lcd_touch_get_coordinates(tp, touch_x, touch_y, touch_strength, &touch_cnt, 1);
 
 if (touchpad_pressed && touch_cnt > 0) {
 // A touch is detected
 data->state = LV_INDEV_STATE_PRESSED;
 data->point.x = touch_x[0];
 data->point.y = touch_y[0];
 } else {
 // No touch is detected
 data->state = LV_INDEV_STATE_RELEASED;
 }
 }*/

// --- Use the ESP32 pins from your code snippet ---
#define I2C_SDA_PIN 6
#define I2C_SCL_PIN 7
#define RST_N_PIN   4
#define INT_N_PIN   5

#define I2C_HOST I2C_NUM_0 // I2C port 0

// Global device handle
static ft6336u_dev_t touch_dev;
static i2c_master_bus_handle_t i2c_bus_handle;

// FreeRTOS queue to handle interrupt events
static QueueHandle_t touch_intr_queue = NULL;

/**
 * @brief Initialize the I2C master bus
 */
esp_err_t i2c_master_init(void) {
    i2c_master_bus_config_t i2c_bus_config = {
        .i2c_port = I2C_HOST,
        .sda_io_num = I2C_SDA_PIN,
        .scl_io_num = I2C_SCL_PIN,
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .intr_priority = 0,
        .flags.enable_internal_pullup = true
    };
    
    esp_err_t ret = i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to create I2C master bus: %s", esp_err_to_name(ret));
    }
    return ret;
}



// --- LVGL BRIDGE ---
// These static variables will store the last known touch state
// They are updated by touch_task and read by lvgl_touch_read_cb
static int32_t g_last_x = 0;
static int32_t g_last_y = 0;
static lv_indev_state_t g_last_state = LV_INDEV_STATE_REL; // Start as Released
static SemaphoreHandle_t g_touch_mutex; // <-- Add this
// --- END LVGL BRIDGE ---


/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
 
 // --- LOCK ---
 xSemaphoreTake(g_touch_mutex, portMAX_DELAY);
 
 // Read the shared variables
 ESP_LOGI(TAG, "lvgl_touch_read_cb");
 data->point.x = g_last_x;
 data->point.y = g_last_y;
 data->state = g_last_state;
 
 // --- UNLOCK ---
 xSemaphoreGive(g_touch_mutex);
 }*/

/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
 
 // Lock the mutex to check the state
 xSemaphoreTake(g_touch_mutex, portMAX_DELAY);
 
 // --- This is the new logic ---
 
 // 1. Check if a "press" event is waiting for us
 if (g_last_state == LV_INDEV_STATE_PR) {
 
 // It is. Report the press to LVGL.
 data->point.x = g_last_x;
 data->point.y = g_last_y;
 data->state = LV_INDEV_STATE_PR;
 
 // 2. IMPORTANT: Consume the event
 // We immediately set the global state back to "released".
 // This ensures LVGL only sees "PRESSED" for a single frame.
 g_last_state = LV_INDEV_STATE_REL;
 
 } else {
 // No new press event. Just report "released".
 data->state = LV_INDEV_STATE_REL;
 }
 
 // --- Unlock the mutex ---
 xSemaphoreGive(g_touch_mutex);
 }*/


/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
 
 // --- LOCK THE MUTEX ---
 xSemaphoreTake(g_touch_mutex, portMAX_DELAY);
 
 // --- THIS IS THE FIX ---
 
 // 1. Check if a "press" event is waiting for us
 if (g_last_state == LV_INDEV_STATE_PR) {
 
 // It is. Report the press to LVGL.
 data->point.x = g_last_x;
 data->point.y = g_last_y;
 data->state = LV_INDEV_STATE_PR;
 
 // 2. IMPORTANT: Consume the event
 // We immediately set the global state back to "released".
 // This ensures LVGL only sees "PRESSED" for a single frame.
 g_last_state = LV_INDEV_STATE_REL;
 
 } else {
 // No new press event. Just report "released".
 data->point.x = g_last_x; // Pass last-known coords
 data->point.y = g_last_y;
 data->state = LV_INDEV_STATE_REL;
 }
 
 // --- UNLOCK THE MUTEX ---
 xSemaphoreGive(g_touch_mutex);
 }*/

void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
    
    // Lock the mutex
    xSemaphoreTake(g_touch_mutex, portMAX_DELAY);
    
    // --- This is the fix ---
    
    // 1. Check if a "press" event is waiting for us
    if (g_last_state == LV_INDEV_STATE_PR) {
        
        // It is. Report the press to LVGL.
        data->point.x = g_last_x;
        data->point.y = g_last_y;
        data->state = LV_INDEV_STATE_PR;
        
        // 2. IMPORTANT: Consume the event
        // We immediately set the global state back to "released".
        g_last_state = LV_INDEV_STATE_REL;
        
    } else {
        // No new press event. Just report "released".
        data->point.x = g_last_x;
        data->point.y = g_last_y;
        data->state = LV_INDEV_STATE_REL;
    }
    
    // Unlock the mutex
    xSemaphoreGive(g_touch_mutex);
}



/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
 
 ESP_LOGI(TAG, "lvgl_touch_read_cb");
 
 FT6336U_TouchPointType touch_data;
 
 // 1. Scan the touch controller
 esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
 if (ret != ESP_OK) {
 ESP_LOGE(TAG, "Failed to scan touch: %s", esp_err_to_name(ret));
 // Keep last state if read fails
 data->state = LV_INDEV_STATE_REL;
 return;
 }
 
 // 2. Update LVGL with the new data
 if (touch_data.touch_count == 0) {
 data->state = LV_INDEV_STATE_REL; // Released
 } else {
 // Use the first touch point
 data->point.x = touch_data.tp[0].x;
 data->point.y = touch_data.tp[0].y;
 data->state = LV_INDEV_STATE_PR; // Pressed
 
 // Optional: Log if you still want to see coordinates
 // ESP_LOGI(TAG, "Touch 0: X=%u, Y=%u", data->point.x, data->point.y);
 }
 }*/

/*void updateTouch(void *pvParameter) {
 while (1) {
 FT6336U_TouchPointType touch_data;
 esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
 
 // --- LOCK ---
 xSemaphoreTake(g_touch_mutex, portMAX_DELAY);
 
 if (ret != ESP_OK) {
 g_last_state = LV_INDEV_STATE_REL;
 } else {
 if (touch_data.touch_count == 0) {
 g_last_state = LV_INDEV_STATE_REL;
 } else {
 g_last_x = touch_data.tp[0].x;
 g_last_y = touch_data.tp[0].y;
 g_last_state = LV_INDEV_STATE_PR;
 }
 }
 
 // --- UNLOCK ---
 xSemaphoreGive(g_touch_mutex);
 
 vTaskDelay(pdMS_TO_TICKS(20));
 }
 
 
 uint32_t event;
 FT6336U_TouchPointType touch_data;
 
 while (1) {
 // Wait for an interrupt event from the queue
 if (xQueueReceive(touch_intr_queue, &event, portMAX_DELAY)) {
 g_last_state = LV_INDEV_STATE_REL;
 // Interrupt received, scan the touch controller
 esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
 if (ret != ESP_OK) {
 ESP_LOGE(TAG, "Failed to scan touch: %s", esp_err_to_name(ret));
 g_last_state = LV_INDEV_STATE_REL;
 continue;
 }
 
 if (touch_data.touch_count == 0) {
 ESP_LOGI(TAG, "Touch Released");
 
 g_last_state = LV_INDEV_STATE_REL;
 } else {
 ESP_LOGI(TAG, "Touch Pressed");
 for (int i = 0; i < touch_data.touch_count; i++) {
 ESP_LOGI(TAG, "Touch %d: (%s) X=%u, Y=%u",
 i,
 (touch_data.tp[i].status == touch) ? "NEW" : "STREAM",
 touch_data.tp[i].x,
 touch_data.tp[i].y);
 }
 g_last_x = touch_data.tp[0].x;
 g_last_y = touch_data.tp[0].y;
 g_last_state = LV_INDEV_STATE_PR;
 
 }
 }
 }
 }*/

// --- New Global State for Scrolling UI ---
int g_scroll_offset_y = 0;      // Current scroll position (0 = top is visible)
int g_scroll_total_height = 0;  // Total height of the content (to be calculated)
int g_scroll_viewport_h = 300;  // Visible height of the clipping area (Example)
int g_drag_start_y = 0;         // Tracks initial touch Y when a drag begins
float g_drag_velocity = 0.0f;   // Optional: For kinetic scrolling

#include <math.h>
#include <stdio.h> // Include standard libraries for math and memory

// Assuming Vector3 is defined as:
// typedef struct { float x, y, z; } Vector3;

/**
 * @brief Generates the vertices for a 5-pointed star.
 *
 * @param centerX Center X coordinate.
 * @param centerY Center Y coordinate.
 * @param outerRadius Distance to the outer points.
 * @param innerRadius Distance to the inner points (the notches).
 * @param numVertices Pointer to store the number of vertices generated (will be 10).
 * @return A dynamically allocated array of Vector3 vertices. Caller must free.
 */
Vector3* create_star_vertices(float centerX, float centerY, float outerRadius, float innerRadius, int *numVertices) {
    const int num_points = 5;
    const int total_vertices = num_points * 2;
    *numVertices = total_vertices;
    
    // Allocate 10 vertices in memory
    Vector3* vertices = (Vector3*)cc_safe_alloc(1, total_vertices * sizeof(Vector3));
    if (!vertices) return NULL;
    
    float angle_step = M_PI / num_points; // 36 degrees per point (180/5)
    
    for (int i = 0; i < total_vertices; i++) {
        float radius = (i % 2 == 0) ? outerRadius : innerRadius;
        float angle = i * angle_step - M_PI_2; // Start star pointed upwards (offset by -90 deg)
        
        vertices[i].x = centerX + radius * cosf(angle);
        vertices[i].y = centerY + radius * sinf(angle);
        vertices[i].z = 0.0f; // 2D operation, Z is zero
    }
    
    return vertices;
}



int randNumberTo(int max) {
    return rand() % max;
}

void testGraphics(void);

static bool hasDrawnKeyboard = false;
static int keyboardCursorPosition = 0;

const char* letterForCoordinate(void) {
    return "Q";
}

CCScrollView* findParentScrollView(CCView* view) {
    CCView* current = view;
    while (current != NULL) {
        // Check if this view is the 'contentView' of a ScrollView structure
        // This is tricky because C structs don't have back-pointers to their wrapper.
        // WORKAROUND: We iterate our known scroll views or check a tag.
        
        // BETTER APPROACH:
        // In updateTouch, we look at the touchedView's superview chain.
        // If current->superview's layer has masksToBounds=true and the logic fits...
        
        // FOR THIS EXAMPLE: We will rely on a global pointer for the active scroll view
        // or explicit checking in the touch loop if you track your objects.
        current = (CCView*)current->superview;
    }
    return NULL;
}

// Global to track which icon is currently being pressed
CCView* g_pressed_icon_view = NULL;
ColorRGBA g_color_highlight = {.r=0, .g=0, .b=0, .a=25}; // 0.1 alpha (approx 25/255)
ColorRGBA g_color_transparent = {.r=0, .g=0, .b=0, .a=0};
// Registry to track only the interactive grid items

CCView* find_grid_item_at(int x, int y) {
    // Safety check
    ESP_LOGI(TAG, "find_grid_item_at");
    if (!g_grid_items_registry) return NULL;
    ESP_LOGI(TAG, "find_grid_item_at %d", g_grid_items_registry->count);
    
    // Iterate ONLY the registered grid items
    for (int i = 0; i < g_grid_items_registry->count; i++) {
        CCView* targetView = (CCView*)arrayObjectAtIndex(g_grid_items_registry, i);
        targetView->tag = i;
        
        // Get the frame (relative to parent/screen)
        // Since these are direct children of mainWindowView (0,0),
        // their frame origin is their absolute screen position.
        float vx = targetView->frame->origin->x;
        float vy = targetView->frame->origin->y;
        float vw = targetView->frame->size->width;
        float vh = targetView->frame->size->height;
        
        // Fast Geometry Check
        if (x >= vx && x < (vx + vw) &&
            y >= vy && y < (vy + vh)) {
            return targetView;
        }
    }
    return NULL;
}


// Helper to format bytes into readable KB/MB strings
CCString* formatFileSize(int bytes) {
    char buffer[32];
    if (bytes < 1024) {
        snprintf(buffer, sizeof(buffer), "%d B", bytes);
    } else if (bytes < 1048576) {
        snprintf(buffer, sizeof(buffer), "%.1f KB", bytes / 1024.0);
    } else {
        snprintf(buffer, sizeof(buffer), "%.1f MB", bytes / 1048576.0);
    }
    return stringWithCString(buffer);
}

void setup_files_ui(void) {
    printf("setup_files_ui");
    currentView = CurrentViewFiles;
    
    // 1. Create the new container
    // We create a FRESH view. We do not modify the old one.
    float screenWidth = 320.0f;
    CCView* filesView = viewWithFrame(ccRect(0, 0, screenWidth, 480));
    
    // Style it differently so you know you switched
    filesView->backgroundColor = color(1.0, 1.0, 1.0, 1.0); // Light gray/White
    
    // Create Gradient: Chrome/Metal style
    CCArray* colors1 = array();
    arrayAddObject(colors1, color(0.8, 0.8, 0.9, 1.0)); // Light Blue-ish Gray
    arrayAddObject(colors1, color(0.2, 0.2, 0.3, 1.0)); // Dark Blue-ish Gray
    
    CCArray* locs1 = array();
    arrayAddObject(locs1, numberWithDouble(0.0));
    arrayAddObject(locs1, numberWithDouble(1.0));
    
    // 2. Add a Header
    CCView* header = viewWithFrame(ccRect(0, 30, screenWidth, 60));
    header->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
    layerSetCornerRadius(header->layer, 0.0);
    header->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
    header->layer->shadowOpacity = 0.5; // 50% transparent shadow
    header->layer->shadowOffset = ccPoint(0, 10); // Offset down-right
    header->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2); // Vertical
    
    
    // 3. Add a "Back" or "Close" Button
    // IMPORTANT: You need a way to get back!
    CCView* backBtn = viewWithFrame(ccRect(10, 10, 80, 30));
    backBtn->backgroundColor = color(0.8, 0.0, 0.0, 1.0); // Red
    backBtn->tag = 999; // Special tag for "Back"
    //viewAddSubview(header, backBtn);
    
    CCLabel* myLabel = labelWithFrame(ccRect(0, 0, header->frame->size->width, 30));
    myLabel->text = ccs("Files");
    myLabel->fontSize = 24.0;
    myLabel->textAlignment = CCTextAlignmentCenter; // It will center!
    myLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter; // It will center!
    myLabel->lineBreakMode = CCLineBreakWordWrapping; // It will wrap!
    myLabel->textColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, myLabel);
    
    CCView* navLineH = viewWithFrame(ccRect(0, 30, screenWidth, 1.0));
    navLineH->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, navLineH);
    
    CCView* navLineV = viewWithFrame(ccRect(0, screenWidth/3.0, 1.0, 70));
    navLineV->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, navLineV);
    
    CCView* navLineV2 = viewWithFrame(ccRect(0, (screenWidth/3.0)*2.0, 1.0, 70));
    navLineV2->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, navLineV2);
    
    CCView* cardView = viewWithFrame(ccRect(40, 160, 240, 150));
    cardView->backgroundColor = color(0.9, 1.0, 1.0, 1.0); // White
    layerSetCornerRadius(cardView->layer, 20.0);
    cardView->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
    cardView->layer->shadowOpacity = 0.5; // 50% transparent shadow
    cardView->layer->shadowOffset = ccPoint(10, 10); // Offset down-right
    //viewAddSubview(filesView, cardView);
    
    // 1. Get the data
    // (Ensure you have mounted the filesystem partition, e.g., "/spiflash" or "/sdcard")
    files = get_directory_files_as_array("/spiflash");
    //printf("%s", cStringOfString(objectDescription(files)));
    
    // 2. Setup Date Formatter
    CCDateFormatter* fmt = dateFormatter();
    fmt->dateStyle = CCDateFormatterStyleShort; // e.g., 12/31/25
    fmt->timeStyle = CCDateFormatterStyleShort; // e.g., 14:30
    
    // 3. Layout Constants
    int startY = 70;   // Start below screen top
    int rowH = 40;     // Height of each row
    int screenW = 320; // Your screen width
    
    // Table Columns:
    // Name: 0 -> 140
    // Date: 140 -> 250
    // Size: 250 -> 320
    
    
    
    // 5. Loop through files (Max 10)
    int count = files->count;
    if (count > 9) count = 9; // Limit to 10 labels as requested
    
    for (int i = 0; i < count; i++) {
        CCDictionary* fileData = (CCDictionary*)arrayObjectAtIndex(files, i);
        
        // --- Extract Data ---
        CCString* nameStr = (CCString*)dictionaryObjectForKey(fileData, ccs("Name"));
        CCDate* dateObj = (CCDate*)dictionaryObjectForKey(fileData, ccs("DateModified"));
        CCNumber* sizeNum = (CCNumber*)dictionaryObjectForKey(fileData, ccs("Size"));
        
        // --- Format Strings ---
        CCString* dateStr = stringFromDate(fmt, dateObj);
        CCString* sizeStr = formatFileSize(numberIntValue(sizeNum));
        
        // --- Create Row Container ---
        // We create a container view for the row so we can add a background or border later
        int currentY = startY + 20.0 + (i * rowH);
        CCView* rowView = viewWithFrame(ccRect(0, currentY, screenW, rowH));
        
        // Alternating background colors for readability (Zebra striping)
        if (i % 2 == 0) {
            rowView->backgroundColor = color(0.2, 0.2, 0.2, 1.0); // Dark Gray
        } else {
            rowView->backgroundColor = color(0.15, 0.15, 0.15, 1.0); // Slightly Darker
        }
        
        // --- 1. Name Label (Left Aligned) ---
        CCLabel* lblName = labelWithFrame(ccRect(5, 0, 155, rowH));
        lblName->text = copyCCString(nameStr);;
        lblName->fontSize = 14;
        lblName->textColor = color(1, 1, 1, 1);
        lblName->textAlignment = CCTextAlignmentLeft;
        lblName->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(rowView, lblName);
        
        // --- 2. Date Label (Center/Left) ---
        CCLabel* lblDate = labelWithFrame(ccRect(165, 0, 85, rowH));
        lblDate->text = dateStr ? dateStr : ccs("--/--/--");
        lblDate->fontSize = 10;
        lblDate->textColor = color(0.8, 0.8, 0.8, 1); // Light gray
        lblDate->textAlignment = CCTextAlignmentLeft;
        lblDate->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(rowView, lblDate);
        
        // --- 3. Size Label (Right Aligned) ---
        CCLabel* lblSize = labelWithFrame(ccRect(255, 0, 60, rowH));
        lblSize->text = sizeStr;
        lblSize->fontSize = 10;
        lblSize->textColor = color(0.5, 0.8, 1.0, 1); // Cyan-ish for numbers
        lblSize->textAlignment = CCTextAlignmentRight; // Numbers look better right-aligned
        lblSize->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(rowView, lblSize);
        
        // Add row to main window
        viewAddSubview(filesView, rowView);
    }
    
    viewAddSubview(filesView, header);
    
    CCImageView* headerArrowLeft = imageViewWithFrame(ccRect(5, 5, 10, 20));
    headerArrowLeft->image = imageWithFile(ccs("/spiflash/leftArrow20.png"));
    viewAddSubview(header, headerArrowLeft);
    
    CCView* highlightHeaderColumnView = viewWithFrame(ccRect(5, 30, 140, 30));
    highlightHeaderColumnView->backgroundColor = color(0.0, 0.0, 0.0, 0.1);
    viewAddSubview(header, highlightHeaderColumnView);
    
    // 4. Draw Header Row (Optional, for context)
    CCLabel* hName = labelWithFrame(ccRect(5, 30, 140, 30));
    hName->text = ccs("Filename");
    hName->textColor = color(1.0, 1.0, 1.0, 0.9); // Gray text
    hName->fontSize = 13;
    hName->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(header, hName);
    
    CCImageView* headerArrow = imageViewWithFrame(ccRect(115, 40, 20, 10));
    headerArrow->image = imageWithFile(ccs("/spiflash/downArrow20.png"));
    viewAddSubview(header, headerArrow);
    
    
    CCLabel* hDate = labelWithFrame(ccRect(165, 30, 100, 30));
    hDate->text = ccs("Date");
    hDate->textColor = color(1.0, 1.0, 1.0, 0.9);
    hDate->fontSize = 13;
    hDate->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(header, hDate);
    
    CCLabel* hSize = labelWithFrame(ccRect(255, 30, 100, 30));
    hSize->text = ccs("Size");
    hSize->textColor = color(1.0, 1.0, 1.0, 0.9);
    hSize->fontSize = 13;
    hSize->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(header, hSize);
    
    
    
    // 4. Update the global pointer
    // NOTE: We don't free the old one here, we assume it was pushed to stack
    mainWindowView = filesView;
}

/*void setup_settings_ui(void) {
    printf("setup_files_ui");
    currentView = CurrentViewSettings;
    
    float screenWidth = 320.0f;
    CCView* filesView = viewWithFrame(ccRect(0, 0, screenWidth, 480));
    
    // Style it differently so you know you switched
    filesView->backgroundColor = color(1.0, 1.0, 1.0, 1.0); // Light gray/White
    
    // Create Gradient: Chrome/Metal style
    CCArray* colors1 = array();
    arrayAddObject(colors1, color(0.8, 0.8, 0.9, 1.0)); // Light Blue-ish Gray
    arrayAddObject(colors1, color(0.2, 0.2, 0.3, 1.0)); // Dark Blue-ish Gray
    
    CCArray* locs1 = array();
    arrayAddObject(locs1, numberWithDouble(0.0));
    arrayAddObject(locs1, numberWithDouble(1.0));
    
    // 2. Add a Header
    CCView* header = viewWithFrame(ccRect(0, 30, screenWidth, 60));
    header->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
    layerSetCornerRadius(header->layer, 0.0);
    header->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
    header->layer->shadowOpacity = 0.5; // 50% transparent shadow
    header->layer->shadowOffset = ccPoint(0, 10); // Offset down-right
    header->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2); // Vertical
    
    CCLabel* myLabel = labelWithFrame(ccRect(0, 0, header->frame->size->width, 30));
    myLabel->text = ccs("Settings");
    myLabel->fontSize = 24.0;
    myLabel->textAlignment = CCTextAlignmentCenter; // It will center!
    myLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter; // It will center!
    myLabel->lineBreakMode = CCLineBreakWordWrapping; // It will wrap!
    myLabel->textColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, myLabel);
    
    CCView* navLineH = viewWithFrame(ccRect(0, 30, screenWidth, 1.0));
    navLineH->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, navLineH);
    
    CCView* navLineV = viewWithFrame(ccRect(0, screenWidth/3.0, 1.0, 70));
    navLineV->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
    //viewAddSubview(header, navLineV);

    
    // 3. Layout Constants
    int startY = 70;   // Start below screen top
    int rowH = 50;     // Height of each row
    int screenW = 320; // Your screen width
    
    settings = arrayWithObjects(ccs("About"), ccs("Locale"), ccs("Calendar/Clock"), ccs("WiFi"), ccs("Bluetooth"), ccs("Style"), ccs("Disk Storage"), ccs("CPU/RAM"), NULL);
    
    // 5. Loop through files (Max 10)
    int count = settings->count;
    if (count > 9) count = 9; // Limit to 10 labels as requested
    
    for (int i = 0; i < count; i++) {
        CCString* nameStr = (CCString*)arrayObjectAtIndex(settings, i);
        
        // --- Create Row Container ---
        // We create a container view for the row so we can add a background or border later
        int currentY = startY + 20.0 + (i * rowH);
        CCView* rowView = viewWithFrame(ccRect(0, currentY, screenW, rowH));
        rowView->tag = i;
        
        // Alternating background colors for readability (Zebra striping)
        if (i % 2 == 0) {
            rowView->backgroundColor = color(0.2, 0.2, 0.2, 1.0); // Dark Gray
        } else {
            rowView->backgroundColor = color(0.15, 0.15, 0.15, 1.0); // Slightly Darker
        }
        
        // --- 1. Name Label (Left Aligned) ---
        CCLabel* lblName = labelWithFrame(ccRect(5, 0, screenW-35, rowH));
        lblName->text = copyCCString(nameStr);
        lblName->fontSize = 24;
        lblName->textColor = color(1, 1, 1, 1);
        lblName->textAlignment = CCTextAlignmentLeft;
        lblName->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(rowView, lblName);
        
        CCImageView* headerArrowRight = imageViewWithFrame(ccRect(screenW-20, 10, 15, 30));
        headerArrowRight->image = imageWithFile(ccs("/spiflash/rightArrow30.png"));
        viewAddSubview(rowView, headerArrowRight);
        
        // Add row to main window
        viewAddSubview(filesView, rowView);
    }
    
    viewAddSubview(filesView, header);
    
    CCImageView* headerArrowLeft = imageViewWithFrame(ccRect(5, 5, 10, 20));
    headerArrowLeft->image = imageWithFile(ccs("/spiflash/leftArrow20.png"));
    viewAddSubview(header, headerArrowLeft);
    
    CCView* highlightHeaderColumnView = viewWithFrame(ccRect(5, 30, 140, 30));
    highlightHeaderColumnView->backgroundColor = color(0.0, 0.0, 0.0, 0.1);
    viewAddSubview(header, highlightHeaderColumnView);
    
    // 4. Draw Header Row (Optional, for context)
    CCLabel* hName = labelWithFrame(ccRect(5, 30, 140, 30));
    hName->text = ccs("Filename");
    hName->textColor = color(1.0, 1.0, 1.0, 0.9); // Gray text
    hName->fontSize = 13;
    hName->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(header, hName);
    

    
    
    
    // 4. Update the global pointer
    // NOTE: We don't free the old one here, we assume it was pushed to stack
    mainWindowView = filesView;
}*/

// --- Settings Globals ---
//static CCArray* settings = NULL;
static int settingsPageIndex = 0;
static const int SETTINGS_PER_PAGE = 6; // How many rows per page
static CCView* uiSettingsContainer = NULL; // Holds the rows + buttons
static CCView* buttonsContainer = NULL;

// Tags to identify buttons
#define TAG_SETTINGS_PREV 2001
#define TAG_SETTINGS_NEXT 2002
#define TAG_SETTINGS_ROW_BASE 3000

void update_settings_list(void) {
    // 1. Clear previous container if it exists
    // We remove it from the main window and free its memory
    if (uiSettingsContainer) {
        if (mainWindowView && mainWindowView->subviews) {
            viewRemoveFromSuperview(uiSettingsContainer);
        }
        freeViewHierarchy(uiSettingsContainer); // CAREFUL: Ensure this unlinks from parent
    }
    
    if (buttonsContainer) {
        if (mainWindowView && mainWindowView->subviews) {
            viewRemoveFromSuperview(buttonsContainer);
        }
        freeViewHierarchy(buttonsContainer);
    }
    
    // 2. Create New Container (Below the header)
    // Header is 90px tall (30 padding + 60 header), so start at Y=90
    uiSettingsContainer = viewWithFrame(ccRect(0, 90, 320, 390));
    uiSettingsContainer->backgroundColor = color(1,1,1,1); // Transparent
    viewAddSubview(mainWindowView, uiSettingsContainer);
    
    // Gradient
    CCArray* colors1 = array();
    arrayAddObject(colors1, color(0.8, 0.8, 0.9, 1.0));
    arrayAddObject(colors1, color(0.2, 0.2, 0.3, 1.0));
    CCArray* locs1 = array();
    arrayAddObject(locs1, numberWithDouble(0.0));
    arrayAddObject(locs1, numberWithDouble(1.0));

    buttonsContainer = viewWithFrame(ccRect(0, 390, 320, 90));
    buttonsContainer->backgroundColor = color(1,1,1,1); // Transparent
    buttonsContainer->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2);
    viewAddSubview(mainWindowView, buttonsContainer);
    
    // 3. Calculate Pagination Bounds
    int totalItems = settings->count;
    int startIndex = settingsPageIndex * SETTINGS_PER_PAGE;
    int endIndex = startIndex + SETTINGS_PER_PAGE;
    if (endIndex > totalItems) endIndex = totalItems;

    int rowH = 50;
    int screenW = 320;
    int currentY = 0; // Relative to container

    // 4. Draw Rows
    for (int i = startIndex; i < endIndex; i++) {
        CCString* nameStr = (CCString*)arrayObjectAtIndex(settings, i);
        
        // Row View
        CCView* rowView = viewWithFrame(ccRect(0, currentY, screenW, rowH));
        rowView->tag = TAG_SETTINGS_ROW_BASE + i; // Tag helps us know which item was clicked
        
        // Zebra Striping
        if (i % 2 == 0) {
            rowView->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
        } else {
            rowView->backgroundColor = color(0.15, 0.15, 0.15, 1.0);
        }

        // Label
        CCLabel* lblName = labelWithFrame(ccRect(15, 0, screenW-50, rowH));
        lblName->text = copyCCString(nameStr);
        lblName->fontSize = 24;
        lblName->textColor = color(1, 1, 1, 1);
        lblName->textAlignment = CCTextAlignmentLeft;
        lblName->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(rowView, lblName);
        
        // Arrow
        CCImageView* arrow = imageViewWithFrame(ccRect(screenW-25, 10, 15, 30));
        arrow->image = imageWithFile(ccs("/spiflash/rightArrow30.png"));
        viewAddSubview(rowView, arrow);

        viewAddSubview(uiSettingsContainer, rowView);
        currentY += rowH;
        
        if (i == endIndex - 1) {
            printf("add shadowOpacity endIndex");
            rowView->layer->shadowOpacity = 0.5;
            rowView->layer->shadowRadius = 10.0;
            rowView->layer->shadowOffset = ccPoint(0, 10);
        }
    }
    
    // Gradient
    /*CCArray* colors2 = array();
    arrayAddObject(colors2, color(0.0, 0.0, 0.0, 1.0));
    arrayAddObject(colors2, color(0.0, 0.0, 0.0, 0.0));
    CCArray* locs2 = array();
    arrayAddObject(locs2, numberWithDouble(0.0));
    arrayAddObject(locs2, numberWithDouble(1.0));

    CCView *buttonsContainerShadow = viewWithFrame(ccRect(0, 390, 320, 15));
    buttonsContainerShadow->backgroundColor = color(1,1,1,1); // Transparent
    buttonsContainerShadow->layer->gradient = gradientWithColors(colors2, locs2, M_PI_2);
    viewAddSubview(mainWindowView, buttonsContainerShadow);*/

    // 5. Draw Pagination Buttons (Bottom of list)
    int btnY = currentY + 10;
    int btnW = 100;
    int btnH = 40;
    
    btnY = 20;

    // PREV BUTTON (Only show if not on first page)
    if (settingsPageIndex > 0) {
        CCView* btnPrev = viewWithFrame(ccRect(20, btnY, btnW, btnH));
        btnPrev->backgroundColor = color(0.3, 0.3, 0.4, 1.0);
        btnPrev->layer->cornerRadius = 20;
        btnPrev->tag = TAG_SETTINGS_PREV;
        btnPrev->layer->shadowOpacity = 0.5;
        btnPrev->layer->shadowOffset = ccPoint(5, 5);
        btnPrev->layer->shadowRadius = 5;
        btnPrev->layer->borderWidth = 2.0;
        btnPrev->layer->borderColor = color(1.0, 1.0, 1.0, 0.7);
        
        CCLabel* lblPrev = labelWithFrame(ccRect(0,0,btnW,btnH));
        lblPrev->text = ccs("<< Back");
        lblPrev->textColor = color(1,1,1,1);
        lblPrev->textAlignment = CCTextAlignmentCenter;
        lblPrev->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(btnPrev, lblPrev);
        
        viewAddSubview(buttonsContainer, btnPrev);
    }

    // NEXT BUTTON (Only show if more items exist)
    if (endIndex < totalItems) {
        CCView* btnNext = viewWithFrame(ccRect(screenW - 20 - btnW, btnY, btnW, btnH));
        btnNext->backgroundColor = color(0.3, 0.3, 0.4, 1.0);
        btnNext->layer->cornerRadius = 20;
        btnNext->tag = TAG_SETTINGS_NEXT;
        btnNext->layer->shadowOpacity = 0.5;
        btnNext->layer->shadowOffset = ccPoint(5, 5);
        btnNext->layer->shadowRadius = 5;
        btnNext->layer->borderWidth = 2.0;
        btnNext->layer->borderColor = color(1.0, 1.0, 1.0, 0.7);

        CCLabel* lblNext = labelWithFrame(ccRect(0,0,btnW,btnH));
        lblNext->text = ccs("Next >>");
        lblNext->textColor = color(1,1,1,1);
        lblNext->textAlignment = CCTextAlignmentCenter;
        lblNext->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(btnNext, lblNext);
        
        viewAddSubview(buttonsContainer, btnNext);
    }
    
    int numSettings = (int)arrayCount(settings);
    
    if (numSettings > SETTINGS_PER_PAGE) {
        CCLabel* pageLbl = labelWithFrame(ccRect(120, btnY, 80, 40));
        char buf[16];
        snprintf(buf, 16, "%d / %d", settingsPageIndex + 1, (numSettings + SETTINGS_PER_PAGE - 1) / SETTINGS_PER_PAGE);
        pageLbl->text = ccs(buf);
        pageLbl->textColor = color(1.0, 1.0, 1.0, 1.0);
        pageLbl->textAlignment = CCTextAlignmentCenter;
        pageLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(buttonsContainer, pageLbl);
    }
}

void setup_settings_ui(void) {
    printf("setup_settings_ui\n");
    currentView = CurrentViewSettings;
    settings = NULL;
    
    // 1. Init Data
    // Note: We create this only once if it's NULL, or re-create it if needed
    if (!settings) {
        settings = arrayWithObjects(
            ccs("About"), ccs("Locale"), ccs("Calendar/Clock"),
            ccs("WiFi"), ccs("Bluetooth"), ccs("Style"),
            ccs("Disk Storage"), ccs("CPU/RAM"), ccs("Power"),
            ccs("Security"), ccs("Updates"), ccs("Developer"), NULL
        );
    }
    settingsPageIndex = 0; // Reset to page 0

    // 2. Main Window Background
    float screenWidth = 320.0f;
    CCView* filesView = viewWithFrame(ccRect(0, 0, screenWidth, 480));
    filesView->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
    
    // 3. Header Setup (Same as your code)
    CCView* header = viewWithFrame(ccRect(0, 30, screenWidth, 60));
    header->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
    header->layer->shadowOpacity = 0.5;
    header->layer->shadowOffset = ccPoint(0, 10);
    
    // Gradient
    CCArray* colors1 = array();
    arrayAddObject(colors1, color(0.8, 0.8, 0.9, 1.0));
    arrayAddObject(colors1, color(0.2, 0.2, 0.3, 1.0));
    CCArray* locs1 = array();
    arrayAddObject(locs1, numberWithDouble(0.0));
    arrayAddObject(locs1, numberWithDouble(1.0));
    header->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2);
    
    //filesView->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2);
    
    CCLabel* myLabel = labelWithFrame(ccRect(0, 0, screenWidth, 60));
    myLabel->text = ccs("Settings");
    myLabel->fontSize = 24.0;
    myLabel->textAlignment = CCTextAlignmentCenter;
    myLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    myLabel->textColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(header, myLabel);
    
    viewAddSubview(filesView, header);
    
    // 4. Set Main View
    mainWindowView = filesView;

    // 5. Draw the List (First Page)
    uiSettingsContainer = NULL; // Reset container pointer
    buttonsContainer = NULL;
    update_settings_list();
}

void handle_settings_touch(int x, int y, int touchState) {
    if (touchState != 1 || !uiSettingsContainer) return; // Only on Touch Down

    // Check if we clicked a button inside the container
    CCView* clickedView = find_subview_at_point(uiSettingsContainer, x, y);
    if (!clickedView) {
        clickedView = find_subview_at_point(buttonsContainer, x, y);
    }
    
    if (clickedView) {
        if (clickedView->tag == TAG_SETTINGS_PREV) {
            printf("Prev Clicked\n");
            if (settingsPageIndex > 0) {
                settingsPageIndex--;
                update_settings_list(); // Redraw
                update_full_ui(); // Flush to screen
            }
        }
        else if (clickedView->tag == TAG_SETTINGS_NEXT) {
            printf("Next Clicked\n");
            // Safety check
            if ((settingsPageIndex + 1) * SETTINGS_PER_PAGE < settings->count) {
                settingsPageIndex++;
                update_settings_list(); // Redraw
                update_full_ui(); // Flush to screen
            }
        }
        else if (clickedView->tag >= TAG_SETTINGS_ROW_BASE) {
            int index = clickedView->tag - TAG_SETTINGS_ROW_BASE;
            //CCString* item = (CCString*)arrayObjectAtIndex(settings, index);
            printf("open row %d", index);
            if (index == 0) {
                
            }
            else if (index == 1) {
                
            }
            else if (index == 2) {
                
            }
            else if (index == 3) {
                if (mainWindowView != NULL) {
                    push_view(mainWindowView);
                }
                showRotatingImageAnimation();
                //showTriangleAnimation();
                setup_wifi_ui();
            }
            else if (index == 4) {
                
            }
        }
    }
}

// --- Calculator State ---
static CCString* calcDisplayStr = NULL;  // The text showing on screen
static double calcStoredValue = 0.0;     // The previous number typed
static char calcCurrentOp = 0;           // The operation: '+', '-', '*', '/'
static bool calcIsNewEntry = true;       // Flag to clear display on next number press

// Tag constants for non-digit buttons to avoid magic numbers
#define TAG_BTN_CLEAR 10
#define TAG_BTN_EQUALS 11
#define TAG_BTN_ADD 12
#define TAG_BTN_SUB 13
#define TAG_BTN_MUL 14
#define TAG_BTN_DIV 15

void update_calculator_label(void); // Forward declaration

void handle_calculator_input(int tag) {
    printf("\nhandle_calculator_input\n");
    // Initialize string if NULL
    if (!calcDisplayStr) calcDisplayStr = ccs("0");

    // --- A. Handle Digits (0-9) ---
    if (tag >= 0 && tag <= 9) {
        if (calcIsNewEntry) {
            // Start fresh (e.g., after pressing + or =)
            // Note: We create a new formatted string
            char buf[4];
            snprintf(buf, sizeof(buf), "%d", tag);
            
            if (calcDisplayStr) freeCCString(calcDisplayStr);
            calcDisplayStr = ccs(buf);
            
            calcIsNewEntry = false;
        } else {
            // Append to existing string
            // We need to manually concatenate C-strings
            char* oldStr = cStringOfString(calcDisplayStr);
            int newLen = strlen(oldStr) + 2; // +1 char + null terminator
            char* newBuf = cc_safe_alloc(1, newLen);
            snprintf(newBuf, newLen, "%s%d", oldStr, tag);
            
            if (calcDisplayStr) freeCCString(calcDisplayStr);
            calcDisplayStr = ccs(newBuf); // ccs uses strdup/malloc internaly
            free(newBuf);
        }
    }
    
    // --- B. Handle Clear (C) ---
    else if (tag == TAG_BTN_CLEAR) {
        if (calcDisplayStr) freeCCString(calcDisplayStr);
        calcDisplayStr = ccs("0");
        calcStoredValue = 0.0;
        calcCurrentOp = 0;
        calcIsNewEntry = true;
    }
    
    // --- C. Handle Operations (+ - * /) ---
    else if (tag >= TAG_BTN_ADD && tag <= TAG_BTN_DIV) {
        // Store the current number
        calcStoredValue = atof(cStringOfString(calcDisplayStr));
        
        // Map tag to char
        if (tag == TAG_BTN_ADD) calcCurrentOp = '+';
        if (tag == TAG_BTN_SUB) calcCurrentOp = '-';
        if (tag == TAG_BTN_MUL) calcCurrentOp = '*';
        if (tag == TAG_BTN_DIV) calcCurrentOp = '/';
        
        calcIsNewEntry = true; // Next digit typed will start a new string
    }
    
    // --- D. Handle Equals (=) ---
    else if (tag == TAG_BTN_EQUALS) {
        double currentValue = atof(cStringOfString(calcDisplayStr));
        double result = currentValue;

        switch (calcCurrentOp) {
            case '+': result = calcStoredValue + currentValue; break;
            case '-': result = calcStoredValue - currentValue; break;
            case '*': result = calcStoredValue * currentValue; break;
            case '/':
                if (currentValue != 0) result = calcStoredValue / currentValue;
                else result = 0; // Error protection
                break;
        }

        // Format Result back to String
        char resBuf[64];
        // Use %.6g to remove trailing zeros automatically
        snprintf(resBuf, sizeof(resBuf), "%.6g", result);
        
        if (calcDisplayStr) freeCCString(calcDisplayStr);
        calcDisplayStr = ccs(resBuf);
        
        // Reset op but keep value in case user hits operation again
        calcStoredValue = result;
        calcCurrentOp = 0;
        calcIsNewEntry = true;
    }
    
    printf("\nupdate_calculator_label\n");

    // Refresh the Screen
    update_calculator_label();
}

CCLabel* uiCalcLabel = NULL; // Global pointer for easy updates

void setup_calculator_ui(void) {
    currentView = CurrentViewCalculator;
    // 1. Reset Root View
    // freeViewHierarchy(mainWindowView); // Ensure you call this before switching apps!
    mainWindowView = viewWithFrame(ccRect(0, 0, 320, 480));
    mainWindowView->backgroundColor = color(0.1, 0.1, 0.1, 1.0); // Dark Background

    // 2. Result Display (Top Rounded Rect)
    CCView* displayContainer = viewWithFrame(ccRect(20, 40, 280, 80));
    layerSetCornerRadius(displayContainer->layer, 15.0);
    displayContainer->backgroundColor = color(0.2, 0.2, 0.2, 1.0); // Darker Gray
    
    // Add Shadow to Display
    displayContainer->layer->shadowOpacity = 0.5;
    displayContainer->layer->shadowRadius = 10;
    displayContainer->layer->shadowOffset = ccPoint(0, 5);
    
    viewAddSubview(mainWindowView, displayContainer);

    // 3. Result Label
    uiCalcLabel = labelWithFrame(ccRect(10, 10, 260, 60)); // Inset inside container
    uiCalcLabel->text = copyCCString(calcDisplayStr ? calcDisplayStr : ccs("0"));
    uiCalcLabel->textColor = color(1, 1, 1, 1);
    uiCalcLabel->fontSize = 32;
    uiCalcLabel->textAlignment = CCTextAlignmentRight; // Standard calc alignment
    uiCalcLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    uiCalcLabel->view->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
    
    viewAddSubview(displayContainer, uiCalcLabel);

    // 4. Button Grid Configuration
    // 4 Columns x 4 Rows
    const char* btnLabels[16] = {
        "7", "8", "9", "/",
        "4", "5", "6", "*",
        "1", "2", "3", "-",
        "C", "0", "=", "+"
    };
    
    int btnTags[16] = {
        7, 8, 9, TAG_BTN_DIV,
        4, 5, 6, TAG_BTN_MUL,
        1, 2, 3, TAG_BTN_SUB,
        TAG_BTN_CLEAR, 0, TAG_BTN_EQUALS, TAG_BTN_ADD
    };

    int startY = 140;
    int btnW = 60;
    int btnH = 60;
    int gap = 15;
    int startX = 20;

    for (int i = 0; i < 16; i++) {
        int col = i % 4;
        int row = i / 4;
        
        int x = startX + (col * (btnW + gap));
        int y = startY + (row * (btnH + gap));

        // Create Button Container
        CCView* btn = viewWithFrame(ccRect(x, y, btnW, btnH));
        layerSetCornerRadius(btn->layer, 30.0); // Fully circular/rounded
        btn->tag = btnTags[i]; // Store the Logic ID in the tag!
        
        // Color Logic
        if (btnTags[i] == TAG_BTN_EQUALS) {
             btn->backgroundColor = color(0.0, 0.6, 0.2, 1.0); // Green for Equals
        } else if (btnTags[i] >= 10) {
             btn->backgroundColor = color(1.0, 0.6, 0.0, 1.0); // Orange for Ops
        } else {
             btn->backgroundColor = color(0.3, 0.3, 0.3, 1.0); // Gray for Numbers
        }
        
        // Add Shadow
        btn->layer->shadowOpacity = 0.3;
        btn->layer->shadowRadius = 5;
        btn->layer->shadowOffset = ccPoint(2, 2);

        // Add Label
        CCLabel* lbl = labelWithFrame(ccRect(0, 0, btnW, btnH));
        lbl->text = ccs(btnLabels[i]); // Literals are safe here IF we don't double-free
        lbl->textColor = color(1, 1, 1, 1);
        lbl->fontSize = 24;
        lbl->textAlignment = CCTextAlignmentCenter;
        lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        
        // Prevent touch events on the label from blocking the button view
        // (Assuming your touch logic checks parents if child doesn't handle)
        
        viewAddSubview(btn, lbl);
        viewAddSubview(mainWindowView, btn);
    }
}

void update_calculator_label(void) {
    if (uiCalcLabel) {
        
        // 1. Clean up old memory (Prevent Leak)
        if (uiCalcLabel->text) {
             freeCCString(uiCalcLabel->text);
        }
        
        // 2. Assign New Text (Deep Copy)
        uiCalcLabel->text = copyCCString(calcDisplayStr);
        
        // 3. OPTIMIZED DRAW:
        // Instead of redrawing the whole screen (slow),
        // we ask the Parent (the gray rounded box) to redraw just this text area.
        //update_view_area_via_parent((CCView*)uiCalcLabel);
        update_label_safe(uiCalcLabel);
        printf("\nupdate_calculator_label update_view_area_via_parent\n");
    }
}

// --- Music Player Globals ---

// UI Pointers for fast updates
static CCLabel* uiMusicTitleLbl = NULL;
static CCLabel* uiMusicArtistLbl = NULL;
static CCView* uiMusicProgressFill = NULL; // We adjust the frame width of this
static CCLabel* uiMusicPlayBtnLbl = NULL;  // To toggle between â–¶ and ||

// Tag Constants for Input Handling
#define TAG_MUSIC_PREV 20
#define TAG_MUSIC_PLAY 21
#define TAG_MUSIC_NEXT 22
#define TAG_MUSIC_PROGRESS_BAR 23 // For tapping to seek later

// Layout Constants (320x480 screen)
#define SCREEN_W 320
#define MAIN_PADDING 20
#define ART_SIZE 280

void setup_music_player_ui(void) {
    // 1. Reset Root View
    currentView = CurrentViewMusic;
    //if (mainWindowView) freeViewHierarchy(mainWindowView);
    mainWindowView = viewWithFrame(ccRect(0, 0, SCREEN_W, 480));
    mainWindowView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);

    // --- TIGHTER LAYOUT MATH ---
    int topPadding = 30;
    int artSize = 220; // Reduced from 280
    int currentY = topPadding;

    //================================================================
    // 2. Album Artwork
    //================================================================
    // Centered horizontally: (320 - 220) / 2 = 50
    int artX = (SCREEN_W - artSize) / 2;
    CCView* artView = viewWithFrame(ccRect(artX, currentY, artSize, artSize));
    artView->backgroundColor = color(0.2, 0.25, 0.35, 1.0);
    layerSetCornerRadius(artView->layer, 20.0);
    artView->layer->shadowOpacity = 0.6;
    artView->layer->shadowRadius = 15;
    artView->layer->shadowOffset = ccPoint(0, 10);
    
    viewAddSubview(mainWindowView, artView);
    currentY += artSize + 25; // Gap reduced to 25

    //================================================================
    // 3. Track Information
    //================================================================
    // Song Title
    uiMusicTitleLbl = labelWithFrame(ccRect(20, currentY, 280, 28));
    uiMusicTitleLbl->text = ccs("Song Title Placeholder");
    uiMusicTitleLbl->textColor = color(1, 1, 1, 1);
    uiMusicTitleLbl->fontSize = 22; // Slightly smaller font
    uiMusicTitleLbl->textAlignment = CCTextAlignmentCenter;
    viewAddSubview(mainWindowView, uiMusicTitleLbl);
    currentY += 28;

    // Artist Name
    uiMusicArtistLbl = labelWithFrame(ccRect(20, currentY, 280, 20));
    uiMusicArtistLbl->text = ccs("Artist Name");
    uiMusicArtistLbl->textColor = color(0.7, 0.7, 0.8, 1.0);
    uiMusicArtistLbl->fontSize = 14;
    uiMusicArtistLbl->textAlignment = CCTextAlignmentCenter;
    viewAddSubview(mainWindowView, uiMusicArtistLbl);
    currentY += 20 + 20; // Gap reduced to 20

    //================================================================
    // 4. Progress Section
    //================================================================
    int progressHeight = 6;
    int progressWidth = 280; // 20px padding on each side
    int progressX = 20;
    
    // A. Track
    CCView* progressTrack = viewWithFrame(ccRect(progressX, currentY, progressWidth, progressHeight));
    progressTrack->backgroundColor = color(0.2, 0.2, 0.25, 1.0);
    layerSetCornerRadius(progressTrack->layer, progressHeight / 2.0);
    progressTrack->tag = TAG_MUSIC_PROGRESS_BAR;
    viewAddSubview(mainWindowView, progressTrack);

    // B. Fill
    uiMusicProgressFill = viewWithFrame(ccRect(progressX, currentY, 0, progressHeight)); // Width 0 init
    uiMusicProgressFill->backgroundColor = color(0.0, 0.8, 1.0, 1.0);
    layerSetCornerRadius(uiMusicProgressFill->layer, progressHeight / 2.0);
    // uiMusicProgressFill->userInteractionEnabled = false;
    viewAddSubview(mainWindowView, uiMusicProgressFill);
    currentY += progressHeight + 8;

    // C. Timestamps
    CCLabel* currTimeLbl = labelWithFrame(ccRect(20, currentY, 60, 14));
    currTimeLbl->text = ccs("0:00");
    currTimeLbl->textColor = color(0.6, 0.6, 0.7, 1.0);
    currTimeLbl->fontSize = 10;
    viewAddSubview(mainWindowView, currTimeLbl);

    CCLabel* totalTimeLbl = labelWithFrame(ccRect(SCREEN_W - 80, currentY, 60, 14));
    totalTimeLbl->text = ccs("-:--");
    totalTimeLbl->textColor = color(0.6, 0.6, 0.7, 1.0);
    totalTimeLbl->fontSize = 10;
    totalTimeLbl->textAlignment = CCTextAlignmentRight;
    viewAddSubview(mainWindowView, totalTimeLbl);
    
    currentY += 14 + 20; // Gap reduced to 20

    //================================================================
    // 5. Playback Controls
    //================================================================
    // Current Y should be approx 395px.
    // We have 85px remaining.
    
    int playBtnSize = 64; // Reduced slightly from 74
    int sideBtnSize = 44; // Reduced slightly from 54
    int spacing = 30;
    int centerX = SCREEN_W / 2;
    
    // We want the CENTER of the buttons to be at a specific Y,
    // ensuring they don't hit the bottom edge.
    // Let's place the button Top at currentY.
    // Button Bottom will be 395 + 64 = 459px. (Safe!)
    
    int controlsCenterY = currentY + (playBtnSize / 2);

    // A. Play Button
        CCView* playBtn = viewWithFrame(ccRect(centerX - (playBtnSize/2), currentY, playBtnSize, playBtnSize));
        playBtn->backgroundColor = color(0.0, 0.8, 1.0, 1.0);
        layerSetCornerRadius(playBtn->layer, playBtnSize / 2.0);
        playBtn->tag = TAG_MUSIC_PLAY;
        
        // --- SHADOW FIX ---
        playBtn->layer->shadowOpacity = 0.4;
        playBtn->layer->shadowRadius = 8;
        playBtn->layer->shadowOffset = ccPoint(0, 4); // <--- MUST BE SET to allocate the pointer!
        // ------------------
        
        uiMusicPlayBtnLbl = labelWithFrame(ccRect(0, 0, playBtnSize, playBtnSize));
        uiMusicPlayBtnLbl->text = ccs(">");
        uiMusicPlayBtnLbl->textColor = color(1, 1, 1, 1);
        uiMusicPlayBtnLbl->fontSize = 28;
        uiMusicPlayBtnLbl->textAlignment = CCTextAlignmentCenter;
        uiMusicPlayBtnLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(playBtn, uiMusicPlayBtnLbl);
        viewAddSubview(mainWindowView, playBtn);
    
    

    // B. Prev Button
    // Align centers vertically
    int prevX = centerX - (playBtnSize/2) - spacing - sideBtnSize;
    int sideBtnY = controlsCenterY - (sideBtnSize/2);
    
    CCView* prevBtn = viewWithFrame(ccRect(prevX, sideBtnY, sideBtnSize, sideBtnSize));
    prevBtn->backgroundColor = color(0.25, 0.25, 0.3, 1.0);
    layerSetCornerRadius(prevBtn->layer, sideBtnSize / 2.0);
    prevBtn->tag = TAG_MUSIC_PREV;

    CCLabel* prevLbl = labelWithFrame(ccRect(0, 0, sideBtnSize, sideBtnSize));
    prevLbl->text = ccs("|<");
    prevLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
    prevLbl->fontSize = 14;
    prevLbl->textAlignment = CCTextAlignmentCenter;
    prevLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(prevBtn, prevLbl);
    viewAddSubview(mainWindowView, prevBtn);

    // C. Next Button
    int nextX = centerX + (playBtnSize/2) + spacing;
    
    CCView* nextBtn = viewWithFrame(ccRect(nextX, sideBtnY, sideBtnSize, sideBtnSize));
    nextBtn->backgroundColor = color(0.25, 0.25, 0.3, 1.0);
    layerSetCornerRadius(nextBtn->layer, sideBtnSize / 2.0);
    nextBtn->tag = TAG_MUSIC_NEXT;

    CCLabel* nextLbl = labelWithFrame(ccRect(0, 0, sideBtnSize, sideBtnSize));
    nextLbl->text = ccs(">|");
    nextLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
    nextLbl->fontSize = 14;
    nextLbl->textAlignment = CCTextAlignmentCenter;
    nextLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(nextBtn, nextLbl);
    viewAddSubview(mainWindowView, nextBtn);
}

// percentage is 0.0 to 1.0
void update_music_progress(float percentage) {
    if (uiMusicProgressFill == NULL) return;

    // Clamp percentage safety check
    if (percentage < 0.0f) percentage = 0.0f;
    if (percentage > 1.0f) percentage = 1.0f;

    int maxWidth = SCREEN_W - (MAIN_PADDING * 2);
    int newWidth = (int)(maxWidth * percentage);

    // Update the width of the fill view
    uiMusicProgressFill->frame->size->width = newWidth;

    // Optimized Redraw: Only redraw the progress track area via its parent
    // Assuming the track is the parent, or just update the root if needed.
    // If you implemented update_view_area_via_parent:
    // update_view_area_via_parent(uiMusicProgressFill);
    
    // Fallback if parent optimization isn't ready:
    update_full_ui();
}

#define MP3_READ_BUF_SIZE 16*1024
#define PCM_BUF_SIZE 1152 * 2

// Volume (0-256)
static int current_volume = 256; // 50%

// --- AUDIO CONFIGURATION ---
#define SAMPLE_RATE     44100
#define WAVE_FREQ_HZ    440.0f
#define PI              3.14159265f

static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;

// --- HELPER: 16-bit Volume Control ---
void write_to_i2s_16bit(i2s_chan_handle_t handle, int16_t *pcm_samples, int sample_count) {
    size_t bytes_written;
    
    // Apply volume in place
    // We cast to int32_t temporarily to avoid overflow during multiplication
    for (int i = 0; i < sample_count; i++) {
        int32_t temp = (int32_t)pcm_samples[i] * current_volume;
        pcm_samples[i] = (int16_t)(temp >> 8); // Divide by 256 and cast back
    }

    // Write DIRECTLY to I2S (No 32-bit conversion needed)
    i2s_channel_write(handle, pcm_samples, sample_count * sizeof(int16_t), &bytes_written, portMAX_DELAY);
}

void play_mp3_file(const char *filename, i2s_chan_handle_t tx_handle) {
    FILE *f = fopen(filename, "rb");
    if (!f) {
        printf("Failed to open file: %s\n", filename);
        return;
    }

    // --- CHANGE: Allocate in SPIRAM (PSRAM) ---
    // This moves ~4KB + ~4KB out of internal RAM
    uint8_t *read_buf = (uint8_t *)heap_caps_malloc(MP3_READ_BUF_SIZE, MALLOC_CAP_SPIRAM);
    int16_t *pcm_buf = (int16_t *)heap_caps_malloc(PCM_BUF_SIZE * sizeof(int16_t), MALLOC_CAP_SPIRAM);

    if (!read_buf || !pcm_buf) {
        printf("Failed to allocate SPIRAM memory! Is PSRAM enabled in menuconfig?\n");
        if (read_buf) free(read_buf);
        if (pcm_buf) free(pcm_buf);
        fclose(f);
        return;
    }

    mp3dec_t mp3d;
    mp3dec_frame_info_t info;
    mp3dec_init(&mp3d);
    
    int bytes_in_buf = 0;
    int buf_offset = 0;

    while (1) {
        if (bytes_in_buf < MP3_READ_BUF_SIZE && !feof(f)) {
            if (bytes_in_buf > 0) {
                memmove(read_buf, read_buf + buf_offset, bytes_in_buf);
            }
            int bytes_read = fread(read_buf + bytes_in_buf, 1, MP3_READ_BUF_SIZE - bytes_in_buf, f);
            bytes_in_buf += bytes_read;
            buf_offset = 0;
            if (bytes_read == 0 && bytes_in_buf == 0) break;
        }

        int samples = mp3dec_decode_frame(&mp3d, read_buf + buf_offset, bytes_in_buf, pcm_buf, &info);
        
        buf_offset += info.frame_bytes;
        bytes_in_buf -= info.frame_bytes;

        if (samples > 0) {
            // Send 16-bit data
            write_to_i2s_16bit(tx_handle, pcm_buf, samples * info.channels);
        } else {
            if (bytes_in_buf > 0 && info.frame_bytes == 0) {
                buf_offset++;
                bytes_in_buf--;
            }
        }
    }

    free(read_buf);
    free(pcm_buf);
    fclose(f);
    printf("Playback finished.\n");
}

void mp3_task_wrapper(void *arg) {
    // We can pass the handle via valid logic, or just make tx_handle global
    play_mp3_file("/sdcard/frair.mp3", tx_handle);
    
    // Delete this task when song finishes so we don't crash
    vTaskDelete(NULL);
}

typedef struct {
    char riff_tag[4];       // "RIFF"
    uint32_t riff_len;      // Total file size - 8
    char wave_tag[4];       // "WAVE"
    char fmt_tag[4];        // "fmt "
    uint32_t fmt_len;       // 16 for PCM
    uint16_t audio_format;  // 1 for PCM
    uint16_t num_channels;  // 1 for Mono, 2 for Stereo
    uint32_t sample_rate;   // e.g., 44100
    uint32_t byte_rate;     // sample_rate * channels * (bit_depth/8)
    uint16_t block_align;   // channels * (bit_depth/8)
    uint16_t bits_per_sample; // 16
    char data_tag[4];       // "data"
    uint32_t data_len;      // Total bytes of audio data
} wav_header_t;

// Function to generate the header
void write_wav_header(FILE *f, int sample_rate, int channels, int total_data_len) {
    wav_header_t header;

    // RIFF Chunk
    memcpy(header.riff_tag, "RIFF", 4);
    header.riff_len = total_data_len + 36; // File size - 8
    memcpy(header.wave_tag, "WAVE", 4);

    // fmt Chunk
    memcpy(header.fmt_tag, "fmt ", 4);
    header.fmt_len = 16;
    header.audio_format = 1; // PCM
    header.num_channels = channels;
    header.sample_rate = sample_rate;
    header.bits_per_sample = 16;
    header.byte_rate = sample_rate * channels * (16 / 8);
    header.block_align = channels * (16 / 8);

    // data Chunk
    memcpy(header.data_tag, "data", 4);
    header.data_len = total_data_len;

    // Write header to beginning of file
    fseek(f, 0, SEEK_SET); // Jump to start
    fwrite(&header, sizeof(wav_header_t), 1, f);
    fseek(f, 0, SEEK_END); // Jump back to end
}

#define RECORD_TIME_SEC 10
#define SAMPLE_RATE 44100
#define CHANNELS 2      // We use Stereo mode in I2S, even if mic is mono
#define BIT_DEPTH 16

// Control Flags
static volatile bool is_recording_active = false;
static TaskHandle_t record_task_handle = NULL;

// Make sure your I2S RX Handle is global so the task can see it
// (Define this near your setupHybridAudio function)
extern i2s_chan_handle_t rx_handle;

void record_wav_task(void *args) {
    ESP_LOGI("REC", "Opening file for recording...");

    // 1. Open File
    FILE *f = fopen("/sdcard/recording.wav", "wb");
    if (f == NULL) {
        ESP_LOGE("REC", "Failed to open file!");
        is_recording_active = false;
        record_task_handle = NULL;
        vTaskDelete(NULL);
        return;
    }

    // 2. Write Placeholder Header
    wav_header_t dummy_header = {0};
    fwrite(&dummy_header, sizeof(wav_header_t), 1, f);

    // 3. Allocate Buffer
    size_t chunk_size = 4096;
    char *buffer = cc_safe_alloc(1, chunk_size);
    size_t bytes_read = 0;
    size_t total_bytes_recorded = 0;

    // 4. The Loop (Runs while is_recording_active is TRUE)
    while (is_recording_active) {
        // Read audio from I2S
        if (i2s_channel_read(rx_handle, buffer, chunk_size, &bytes_read, 100) == ESP_OK) {
            // Write audio to SD Card
            if (bytes_read > 0) {
                fwrite(buffer, 1, bytes_read, f);
                total_bytes_recorded += bytes_read;
            }
        } else {
            // Optional: Short delay to prevent watchdog starvation if I2S fails
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }

    // 5. Cleanup (The "Graceful" part)
    ESP_LOGI("REC", "Stopping... Finalizing WAV Header.");

    // Update the header with the correct file size
    write_wav_header(f, 44100, 2, total_bytes_recorded);

    fclose(f);
    free(buffer);
    
    ESP_LOGI("REC", "File saved. Task deleting.");
    record_task_handle = NULL; // Reset handle
    vTaskDelete(NULL);
}

void start_recording(void) {
    if (is_recording_active) {
        ESP_LOGW("REC", "Already recording!");
        return;
    }
    
    if (rx_handle == NULL) {
        ESP_LOGE("REC", "I2S RX Handle is NULL! Did you run setupHybridAudio?");
        return;
    }

    is_recording_active = true;
    
    ESP_LOGI("REC", "rec_task");
    
    // Create the task (Pinned to Core 1 is usually best for file I/O)
    xTaskCreatePinnedToCore(record_wav_task, "rec_task", 1024 * 6, NULL, 5, &record_task_handle, 1);
}

void stop_recording(void) {
    if (!is_recording_active) {
        return;
    }

    ESP_LOGI("REC", "Stop requested...");
    // Flip the switch. The task will exit its while() loop and clean up.
    is_recording_active = false;
}

void record_task(void *args) {
    // 1. Open File
    FILE *f = fopen("/sdcard/recording.wav", "wb");
    if (f == NULL) {
        ESP_LOGE("REC", "Failed to open file for writing");
        vTaskDelete(NULL);
    }

    // 2. Write Dummy Header (We will fix the sizes later)
    wav_header_t dummy_header = {0};
    fwrite(&dummy_header, sizeof(wav_header_t), 1, f);

    // 3. Allocation
    size_t bytes_read = 0;
    // Buffer size: 1024 samples * 2 channels * 2 bytes = 4096 bytes
    size_t chunk_size = 4096;
    char *buffer = cc_safe_alloc(1, chunk_size);
    
    int total_bytes_recorded = 0;
    int expected_bytes = RECORD_TIME_SEC * SAMPLE_RATE * CHANNELS * (BIT_DEPTH/8);

    ESP_LOGI("REC", "Start Recording...");

    // 4. Recording Loop
    while (total_bytes_recorded < expected_bytes) {
        // Read from Microphone (I2S RX Handle)
        // Note: Use 'rx_handle' here, NOT tx_handle!
        if (i2s_channel_read(rx_handle, buffer, chunk_size, &bytes_read, 1000) == ESP_OK) {
            
            // Write Raw Data to SD Card
            fwrite(buffer, 1, bytes_read, f);
            total_bytes_recorded += bytes_read;
        }
    }

    ESP_LOGI("REC", "Recording Complete. Finalizing...");

    // 5. Finalize Header
    // Go back to the top of the file and write the correct lengths
    write_wav_header(f, SAMPLE_RATE, CHANNELS, total_bytes_recorded);

    // 6. Cleanup
    fclose(f);
    free(buffer);
    vTaskDelete(NULL);
}

// --- Gallery Globals ---
static CCArray* galleryImagePaths = NULL; // Array of CCString paths
static int galleryCurrentPage = 0;
static int gallerySelectedIdx = -1; // -1 means no selection (Grid Mode)
static float galleryZoomScale = 1.0f;

// Layout Constants
#define ITEMS_PER_PAGE 12 // 3 cols x 4 rows
#define SCREEN_W 320
#define SCREEN_H 480
#define THUMB_SIZE 90
#define THUMB_GAP 10
#define TOP_MARGIN 50

// Tags for Input
#define TAG_GAL_PREV_PAGE 100
#define TAG_GAL_NEXT_PAGE 101
#define TAG_GAL_BACK      102
#define TAG_GAL_ZOOM_IN   103
#define TAG_GAL_ZOOM_OUT  104
#define TAG_GAL_PHOTO_BASE 1000 // Photo Index = Tag - 1000

void setup_gallery_ui(void); // Forward declaration

void init_gallery_data() {
    // Only load once
    if (galleryImagePaths == NULL) {
        galleryImagePaths = array();
        // Mock Data - In reality, use your get_directory_files_as_array logic here!
        arrayAddObject(galleryImagePaths, ccs("/spiflash/files.png"));
        arrayAddObject(galleryImagePaths, ccs("/spiflash/settings.png"));
        arrayAddObject(galleryImagePaths, ccs("/spiflash/messages.png"));
        // ... add more for testing pagination ...
        //for(int i=4; i<=30; i++) {
        //     char buf[32]; snprintf(buf, 32, "/spiflash/img%d.png", i);
        //     arrayAddObject(galleryImagePaths, ccs(buf));
        //}
    }
}

void handle_gallery_touch(int tag) {
    
    // A. Handle Photo Tap (Enter Detail Mode)
    if (tag >= TAG_GAL_PHOTO_BASE) {
        gallerySelectedIdx = tag - TAG_GAL_PHOTO_BASE;
        galleryZoomScale = 1.0f; // Reset zoom
        setup_gallery_ui(); // Redraw in Detail Mode
    }
    
    // B. Handle Pagination
    else if (tag == TAG_GAL_PREV_PAGE) {
        if (galleryCurrentPage > 0) {
            galleryCurrentPage--;
            setup_gallery_ui();
        }
    }
    else if (tag == TAG_GAL_NEXT_PAGE) {
        int total = galleryImagePaths->count;
        int maxPage = (total - 1) / ITEMS_PER_PAGE;
        if (galleryCurrentPage < maxPage) {
            galleryCurrentPage++;
            setup_gallery_ui();
        }
    }
    
    // C. Handle Detail View Controls
    else if (tag == TAG_GAL_BACK) {
        gallerySelectedIdx = -1; // Return to grid
        setup_gallery_ui();
    }
    else if (tag == TAG_GAL_ZOOM_IN) {
        galleryZoomScale += 0.25f;
        if (galleryZoomScale > 3.0f) galleryZoomScale = 3.0f;
        setup_gallery_ui(); // Re-layout with new scale
    }
    else if (tag == TAG_GAL_ZOOM_OUT) {
        galleryZoomScale -= 0.25f;
        if (galleryZoomScale < 0.5f) galleryZoomScale = 0.5f;
        setup_gallery_ui();
    }
}

// --- Helper: Draw Grid ---
void layout_grid_mode(void) {
    // 1. Header
    CCLabel* title = labelWithFrame(ccRect(0, 0, SCREEN_W, 40));
    char buf[32];
    snprintf(buf, 32, "Gallery - Page %d", galleryCurrentPage + 1);
    title->text = ccs(buf);
    title->textColor = color(1, 1, 1, 1);
    title->textAlignment = CCTextAlignmentCenter;
    title->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(mainWindowView, title);

    // 2. Calculate Range
    int startIdx = galleryCurrentPage * ITEMS_PER_PAGE;
    int endIdx = startIdx + ITEMS_PER_PAGE;
    if (endIdx > galleryImagePaths->count) endIdx = galleryImagePaths->count;

    // 3. Draw Grid
    int col = 0;
    int row = 0;
    
    // Center the grid: (3 * 90) + (2 * 10) = 290px wide. Screen is 320.
    // Start X = (320 - 290) / 2 = 15.
    int startX = 15;
    
    for (int i = startIdx; i < endIdx; i++) {
        int x = startX + (col * (THUMB_SIZE + THUMB_GAP));
        int y = TOP_MARGIN + (row * (THUMB_SIZE + THUMB_GAP));
        
        // Container for Image
        CCView* photoFrame = viewWithFrame(ccRect(x, y, THUMB_SIZE, THUMB_SIZE));
        photoFrame->backgroundColor = color(0.2, 0.2, 0.2, 1.0); // Placeholder color
        photoFrame->tag = TAG_GAL_PHOTO_BASE + i; // Store Index
        layerSetCornerRadius(photoFrame->layer, 8.0);
        
        // Image View
        // (Assuming you have CCImageView. If not, use CCView with image property)
        CCImageView* img = imageViewWithFrame(ccRect(0, 0, THUMB_SIZE, THUMB_SIZE));
        
        // Use DEEP COPY to prevent crash when freeing views vs array
        CCString* path = (CCString*)arrayObjectAtIndex(galleryImagePaths, i);
        img->image = imageWithFile(copyCCString(path)); // Mock implementation
        
        // Just put a Label on top if no real image loader yet
        CCLabel* lbl = labelWithFrame(ccRect(0,0,THUMB_SIZE,THUMB_SIZE));
        lbl->text = copyCCString(path); // Show filename
        lbl->fontSize = 8;
        lbl->textAlignment = CCTextAlignmentCenter;
        viewAddSubview(photoFrame, lbl);
        
        viewAddSubview(photoFrame, img);
        viewAddSubview(mainWindowView, photoFrame);
        
        // Grid Math
        col++;
        if (col >= 3) {
            col = 0;
            row++;
        }
    }

    // 4. Pagination Buttons (Bottom)
    int btnY = SCREEN_H - 50;
    int btnW = 80;
    
    // Prev Button
    if (galleryCurrentPage > 0) {
        CCView* prevBtn = viewWithFrame(ccRect(10, btnY, btnW, 40));
        prevBtn->backgroundColor = color(0.3, 0.3, 0.3, 1.0);
        layerSetCornerRadius(prevBtn->layer, 5);
        prevBtn->tag = TAG_GAL_PREV_PAGE;
        
        CCLabel* l = labelWithFrame(ccRect(0,0,btnW,40));
        l->text = ccs("< Prev");
        l->textColor = color(1,1,1,1);
        l->textAlignment = CCTextAlignmentCenter;
        l->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(prevBtn, l);
        viewAddSubview(mainWindowView, prevBtn);
    }

    // Next Button
    int maxPage = (galleryImagePaths->count - 1) / ITEMS_PER_PAGE;
    if (galleryCurrentPage < maxPage) {
        CCView* nextBtn = viewWithFrame(ccRect(SCREEN_W - 10 - btnW, btnY, btnW, 40));
        nextBtn->backgroundColor = color(0.3, 0.3, 0.3, 1.0);
        layerSetCornerRadius(nextBtn->layer, 5);
        nextBtn->tag = TAG_GAL_NEXT_PAGE;
        
        CCLabel* l = labelWithFrame(ccRect(0,0,btnW,40));
        l->text = ccs("Next >");
        l->textColor = color(1,1,1,1);
        l->textAlignment = CCTextAlignmentCenter;
        l->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(nextBtn, l);
        viewAddSubview(mainWindowView, nextBtn);
    }
}

// --- Helper: Draw Detail ---
void layout_detail_mode(void) {
    // 1. Get Path
    CCString* path = (CCString*)arrayObjectAtIndex(galleryImagePaths, gallerySelectedIdx);

    // 2. Image Container (Full Screen, Black)
    // We use a container to Clip Bounds if zoomed in heavily
    CCView* container = viewWithFrame(ccRect(0, 0, SCREEN_W, SCREEN_H));
    container->backgroundColor = color(0, 0, 0, 1.0);
    container->layer->masksToBounds = true; // Clip zoom
    viewAddSubview(mainWindowView, container);

    // 3. Calculate Zoomed Frame (Aspect Fit Simulation)
    // Base size: 320 width. Assuming square images for now: 320x320.
    float baseW = 320.0f;
    float baseH = 320.0f;
    
    float finalW = baseW * galleryZoomScale;
    float finalH = baseH * galleryZoomScale;
    
    // Center it: (Screen - Final) / 2
    float finalX = (SCREEN_W - finalW) / 2.0f;
    float finalY = (SCREEN_H - finalH) / 2.0f;

    CCImageView* fullImg = imageViewWithFrame(ccRect((int)finalX, (int)finalY, (int)finalW, (int)finalH));
    fullImg->image = imageWithFile(copyCCString(path));
    //fullImg->backgroundColor = color(0.5, 0.5, 0.5, 1.0); // Placeholder
    viewAddSubview(container, fullImg);

    // Debug Label for filename
    CCLabel* fn = labelWithFrame(ccRect(0, 0, (int)finalW, 20));
    fn->text = copyCCString(path);
    //viewAddSubview(fullImg->view, fn);

    // 4. Controls Overlay
    // Back Button (Top Left)
    CCView* backBtn = viewWithFrame(ccRect(10, 30, 60, 40));
    backBtn->backgroundColor = color(0, 0, 0, 0.5); // Semi-transparent
    layerSetCornerRadius(backBtn->layer, 5);
    backBtn->tag = TAG_GAL_BACK;
    
    CCLabel* bl = labelWithFrame(ccRect(0,0,60,40));
    bl->text = ccs("Back");
    bl->textColor = color(1,1,1,1);
    bl->textAlignment = CCTextAlignmentCenter;
    bl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(backBtn, bl);
    viewAddSubview(mainWindowView, backBtn);

    // Zoom Buttons (Bottom Center)
    int zBtnSize = 50;
    int zY = SCREEN_H - 70;
    
    // Zoom Out (-)
    CCView* zOut = viewWithFrame(ccRect((SCREEN_W/2) - 60, zY, zBtnSize, zBtnSize));
    zOut->backgroundColor = color(0,0,0,0.5);
    layerSetCornerRadius(zOut->layer, 25); // Circle
    zOut->tag = TAG_GAL_ZOOM_OUT;
    
    CCLabel* lOut = labelWithFrame(ccRect(0,0,zBtnSize,zBtnSize));
    lOut->text = ccs("-");
    lOut->textColor = color(1,1,1,1);
    lOut->fontSize = 30;
    lOut->textAlignment = CCTextAlignmentCenter;
    lOut->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(zOut, lOut);
    viewAddSubview(mainWindowView, zOut);

    // Zoom In (+)
    CCView* zIn = viewWithFrame(ccRect((SCREEN_W/2) + 10, zY, zBtnSize, zBtnSize));
    zIn->backgroundColor = color(0,0,0,0.5);
    layerSetCornerRadius(zIn->layer, 25);
    zIn->tag = TAG_GAL_ZOOM_IN;

    CCLabel* lIn = labelWithFrame(ccRect(0,0,zBtnSize,zBtnSize));
    lIn->text = ccs("+");
    lIn->textColor = color(1,1,1,1);
    lIn->fontSize = 30;
    lIn->textAlignment = CCTextAlignmentCenter;
    lIn->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(zIn, lIn);
    viewAddSubview(mainWindowView, zIn);
}

// --- Main Layout Entry Point ---
void setup_gallery_ui(void) {
    currentView = CurrentViewPhotos;
    init_gallery_data();
    
    // Clean up previous UI
    // Ensure you have safe logic for this as discussed previously!
    //if (mainWindowView) freeViewHierarchy(mainWindowView);
    
    mainWindowView = viewWithFrame(ccRect(0, 0, SCREEN_W, SCREEN_H));
    mainWindowView->backgroundColor = color(0.1, 0.1, 0.1, 1.0);

    if (gallerySelectedIdx == -1) {
        layout_grid_mode();
    } else {
        layout_detail_mode();
    }
    
    update_full_ui();
}



// Keyboard Modes
typedef enum {
    KB_MODE_ABC_LOWER,
    KB_MODE_ABC_UPPER,
    KB_MODE_NUMBERS,
    KB_MODE_SYMBOLS
} KeyboardMode;

static KeyboardMode kbCurrentMode = KB_MODE_ABC_LOWER;

// Tags for special keys
#define TAG_KB_KEY_BASE 2000 // ASCII char will be added to this
#define TAG_KB_SHIFT    3001
#define TAG_KB_BACK     3002
#define TAG_KB_MODE_123 3003
#define TAG_KB_MODE_ABC 3004
#define TAG_KB_SPACE    3005
#define TAG_KB_RETURN   3006
#define TAG_KB_SYM      3007

// Layout Constants
#define KB_HEIGHT 200
#define KEY_MARGIN 2
#define KEY_HEIGHT 40

CCView* create_key_btn(const char* text, int x, int y, int w, int h, int tag) {
    // Container
    CCView* key = viewWithFrame(ccRect(x, y, w, h));
    key->backgroundColor = color(0.3, 0.3, 0.35, 1.0); // Dark Blue-Gray key
    layerSetCornerRadius(key->layer, 6.0);
    key->tag = tag;
    
    // Shadow for depth
    //key->layer->shadowOpacity = 0.3;
    //key->layer->shadowRadius = 2;
    //key->layer->shadowOffset = ccPoint(0, 2);

    // Label
    CCLabel* lbl = labelWithFrame(ccRect(0, 0, w, h));
    
    // CRITICAL: Use copyCCString to prevent double-free crashes if layout is rebuilt
    CCString* tempStr = ccs(text);
    lbl->text = copyCCString(tempStr);
    freeCCString(tempStr); // Clean up the temp wrapper
    
    lbl->textColor = color(1, 1, 1, 1);
    lbl->fontSize = 18;
    lbl->textAlignment = CCTextAlignmentCenter;
    lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    
    viewAddSubview(key, lbl);
    return key;
}

void layout_keyboard_keys(void) {
    if (!uiKeyboardView) return;
    
    if (uiKeyboardView && uiKeyboardView->subviews) {
            for (int i = uiKeyboardView->subviews->count - 1; i >= 0; i--) {
                CCView* keyView = (CCView*)arrayObjectAtIndex(uiKeyboardView->subviews, i);
                viewRemoveFromSuperview(keyView);
                // Optionally free memory if your system doesn't auto-handle it
                 freeViewHierarchy(keyView);
            }
        }

    // 1. Clear existing keys (Clean rebuild)
    //freeViewHierarchy(uiKeyboardView);
    // Re-initialize the container logic (since freeViewHierarchy freed the struct)
    // Actually, it's safer to just remove subviews if we want to keep the container.
    // But given your framework, let's assume uiKeyboardView is just a pointer we manage.
    // Let's assume the container is passed in or managed externally.
    // Ideally: remove all subviews.
    // For now, let's assume we are rebuilding the subviews of a fresh container.

    // Row definitions based on Mode
    const char* row1 = "";
    const char* row2 = "";
    const char* row3 = "";
    
    if (kbCurrentMode == KB_MODE_ABC_LOWER) {
        row1 = "qwertyuiop";
        row2 = "asdfghjkl";
        row3 = "zxcvbnm";
    } else if (kbCurrentMode == KB_MODE_ABC_UPPER) {
        row1 = "QWERTYUIOP";
        row2 = "ASDFGHJKL";
        row3 = "ZXCVBNM";
    } else if (kbCurrentMode == KB_MODE_NUMBERS) {
        row1 = "1234567890";
        row2 = "-/:;()$&@";
        row3 = ".,?!'";
    }

    int screenW = 320;
    int keyW = (screenW - (KEY_MARGIN * 11)) / 10; // Approx 30px
    int startY = 10;
    
    // --- ROW 1 ---
    int len1 = strlen(row1);
    int x = KEY_MARGIN;
    for (int i=0; i<len1; i++) {
        char buf[2] = {row1[i], '\0'};
        // Tag is the ASCII value + Base
        viewAddSubview(uiKeyboardView, create_key_btn(buf, x, startY, keyW, KEY_HEIGHT, TAG_KB_KEY_BASE + row1[i]));
        x += keyW + KEY_MARGIN;
    }

    // --- ROW 2 ---
    int len2 = strlen(row2);
    x = KEY_MARGIN + (keyW / 2); // Indent row 2
    for (int i=0; i<len2; i++) {
        char buf[2] = {row2[i], '\0'};
        viewAddSubview(uiKeyboardView, create_key_btn(buf, x, startY + KEY_HEIGHT + KEY_MARGIN, keyW, KEY_HEIGHT, TAG_KB_KEY_BASE + row2[i]));
        x += keyW + KEY_MARGIN;
    }

    // --- ROW 3 (Shift, Letters, Back) ---
    int y3 = startY + (KEY_HEIGHT + KEY_MARGIN) * 2;
    
    // Shift/Symbol Key (Left)
    if (kbCurrentMode == KB_MODE_NUMBERS) {
        // Switch back to ABC
        viewAddSubview(uiKeyboardView, create_key_btn("ABC", KEY_MARGIN, y3, keyW*1.5, KEY_HEIGHT, TAG_KB_MODE_ABC));
    } else {
        // Shift Key
        const char* shiftLbl = (kbCurrentMode == KB_MODE_ABC_UPPER) ? "S" : "s"; // visual indicator
        viewAddSubview(uiKeyboardView, create_key_btn(shiftLbl, KEY_MARGIN, y3, keyW*1.5, KEY_HEIGHT, TAG_KB_SHIFT));
    }

    // Letters
    int len3 = strlen(row3);
    x = KEY_MARGIN + (keyW * 1.5) + KEY_MARGIN;
    for (int i=0; i<len3; i++) {
        char buf[2] = {row3[i], '\0'};
        viewAddSubview(uiKeyboardView, create_key_btn(buf, x, y3, keyW, KEY_HEIGHT, TAG_KB_KEY_BASE + row3[i]));
        x += keyW + KEY_MARGIN;
    }

    // Backspace (Right)
    int backW = screenW - x - KEY_MARGIN;
    viewAddSubview(uiKeyboardView, create_key_btn("<-", x, y3, backW, KEY_HEIGHT, TAG_KB_BACK));

    // --- ROW 4 (123, Space, Return) ---
    int y4 = startY + (KEY_HEIGHT + KEY_MARGIN) * 3;
    
    // 123 Mode
    viewAddSubview(uiKeyboardView, create_key_btn("123", KEY_MARGIN, y4, keyW*2, KEY_HEIGHT, TAG_KB_MODE_123));
    
    // Space
    int spaceW = screenW - (keyW*4) - (KEY_MARGIN*4);
    viewAddSubview(uiKeyboardView, create_key_btn("Space", KEY_MARGIN + keyW*2 + KEY_MARGIN, y4, spaceW, KEY_HEIGHT, TAG_KB_SPACE));
    
    // Return
    viewAddSubview(uiKeyboardView, create_key_btn("Ret", KEY_MARGIN + keyW*2 + KEY_MARGIN + spaceW + KEY_MARGIN, y4, screenW - (KEY_MARGIN + keyW*2 + KEY_MARGIN + spaceW + KEY_MARGIN) - KEY_MARGIN, KEY_HEIGHT, TAG_KB_RETURN));
}

void setup_keyboard_ui(CCLabel* targetLabel) {
    printf("--- Setup Keyboard UI Start ---\n");

    // 1. Set Globals
    uiTargetLabel = targetLabel;
    
    // Setup Input Buffer
    if (uiInputBuffer) freeCCString(uiInputBuffer);
    if (targetLabel && targetLabel->text) {
        uiInputBuffer = copyCCString(targetLabel->text);
    } else {
        uiInputBuffer = ccs("");
    }

    // 2. Clear previous keyboard if it exists
    if (uiKeyboardView) {
        // Since we are about to overwrite the pointer, we should ideally remove the old one
        // But for now, just NULLing it to start fresh is safe enough if freeViewHierarchy handles the rest
        uiKeyboardView = NULL;
    }

    // 3. Create Container
    // Use CC_CALLOC via viewWithFrame.
    // MAKE SURE YOU ARE NOT TYPING "CCView* uiKeyboardView =" here.
    uiKeyboardView = viewWithFrame(ccRect(0, 480 - 200, 320, 200));
    
    // --- DEBUG CHECK 1 ---
    if (uiKeyboardView == NULL) {
        printf("CRITICAL ERROR: uiKeyboardView is NULL. Out of Memory?\n");
        return;
    } else {
        printf("Keyboard View Allocated at %p\n", uiKeyboardView);
    }

    uiKeyboardView->backgroundColor = color(0.15, 0.15, 0.18, 1.0);
    uiKeyboardView->layer->shadowOpacity = 0.5;
    uiKeyboardView->layer->shadowRadius = 10;
    // Fix crash risk: Explicitly set offset
    uiKeyboardView->layer->shadowOffset = ccPoint(0, -5);

    // 4. Populate Keys
    layout_keyboard_keys();
    
    // --- DEBUG CHECK 2 ---
    if (uiKeyboardView->subviews == NULL || uiKeyboardView->subviews->count == 0) {
        printf("WARNING: Keyboard Container created, but Keys are missing!\n");
    } else {
        printf("Keyboard Keys Added: %d keys\n", uiKeyboardView->subviews->count);
    }

    // 5. Add to Main Window
    viewAddSubview(mainWindowView, uiKeyboardView);
    
    // 6. Force Update
    update_full_ui();
    printf("--- Setup Keyboard UI Done ---\n");
}

// Helper to Traverse Subviews and find touched key
CCView* find_key_at_point(int x, int y) {
    if (!uiKeyboardView || !uiKeyboardView->subviews) return NULL;
    
    // Iterate Backwards (Topmost view first)
    for (int i = uiKeyboardView->subviews->count - 1; i >= 0; i--) {
        CCView* key = (CCView*)arrayObjectAtIndex(uiKeyboardView->subviews, i);
        
        // Convert screen X/Y to Keyboard-Relative X/Y
        int localX = x - (int)uiKeyboardView->frame->origin->x;
        int localY = y - (int)uiKeyboardView->frame->origin->y;
        
        // Check intersection
        if (localX >= key->frame->origin->x &&
            localX <= key->frame->origin->x + key->frame->size->width &&
            localY >= key->frame->origin->y &&
            localY <= key->frame->origin->y + key->frame->size->height) {
            
            return key;
        }
    }
    return NULL;
}

void handle_keyboard_touch(int x, int y) {
    // 1. Find the Key
    CCView* key = find_key_at_point(x, y);
    if (!key) return;
    
    int tag = key->tag;
    
    // 2. Handle Logic
    if (tag >= TAG_KB_KEY_BASE && tag < TAG_KB_KEY_BASE + 255) {
        // --- Character Key ---
        char c = (char)(tag - TAG_KB_KEY_BASE);
        
        // Append to buffer
        char* oldStr = cStringOfString(uiInputBuffer);
        int len = strlen(oldStr);
        char* newBuf = calloc(1, len + 2); // +1 char, +1 null
        strcpy(newBuf, oldStr);
        newBuf[len] = c;
        
        freeCCString(uiInputBuffer);
        uiInputBuffer = ccs(newBuf); // ccs uses strdup/malloc
        free(newBuf);
    }
    else if (tag == TAG_KB_SPACE) {
        // Append Space
        char* oldStr = cStringOfString(uiInputBuffer);
        int len = strlen(oldStr);
        char* newBuf = calloc(1, len + 2);
        strcpy(newBuf, oldStr);
        newBuf[len] = ' ';
        
        freeCCString(uiInputBuffer);
        uiInputBuffer = ccs(newBuf);
        free(newBuf);
    }
    else if (tag == TAG_KB_BACK) {
        // Backspace
        char* oldStr = cStringOfString(uiInputBuffer);
        int len = strlen(oldStr);
        if (len > 0) {
            char* newBuf = strdup(oldStr);
            newBuf[len - 1] = '\0'; // Truncate
            
            freeCCString(uiInputBuffer);
            uiInputBuffer = ccs(newBuf);
            free(newBuf);
        }
    }
    else if (tag == TAG_KB_SHIFT) {
        // Toggle Shift
        kbCurrentMode = (kbCurrentMode == KB_MODE_ABC_LOWER) ? KB_MODE_ABC_UPPER : KB_MODE_ABC_LOWER;
        layout_keyboard_keys(); // Rebuild UI
        update_full_ui(); // Redraw
        return; // Don't update text label
    }
    else if (tag == TAG_KB_MODE_123) {
        kbCurrentMode = KB_MODE_NUMBERS;
        layout_keyboard_keys();
        update_full_ui();
        return;
    }
    else if (tag == TAG_KB_MODE_ABC) {
        kbCurrentMode = KB_MODE_ABC_LOWER;
        layout_keyboard_keys();
        update_full_ui();
        return;
    }
    else if (tag == TAG_KB_RETURN) {
        // Done editing
        // Hide keyboard logic here if you want
        printf("Return pressed. Final text: %s\n", cStringOfString(uiInputBuffer));
        return;
    }

    // 3. Update Target Label
    if (uiTargetLabel) {
        if (uiTargetLabel->text) freeCCString(uiTargetLabel->text);
        uiTargetLabel->text = copyCCString(uiInputBuffer);
        
        // Optimized Redraw
        //update_view_area_via_parent((CCView*)uiTargetLabel->view);
        //update_view_only((CCView*)uiTargetLabel->view);
        //update_full_ui();
        update_label_safe(uiTargetLabel);
    }
}

void debug_print_view_hierarchy(CCView* view, int depth) {
    if (!view) {
        printf("View is NULL\n");
        return;
    }

    // Indentation
    for (int i=0; i<depth; i++) printf("  ");

    // Print View Details
    printf("- Type: %d | Frame: %d, %d, %d, %d | Subviews: %d | Addr: %p\n",
           view->type,
           (int)view->frame->origin->x, (int)view->frame->origin->y,
           (int)view->frame->size->width, (int)view->frame->size->height,
           (view->subviews ? view->subviews->count : 0),
           view);

    // Recurse
    if (view->subviews) {
        for (int i=0; i<view->subviews->count; i++) {
            void* child = arrayObjectAtIndex(view->subviews, i);
            // Unwrap if needed (Label, etc) - simplified for View/Container check
            // Assuming child is CCView* or compatible wrapper start
             CCView* childView = (CCView*)child;
             // Check if it's a wrapper to get the real view for frame printing
             // (Logic from viewAddSubview)
             CCType type = *((CCType*)child);
             if (type == CCType_Label) childView = ((CCLabel*)child)->view;
             // ... etc
             
             debug_print_view_hierarchy(childView, depth + 1);
        }
    }
}

void setup_text_ui(void) {
    currentView = CurrentViewText;
    //if (mainWindowView) freeViewHierarchy(mainWindowView);
    mainWindowView = viewWithFrame(ccRect(0, 0, SCREEN_W, 480));
    mainWindowView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
    
    CCLabel* myLabel = labelWithFrame(ccRect(15, 0, 320-30, 120));
    myLabel->text = ccs("This is a long string that will now auto-wrap inside the box!");
    myLabel->fontSize = 12.0;
    myLabel->textAlignment = CCTextAlignmentCenter; // It will center!
    myLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter; // It will center!
    myLabel->lineBreakMode = CCLineBreakWordWrapping; // It will wrap!
    myLabel->textColor = color(1.0, 1.0, 1.0, 1.0);
    viewAddSubview(mainWindowView, myLabel);
    
    myLabel->view->backgroundColor = mainWindowView->backgroundColor;
    
    setup_keyboard_ui(myLabel);
    
    printf("--- UI DEBUG START ---\n");
    debug_print_view_hierarchy(mainWindowView, 0);
    printf("--- UI DEBUG END ---\n");
}

// --- WiFi Layout Globals ---
static CCView* uiWifiContainer = NULL;      // Main container
static CCView* uiWifiListContainer = NULL;  // Scrollable area for networks
static CCView* uiWifiToggleBtn = NULL;      // On/Off Switch
static bool isWifiEnabled = true;           // State Tracker

// Tag Constants
#define TAG_WIFI_TOGGLE      4000
#define TAG_WIFI_NET_BASE    4100 // Network rows start here
#define TAG_WIFI_BACK        4001

// Layout Constants
#define WIFI_ROW_HEIGHT      50
#define WIFI_HEADER_HEIGHT   140


// --- Global Data Storage ---
WifiNetwork g_wifi_scan_results[MAX_WIFI_RESULTS];
int g_wifi_scan_count = 0;
static bool wifi_initialized = false;
// --- Pagination Globals ---
static int g_wifi_page_index = 0;
#define WIFI_ITEMS_PER_PAGE  5

// New Tags for Buttons
#define TAG_WIFI_BTN_PREV    4002
#define TAG_WIFI_BTN_NEXT    4003

CCView* create_wifi_row(const char* ssid, int rssi, int index, int yPos) {
    // 1. Row Container
    CCView* row = viewWithFrame(ccRect(0, yPos, 320, WIFI_ROW_HEIGHT));
    row->backgroundColor = color(0.12, 0.12, 0.16, 1.0); // Slightly lighter background
    row->tag = TAG_WIFI_NET_BASE + index;
    
    // Separator Line (Bottom)
    CCView* line = viewWithFrame(ccRect(15, WIFI_ROW_HEIGHT - 1, 320-15, 1));
    line->backgroundColor = color(0.3, 0.3, 0.35, 1.0);
    viewAddSubview(row, line);

    // 2. SSID Label (Left)
    CCLabel* nameLbl = labelWithFrame(ccRect(15, 0, 200, WIFI_ROW_HEIGHT));
    nameLbl->text = ccs(ssid);
    nameLbl->textColor = color(1, 1, 1, 1);
    nameLbl->fontSize = 18;
    nameLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(row, nameLbl);

    // 3. Signal Strength Label (Right)
    CCLabel* signalLbl = labelWithFrame(ccRect(250, 0, 55, WIFI_ROW_HEIGHT));
    
    // Simple visual RSSI logic
    if (rssi > -60) signalLbl->text = ccs("High");
    else if (rssi > -80) signalLbl->text = ccs("Med");
    else signalLbl->text = ccs("Low");
    
    signalLbl->textColor = color(0.6, 0.6, 0.6, 1.0);
    signalLbl->fontSize = 12;
    signalLbl->textAlignment = CCTextAlignmentRight;
    signalLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(row, signalLbl);

    return row;
}

// Remove the old mockNetworks array and use the external globals

void refresh_wifi_list_ui(void) {
    if (!uiWifiListContainer) return;

    // 1. Clear previous list
    // (Assuming you have a way to clear children, otherwise we rely on rebuilding the container)
    // If you don't have removeAllSubviews, you might need to recreate uiWifiListContainer in setup_wifi_ui
    // For now, let's assume we are rebuilding the list container content.
    
    if (uiWifiListContainer->subviews) {
        // Rudimentary clear: resetting count (Warning: causes memory leak if views aren't freed)
        // Ideally: freeViewHierarchy(uiWifiListContainer); then recreate it.
        // Or loop and remove.
        uiWifiListContainer->subviews = array();
        //uiWifiListContainer->subviews->count = 0;
    }

    if (!isWifiEnabled) {
        CCLabel* msg = labelWithFrame(ccRect(0, 50, 320, 30));
        msg->text = ccs("Wi-Fi is Disabled");
        msg->textColor = color(0.5, 0.5, 0.5, 1.0);
        msg->textAlignment = CCTextAlignmentCenter;
        viewAddSubview(uiWifiListContainer, msg);
        return;
    }

    // 2. Calculate Page Slice
    int start_idx = g_wifi_page_index * WIFI_ITEMS_PER_PAGE;
    int end_idx = start_idx + WIFI_ITEMS_PER_PAGE;
    if (end_idx > g_wifi_scan_count) end_idx = g_wifi_scan_count;

    // 3. Render Rows
    int currentY = 0;
    
    // Add "No Networks" message if empty
    if (g_wifi_scan_count == 0) {
        CCLabel* msg = labelWithFrame(ccRect(0, 50, 320, 30));
        msg->text = ccs("Searching...");
        msg->textColor = color(0.6, 0.6, 0.6, 1.0);
        msg->textAlignment = CCTextAlignmentCenter;
        viewAddSubview(uiWifiListContainer, msg);
    }

    for (int i = start_idx; i < end_idx; i++) {
        // Pass 'i' as the index so the tag matches the global array index
        CCView* row = create_wifi_row(
            g_wifi_scan_results[i].ssid,
            g_wifi_scan_results[i].rssi,
            i,
            currentY
        );
        viewAddSubview(uiWifiListContainer, row);
        currentY += WIFI_ROW_HEIGHT;
    }

    // 4. Render Pagination Controls (Footer)
    // We place this below the rows
    int footerY = WIFI_ITEMS_PER_PAGE * WIFI_ROW_HEIGHT + 10;
    
    // PREV Button
    if (g_wifi_page_index > 0) {
        CCView* btnPrev = viewWithFrame(ccRect(20, footerY, 100, 40));
        btnPrev->backgroundColor = color(0.2, 0.2, 0.25, 1.0);
        layerSetCornerRadius(btnPrev->layer, 8);
        btnPrev->tag = TAG_WIFI_BTN_PREV;
        
        CCLabel* lbl = labelWithFrame(ccRect(0,0,100,40));
        lbl->text = ccs("< Prev");
        lbl->textColor = color(1,1,1,1);
        lbl->textAlignment = CCTextAlignmentCenter;
        lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(btnPrev, lbl);
        
        viewAddSubview(uiWifiListContainer, btnPrev);
    }

    // NEXT Button
    if (end_idx < g_wifi_scan_count) {
        CCView* btnNext = viewWithFrame(ccRect(200, footerY, 100, 40));
        btnNext->backgroundColor = color(0.2, 0.2, 0.25, 1.0);
        layerSetCornerRadius(btnNext->layer, 8);
        btnNext->tag = TAG_WIFI_BTN_NEXT;
        
        CCLabel* lbl = labelWithFrame(ccRect(0,0,100,40));
        lbl->text = ccs("Next >");
        lbl->textColor = color(1,1,1,1);
        lbl->textAlignment = CCTextAlignmentCenter;
        lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(btnNext, lbl);
        
        viewAddSubview(uiWifiListContainer, btnNext);
    }
    
    printf("wifi scan count %d", g_wifi_scan_count);
    
    // Page Number Indicator
    if (g_wifi_scan_count > WIFI_ITEMS_PER_PAGE) {
        CCLabel* pageLbl = labelWithFrame(ccRect(120, footerY, 80, 40));
        char buf[16];
        snprintf(buf, 16, "%d / %d", g_wifi_page_index + 1, (g_wifi_scan_count + WIFI_ITEMS_PER_PAGE - 1) / WIFI_ITEMS_PER_PAGE);
        pageLbl->text = ccs(buf);
        pageLbl->textColor = color(0.5, 0.5, 0.5, 1.0);
        pageLbl->textAlignment = CCTextAlignmentCenter;
        pageLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(uiWifiListContainer, pageLbl);
    }
}

void turn_off_wifi_and_free_ram(void) {
    ESP_LOGW("WIFI", "Shutting down Wi-Fi to reclaim RAM...");

    // 1. Stop the Wi-Fi Driver (Turns off Radio, saves power)
    // Memory is STILL allocated here.
    esp_err_t err = esp_wifi_stop();
    if (err == ESP_ERR_WIFI_NOT_INIT) {
        ESP_LOGW("WIFI", "Wi-Fi was not initialized, nothing to do.");
        return;
    }
    ESP_ERROR_CHECK(err);

    // 2. De-Initialize the Driver (The Magic Step)
    // This effectively "un-mallocs" the 60KB+ of Internal RAM.
    ESP_ERROR_CHECK(esp_wifi_deinit());

    // Optional: Destroy the default pointers if you created them
    // (This cleans up small remaining handles, but deinit does the heavy lifting)
    // esp_netif_destroy_default_wifi(your_netif_handle);

    ESP_LOGW("WIFI", "Wi-Fi De-initialized. RAM should be back.");
    
    // Prove it
    ESP_LOGI("MEM", "Free Internal Heap: %ld bytes", (long)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
}

// 1. One-Time Initialization
// Call this inside app_main() BEFORE you show any UI!
void init_wifi_stack_once(void) {
    checkAvailableMemory();
    if (wifi_initialized) return; // Prevention

    // Initialize NVS (Required for WiFi)
   /* esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);*/

    // Initialize Network Stack
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif);

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    
    // 1. Slash the RX/TX Buffers (The biggest RAM savers)
    // Default is usually 32. We drop to 4-8.
    // Note: This might reduce max throughput speed, but fixes the crash.
    cfg.dynamic_rx_buf_num = 4;  // Minimum safe value
    cfg.cache_tx_buf_num = 4;    // Minimum safe value

    // 2. Cripple the AMPDU (Aggregation)
    // AMPDU is "High Speed Mode". We don't need 100mbps for audio.
    cfg.ampdu_rx_enable = 0;     // Disable completely if possible, or set extremely low
    cfg.ampdu_tx_enable = 0;     // Disable completely

    // 3. Force "nvs" off (Optional, saves a tiny bit of RAM/Flash ops)
    cfg.nvs_enable = 0;
    
    
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());
    
    esp_wifi_set_ps(WIFI_PS_NONE);

    wifi_initialized = true;
    printf("WiFi Stack Initialized.\n");
    checkAvailableMemory();
}

void trigger_wifi_scan(void) {
    if (!wifi_initialized) {
        printf("Error: WiFi not initialized! Call init_wifi_stack_once() first.\n");
        return;
    }
    
    // Reset to first page
    g_wifi_page_index = 0;
    
    printf("Starting WiFi Scan...\n");

    // 1. Start Scan (Blocking)
    // This blocks for ~1.5s. Ensure Watchdog doesn't bite if this task is high priority.
    esp_wifi_scan_start(NULL, true);

    // 2. Get number of results found
    uint16_t ap_count = 0;
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
    
    // Safety limit
    if (ap_count > MAX_WIFI_RESULTS) ap_count = MAX_WIFI_RESULTS;

    // 3. FIX: Allocate buffer on HEAP, not Stack
    wifi_ap_record_t *ap_info = (wifi_ap_record_t *)calloc(ap_count, sizeof(wifi_ap_record_t));
    
    if (!ap_info) {
        printf("CRITICAL: Out of memory for WiFi scan!\n");
        return;
    }

    // 4. Get Records into Heap Buffer
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_info));

    // 5. Update Global Data
    g_wifi_scan_count = ap_count;
    
    for (int i = 0; i < ap_count; i++) {
        // Safe Copy
        strncpy(g_wifi_scan_results[i].ssid, (char *)ap_info[i].ssid, 32);
        g_wifi_scan_results[i].ssid[32] = '\0';
        
        g_wifi_scan_results[i].rssi = ap_info[i].rssi;
        g_wifi_scan_results[i].channel = ap_info[i].primary;
        g_wifi_scan_results[i].auth_mode = ap_info[i].authmode;
        
        // Debug
        // printf("[%d] %s (%d)\n", i, g_wifi_scan_results[i].ssid, g_wifi_scan_results[i].rssi);
    }
    
    // 6. Free the Heap Buffer
    free(ap_info);
    
    printf("Scan Done. Found %d networks.\n", g_wifi_scan_count);
    
    // 7. Refresh UI
    refresh_wifi_list_ui();
    update_full_ui();
}

void hideTriangleAnimation(void);
void triangle_animation_task(void *pvParameter);

// Defines the variable (starts as NULL because no task is running yet)
TaskHandle_t g_triangle_task_handle = NULL;
TaskHandle_t g_image_task_handle = NULL;

// Parameters for the image animation
#define IMG_ANIM_X 130  // 160 - 50 (Centered)
#define IMG_ANIM_Y 210  // 240 - 50 (Centered)
#define IMG_ANIM_W 60
#define IMG_ANIM_H 60

void setup_wifi_ui(void) {
    // 1. Reset Root View
    // if (mainWindowView) freeViewHierarchy(mainWindowView);
    currentView = CurrentViewWifi;
    init_wifi_stack_once();//
    trigger_wifi_scan();
    
    
    mainWindowView = viewWithFrame(ccRect(0, 0, 320, 480));
    mainWindowView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);

    //================================================================
    // 2. Header Area
    //================================================================
    CCLabel* title = labelWithFrame(ccRect(0, 40, 320, 30));
    title->text = ccs("Wi-Fi Networks");
    title->textColor = color(1, 1, 1, 1);
    title->fontSize = 24;
    title->textAlignment = CCTextAlignmentCenter;
    viewAddSubview(mainWindowView, title);

    // --- Toggle Switch Area ---
    int switchY = 90;
    
    // Label
    CCLabel* lbl = labelWithFrame(ccRect(20, switchY, 100, 30));
    lbl->text = ccs("Wi-Fi");
    lbl->fontSize = 20;
    lbl->textColor = color(1,1,1,1);
    lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    viewAddSubview(mainWindowView, lbl);

    // Toggle Button (Container)
    uiWifiToggleBtn = viewWithFrame(ccRect(240, switchY, 60, 30));
    uiWifiToggleBtn->tag = TAG_WIFI_TOGGLE;
    layerSetCornerRadius(uiWifiToggleBtn->layer, 15);
    
    // Color Logic
    if (isWifiEnabled) uiWifiToggleBtn->backgroundColor = color(0.2, 0.8, 0.4, 1.0); // Green
    else               uiWifiToggleBtn->backgroundColor = color(0.4, 0.4, 0.4, 1.0); // Grey

    // Knob (Circle)
    int knobX = isWifiEnabled ? 32 : 2;
    CCView* knob = viewWithFrame(ccRect(knobX, 2, 26, 26));
    knob->backgroundColor = color(1,1,1,1);
    layerSetCornerRadius(knob->layer, 13);
    // knob->userInteractionEnabled = false; // Pass touches through
    viewAddSubview(uiWifiToggleBtn, knob);
    
    viewAddSubview(mainWindowView, uiWifiToggleBtn);

    //================================================================
    // 3. Network List Area
    //================================================================
    uiWifiListContainer = viewWithFrame(ccRect(0, WIFI_HEADER_HEIGHT, 320, 480 - WIFI_HEADER_HEIGHT));
    // uiWifiListContainer->clipsToBounds = true; // Essential for scrolling lists
    viewAddSubview(mainWindowView, uiWifiListContainer);

    
    hideRotatingImageAnimation();
    
    
    refresh_wifi_list_ui();
    
    //hideTriangleAnimation();
    
    
    update_full_ui();
    
    
}





void hideTriangleAnimation(void) {
    if (g_triangle_task_handle != NULL) {
        ESP_LOGI(TAG, "Stopping Triangle Animation...");
        
        // 1. Delete the task
        vTaskDelete(g_triangle_task_handle);
        g_triangle_task_handle = NULL;
        
        // 2. Restore the background one last time to clean up the screen
        GraphicsCommand cmd_restore = {
            .cmd = CMD_ANIM_RESTORE_BG,
            .x = ANIM_X, .y = ANIM_Y,
            .w = ANIM_W, .h = ANIM_H
        };
        xQueueSend(g_graphics_queue, &cmd_restore, portMAX_DELAY);
        
        // 3. Flush the clean screen
        GraphicsCommand cmd_flush = {
            .cmd = CMD_UPDATE_AREA,
            .x = ANIM_X, .y = ANIM_Y,
            .w = ANIM_W, .h = ANIM_H
        };
        xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
        
        // 4. Free the background buffer
        // free_anim_buffer(); // Implement this if your init_anim_buffer uses malloc
    }
}

void showRotatingImageAnimation(void) {
    // 1. Initialize buffer for background save/restore
    init_anim_buffer(); // Ensure this handles the size defined in IMG_ANIM_W/H
    
    // 2. Wait for previous UI to settle
    vTaskDelay(pdMS_TO_TICKS(500));
    ESP_LOGI(TAG, "CMD_ANIM_SAVE_BG for Image");
    
    // 3. Command: Snapshot the background behind where the image will spin
    GraphicsCommand cmd_save = {
        .cmd = CMD_ANIM_SAVE_BG,
        .x = IMG_ANIM_X-30, .y = IMG_ANIM_Y-30,
        .w = IMG_ANIM_W+60, .h = IMG_ANIM_H+60
    };
    xQueueSend(g_graphics_queue, &cmd_save, portMAX_DELAY);
    
    // 4. Sync: Wait for snapshot to finish
    vTaskDelay(pdMS_TO_TICKS(100));
    
    // 5. Start the Animation Task
    xTaskCreatePinnedToCore(
        rotating_image_task,
        "rot_img_task",
        4096,
        NULL,
        4,
        &g_image_task_handle, // Save handle for cancellation
        0
    );
}

void hideRotatingImageAnimation(void) {
    if (g_image_task_handle != NULL) {
        ESP_LOGI(TAG, "Stopping Rotating Image Animation...");
        
        // 1. Delete the task
        vTaskDelete(g_image_task_handle);
        g_image_task_handle = NULL;
        
        // 2. Restore background to wipe the spinner
        GraphicsCommand cmd_restore = {
            .cmd = CMD_ANIM_RESTORE_BG,
            .x = IMG_ANIM_X, .y = IMG_ANIM_Y,
            .w = IMG_ANIM_W, .h = IMG_ANIM_H
        };
        xQueueSend(g_graphics_queue, &cmd_restore, portMAX_DELAY);
        
        // 3. Flush
        GraphicsCommand cmd_flush = {
            .cmd = CMD_UPDATE_AREA,
            .x = IMG_ANIM_X, .y = IMG_ANIM_Y,
            .w = IMG_ANIM_W, .h = IMG_ANIM_H
        };
        xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
        
        // 4. Free buffer
        // free_anim_buffer();
    }
}

void rotating_image_task(void *pvParameter) {
    ESP_LOGI(TAG, "rotating_image_task started");
    
    float angle = 0.0f;
    const float speed = 0.15f;
    
    // Geometry Constants
    float w = (float)IMG_ANIM_W;
    float h = (float)IMG_ANIM_H;
    float cx = IMG_ANIM_X + (w / 2.0f);
    float cy = IMG_ANIM_Y + (h / 2.0f);
    
    // Pre-calculate the "centering" matrix (It never changes)
    // This moves the image's top-left from (0,0) to (-w/2, -h/2)
    // so that (0,0) becomes the center of the image.
    Matrix3x3 mat_center_offset = TranslationMatrix(-w / 2.0f, -h / 2.0f);
    Matrix3x3 mat_screen_position = TranslationMatrix(cx, cy);

    while (1) {
        // --- STEP 1: RESTORE BACKGROUND ---
        GraphicsCommand cmd_restore = {
            .cmd = CMD_ANIM_RESTORE_BG,
            .x = IMG_ANIM_X-30, .y = IMG_ANIM_Y-30,
            .w = IMG_ANIM_W+60, .h = IMG_ANIM_H+60
        };
        xQueueSend(g_graphics_queue, &cmd_restore, 0);

        // --- STEP 2: CALCULATE TRANSFORM MATRIX ---
        
        // A. Get Rotation Matrix for current angle
        Matrix3x3 mat_rot = RotationMatrix(angle);
        
        // B. Combine: Rotate * CenterOffset
        // Order matters! This applies the offset first, then the rotation.
        Matrix3x3 mat_temp = MultiplyMatrix3x3(&mat_rot, &mat_center_offset);
        
        // C. Combine: ScreenPosition * (Rotate * CenterOffset)
        // This moves the rotated shape to the final X,Y on screen.
        Matrix3x3 mat_final = MultiplyMatrix3x3(&mat_screen_position, &mat_temp);
        
        // --- STEP 3: SEND DRAW COMMAND ---
        GraphicsCommand cmd_draw = {
            .cmd = CMD_DRAW_IMAGE_FILE,
            .x = IMG_ANIM_X, .y = IMG_ANIM_Y, // Logical bounds (for updates)
            .w = IMG_ANIM_W, .h = IMG_ANIM_H,
            .hasTransform = true,
            .transform = mat_final
        };
        
        // Set the image path safely
        strncpy(cmd_draw.imagePath, "/spiflash/loading.png", 63);
        
        xQueueSend(g_graphics_queue, &cmd_draw, 0);

        // --- STEP 4: FLUSH ---
        GraphicsCommand cmd_flush = {
            .cmd = CMD_UPDATE_AREA,
            .x = IMG_ANIM_X-30, .y = IMG_ANIM_Y-30,
            .w = IMG_ANIM_W+60, .h = IMG_ANIM_H+60
        };
        xQueueSend(g_graphics_queue, &cmd_flush, 0);

        // Update angle
        angle += speed;
        if (angle > M_PI * 2) angle -= M_PI * 2;

        vTaskDelay(pdMS_TO_TICKS(33)); // ~30 FPS
    }
}

void showTriangleAnimation(void) {
    init_anim_buffer();
    //vTaskDelay(pdMS_TO_TICKS(1000));
    ESP_LOGI(TAG, "CMD_ANIM_SAVE_BG");
    
    
    
    // --- CRITICAL STEP: SYNC ---
    // We must wait for the graphics task to finish drawing the gradient
    // before we snapshot it. 500ms is plenty.
    //vTaskDelay(pdMS_TO_TICKS(1000));
    
    // --- 3. NEW: Start Animation Task (Core 0) ---
    xTaskCreatePinnedToCore(
                            triangle_animation_task,
                            "anim_task",
                            4096,
                            NULL,
                            4,    // Slightly lower priority than graphics/touch
                            &g_triangle_task_handle,
                            0     // Run on Core 0 to leave Core 1 free for rendering
                            );
}

CCPoint* getHandTip(int cx, int cy, float radius, float angleRad) {
    // -PI/2 rotates the "0" angle from 3 o'clock to 12 o'clock
    float adj = angleRad - M_PI_2;
    return ccPoint(cx + cosf(adj)*radius, cy + sinf(adj)*radius);
}

// --- CLOCK APP GLOBALS ---
typedef enum {
    TAB_WORLD_CLOCK = 0,
    TAB_CLOCK_FACE,
    TAB_ALARM,
    TAB_TIMER
} ClockTab;

static ClockTab currentClockTab = TAB_CLOCK_FACE;
static bool clock_app_running = false;
static TaskHandle_t clock_anim_task_handle = NULL;

// UI Pointers
static CCView* clockMainContainer = NULL;
static CCView* tabContentView = NULL;
static CCView* tabBar = NULL;

// Layout Constants
#define CLOCK_CENTER_X 160
#define CLOCK_CENTER_Y 210
#define CLOCK_RADIUS   100

// --- WORLD CLOCK DATA ---
typedef struct {
    char* city;
    int offset; // Hours from UTC (e.g., -5 for EST)
} TimeZone;

static TimeZone zones[] = {
    {"New York", -5}, {"London", 0}, {"Paris", 1},
    {"Tokyo", 9}, {"Sydney", 11}, {"Los Angeles", -8},
    {"Dubai", 4}, {"Hong Kong", 8}
};
static int zonesCount = 8;

// --- WORLD CLOCK PAGINATION ---
static int worldClockPage = 0;
static const int CLOCK_ITEMS_PER_PAGE = 3; // 5 items fits nicely in 420px
#define TAG_CLOCK_PREV 701
#define TAG_CLOCK_NEXT 702

// --- WORLD CLOCK GLOBALS ---
static CCView* mapContainerView = NULL;
// The actual map image
static CCImageView* worldMapImageView = NULL;
// The transparent view where we draw the darkness
static CCView* dayNightOverlayView = NULL;

// Constants for map size/position
#define MAP_X 0
#define MAP_Y 80 // Below the city list
#define MAP_W 320
#define MAP_H 120

// --- ALARM DATA ---
static int alarmHour = 7;
static int alarmMinute = 30;
static bool alarmEnabled = false;

// --- TIMER DATA ---
static int timerSecondsTotal = 0; // Set time (e.g. 60s)
static int timerSecondsLeft = 0;  // Countdown value
static bool timerRunning = false;
static uint64_t lastTimerTick = 0;

void switch_clock_tab(ClockTab tab);

void clock_animation_task(void *pvParam) {
    // 1. Wait for UI to settle
    vTaskDelay(pdMS_TO_TICKS(500));

    // 2. Define Dirty Area (The square containing the clock)
    // Add 10px padding for safety
    int dirty_x = CLOCK_CENTER_X - CLOCK_RADIUS - 10;
    int dirty_y = CLOCK_CENTER_Y - CLOCK_RADIUS - 10;
    int dirty_w = (CLOCK_RADIUS * 2) + 20;
    int dirty_h = (CLOCK_RADIUS * 2) + 20;

    // 3. Save the Static Background (Numbers, Black BG)
    GraphicsCommand cmd_save = {
        .cmd = CMD_ANIM_SAVE_BG,
        .x = dirty_x, .y = dirty_y,
        .w = dirty_w, .h = dirty_h
    };
    xQueueSend(g_graphics_queue, &cmd_save, portMAX_DELAY);
    vTaskDelay(pdMS_TO_TICKS(100)); // Allow save to complete

    while (clock_app_running) {
        
        // --- 1. HANDLE TIMER LOGIC (Always runs) ---
        if (timerRunning && timerSecondsLeft > 0) {
            uint64_t now = esp_timer_get_time() / 1000; // ms
            if (now - lastTimerTick >= 1000) {
                timerSecondsLeft--;
                lastTimerTick = now;
                
                // If we are looking at the timer tab, refresh UI
                if (currentClockTab == TAB_TIMER) {
                    // Quick hack: just redraw the whole tab to update the label
                    // A better way is to update just the label text
                    // But since we are in a task, we need to trigger the main UI thread
                    // For now, assume we can trigger an update:
                    // xQueueSend(ui_event_queue, &UPDATE_MSG, 0);
                }
                
                if (timerSecondsLeft == 0) {
                    timerRunning = false;
                    // TODO: Play Sound!
                    ESP_LOGI(TAG, "TIMER FINISHED!");
                }
            }
        }

        if (currentClockTab == TAB_CLOCK_FACE) {
            
            // A. Get Time
            time_t now;
            struct tm timeinfo;
            time(&now);
            localtime_r(&now, &timeinfo);

            float sec = timeinfo.tm_sec;
            float min = timeinfo.tm_min;
            float hour = timeinfo.tm_hour % 12;

            // B. Restore Background (Erase old hands)
            GraphicsCommand cmd_restore = {
                .cmd = CMD_ANIM_RESTORE_BG,
                .x = dirty_x, .y = dirty_y,
                .w = dirty_w, .h = dirty_h
            };
            xQueueSend(g_graphics_queue, &cmd_restore, 0);

            // C. Calculate Angles
            float secAng  = (sec / 60.0f) * M_PI * 2;
            float minAng  = ((min + sec/60.0f) / 60.0f) * M_PI * 2;
            float hourAng = ((hour + min/60.0f) / 12.0f) * M_PI * 2;

            // D. Draw Hands (Order: Hour -> Minute -> Second)
            
            // Hour Hand (Short, Thick)
            CCPoint* hourTip = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, 50, hourAng);
            GraphicsCommand cmd_h = {
                .cmd = CMD_DRAW_ROUNDED_HAND,
                .x = CLOCK_CENTER_X, .y = CLOCK_CENTER_Y,
                .w = (int)hourTip->x, .h = (int)hourTip->y,
                .radius = 6, // 6px Thick
                .color = {200, 200, 200, 255} // Light Gray
            };
            xQueueSend(g_graphics_queue, &cmd_h, 0);

            // Minute Hand (Long, Medium)
            CCPoint* minTip = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, 80, minAng);
            GraphicsCommand cmd_m = {
                .cmd = CMD_DRAW_ROUNDED_HAND,
                .x = CLOCK_CENTER_X, .y = CLOCK_CENTER_Y,
                .w = (int)minTip->x, .h = (int)minTip->y,
                .radius = 4, // 4px Thick
                .color = {255, 255, 255, 255} // White
            };
            xQueueSend(g_graphics_queue, &cmd_m, 0);

            // Second Hand (Long, Thin, Red)
            CCPoint* secTip = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, 90, secAng);
            GraphicsCommand cmd_s = {
                .cmd = CMD_DRAW_ROUNDED_HAND,
                .x = CLOCK_CENTER_X, .y = CLOCK_CENTER_Y,
                .w = (int)secTip->x, .h = (int)secTip->y,
                .radius = 2, // 2px Thick
                .color = {0, 0, 255, 255} // Red (BGR format: Blue is 0, Red is 255)
            };
            xQueueSend(g_graphics_queue, &cmd_s, 0);

            // Center Dot (Cover the joints)
            // (Optional: send a CMD_DRAW_CIRCLE_FILLED here if you have one exposed)

            // E. Flush Screen
            GraphicsCommand cmd_flush = {
                .cmd = CMD_UPDATE_AREA,
                .x = dirty_x, .y = dirty_y,
                .w = dirty_w, .h = dirty_h
            };
            xQueueSend(g_graphics_queue, &cmd_flush, 0);
        }
        
        if (currentClockTab == TAB_TIMER && timerRunning) {
             // We can use a GraphicsCommand to overwrite the text area
             // Or simpler: switch_clock_tab(TAB_TIMER) call from main loop?
             // Since we are in a task, we shouldn't touch UI objects directly.
             // Ideally, send a message to main loop.
        }
        
        // 30 FPS update is plenty for a smooth second hand
        vTaskDelay(pdMS_TO_TICKS(33));
    }
    
    vTaskDelete(NULL);
}

// Helper to format time strings (HH:MM)
void formatTimeStr(char* buf, int h, int m, bool seconds) {
    if (seconds) sprintf(buf, "%02d:%02d:%02d", h, m, timerSecondsLeft % 60);
    else sprintf(buf, "%02d:%02d", h, m);
}

void setup_clock_app(void) {
    ESP_LOGI(TAG, "Starting Clock App");
    clock_app_running = true;
    currentView = CurrentViewClock;

    // 1. Create Main Container (Black Background)
    clockMainContainer = viewWithFrame(ccRect(0, 0, 320, 480));
    clockMainContainer->backgroundColor = color(0, 0, 0, 1);
    mainWindowView = clockMainContainer;

    // 2. Create Content Area (Top 420px)
    tabContentView = viewWithFrame(ccRect(0, 0, 320, 420));
    viewAddSubview(clockMainContainer, tabContentView);

    // 3. Create Tab Bar (Bottom 60px)
    tabBar = viewWithFrame(ccRect(0, 420, 320, 60));
    tabBar->backgroundColor = color(0.15, 0.15, 0.15, 1);
    viewAddSubview(clockMainContainer, tabBar);

    // Add Tab Buttons
    const char* titles[] = {"World", "Clock", "Alarm", "Timer"};
    int tabW = 320 / 4;
    
    for (int i=0; i<4; i++) {
        CCView* tab = viewWithFrame(ccRect(i*tabW, 0, tabW, 60));
        
        // Tab Label
        CCLabel* lbl = labelWithFrame(ccRect(0, 35, tabW, 20));
        lbl->text = ccs(titles[i]);
        lbl->fontSize = 12;
        lbl->textColor = (i == TAB_CLOCK_FACE) ? color(1, 0.6, 0, 1) : color(0.6, 0.6, 0.6, 1);
        lbl->textAlignment = CCTextAlignmentCenter;
        viewAddSubview(tab, lbl);
        
        // Simple Circle Icon (Placeholder for real icons)
        CCView* icon = viewWithFrame(ccRect((tabW-24)/2, 8, 24, 24));
        icon->backgroundColor = (i == TAB_CLOCK_FACE) ? color(1, 0.6, 0, 1) : color(0.4, 0.4, 0.4, 1);
        icon->layer->cornerRadius = 12;
        viewAddSubview(tab, icon);

        viewAddSubview(tabBar, tab);
    }

    // 4. Start the Animation Task
    //xTaskCreatePinnedToCore(clock_animation_task, "clock_anim", 4096, NULL, 5, &clock_anim_task_handle, 1);

    // 5. Load Initial Tab
    switch_clock_tab(TAB_CLOCK_FACE);
}

void draw_analog_face_ui(void); // Forward Declaration



void draw_analog_face_ui(void) {
    // Draw the static parts of the clock (Numbers, Ticks)
    // We do this via Views so they persist
    
    // Draw Dial Ticks
    for (int i=0; i<12; i++) {
        float angle = (i / 12.0f) * M_PI * 2;
        CCPoint* start = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_RADIUS - 10, angle);
        CCPoint* end   = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_RADIUS, angle);
        
        // Since we don't have a "Line View", we use small thin Views
        // Or simpler: Just rely on the Animation Task to draw the static face too?
        // BETTER: Let's put 4 main labels for 12, 3, 6, 9
    }

    CCLabel* l12 = labelWithFrame(ccRect(CLOCK_CENTER_X-20, CLOCK_CENTER_Y-CLOCK_RADIUS+10, 40, 30));
    l12->text = ccs("12"); l12->fontSize=24; l12->textAlignment=CCTextAlignmentCenter; l12->textColor=color(1,1,1,1);
    viewAddSubview(tabContentView, l12);

    CCLabel* l6 = labelWithFrame(ccRect(CLOCK_CENTER_X-20, CLOCK_CENTER_Y+CLOCK_RADIUS-40, 40, 30));
    l6->text = ccs("6"); l6->fontSize=24; l6->textAlignment=CCTextAlignmentCenter; l6->textColor=color(1,1,1,1);
    viewAddSubview(tabContentView, l6);
    
    CCLabel* l3 = labelWithFrame(ccRect(CLOCK_CENTER_X+CLOCK_RADIUS-40, CLOCK_CENTER_Y-15, 40, 30));
    l3->text = ccs("3"); l3->fontSize=24; l3->textAlignment=CCTextAlignmentCenter; l3->textColor=color(1,1,1,1);
    viewAddSubview(tabContentView, l3);
    
    CCLabel* l9 = labelWithFrame(ccRect(CLOCK_CENTER_X-CLOCK_RADIUS, CLOCK_CENTER_Y-15, 40, 30));
    l9->text = ccs("9"); l9->fontSize=24; l9->textAlignment=CCTextAlignmentCenter; l9->textColor=color(1,1,1,1);
    viewAddSubview(tabContentView, l9);
}

/*void handle_clock_touch(int x, int y, int type) {
    // Only handle "Touch Up" (Release)
    if (type != 1) return;

    // Check Tab Bar Area (Bottom 60px)
    if (y > 420) {
        int tabWidth = 320 / 4;
        int index = x / tabWidth;
        
        if (index >= 0 && index <= 3 && index != currentClockTab) {
            switch_clock_tab((ClockTab)index);
            
            // Re-trigger the BG Save if we went back to Clock Face
            // (Because switching tabs likely destroyed the screen content)
            if (index == TAB_CLOCK_FACE) {
                 // The animation task needs a signal to re-save the background
                 // A simple way is to delete and recreate the task,
                 // or use a flag/semaphore.
                 // For simplicity, recreation is safest:
                 if (clock_anim_task_handle) vTaskDelete(clock_anim_task_handle);
                 xTaskCreatePinnedToCore(clock_animation_task, "clock_anim", 4096, NULL, 5, &clock_anim_task_handle, 1);
            }
        }
    }
    
    CCView* row = find_subview_at_point(mainWindowView, x, y);
    
    // Inside handle_clock_touch...
    if (currentClockTab == TAB_WORLD_CLOCK) {
        if (row->tag == TAG_CLOCK_PREV) {
            if (worldClockPage > 0) {
                worldClockPage--;
                switch_clock_tab(TAB_WORLD_CLOCK); // Refresh
            }
        }
        else if (row->tag == TAG_CLOCK_NEXT) {
            // Safety check to ensure we don't go past end
            if ((worldClockPage + 1) * CLOCK_ITEMS_PER_PAGE < zonesCount) {
                worldClockPage++;
                switch_clock_tab(TAB_WORLD_CLOCK); // Refresh
            }
        }
    }
}*/



void draw_world_clock_ui(void) {
    // 1. Container & Base Image
    // We keep these so the UI system knows the map exists
    mapContainerView = viewWithFrame(ccRect(MAP_X, MAP_Y, MAP_W, MAP_H));
    viewAddSubview(tabContentView, mapContainerView);

    worldMapImageView = imageViewWithFrame(ccRect(0, 0, MAP_W, MAP_H));
    worldMapImageView->image = imageWithFile(ccs("/spiflash/clockmap.png"));
    viewAddSubview(mapContainerView, worldMapImageView);

    // 2. Calculate Day/Night Curve (Run Math ONCE)
    time_t now;
    struct tm timeinfo;
    time(&now);
    gmtime_r(&now, &timeinfo); // UTC Time

    // A. Time Parameter (0.0 - 1.0)
    float currentMinuteUTC = (timeinfo.tm_hour * 60.0f) + timeinfo.tm_min;
    // Offset by 0.5 to center Noon on the map (assuming Greenwich centered map)
    float time01 = fmodf((currentMinuteUTC / 1440.0f) + 0.5f, 1.0f);

    // B. Season Parameter (-1.0 - 1.0)
    float dayOfYear = timeinfo.tm_yday;
    float seasonStrength = -cosf((dayOfYear / 365.0f) * M_PI * 2.0f);

    // 3. Send Draw Command
    // We queue this to happen *after* the current UI update cycle.
    // This ensures the base map image is drawn first, and we draw the darkness on top.
    
    GraphicsCommand cmd_overlay = {
        .cmd = CMD_DRAW_DAY_NIGHT_OVERLAY,
        .x = MAP_X, .y = MAP_Y, .w = MAP_W, .h = MAP_H,
        .radius = time01,         // Time
        .fontSize = seasonStrength  // Season
    };
    
    // Send to queue.
    // If your UI draws very fast, you might need a small vTaskDelay before sending this,
    // or ensure your graphics_task processes this command AFTER the standard view rendering.
    xQueueSend(g_graphics_queue, &cmd_overlay, 0);
    
    // Force a specific area update for the map just in case
    GraphicsCommand cmd_flush = {
        .cmd = CMD_UPDATE_AREA,
        .x = MAP_X, .y = MAP_Y, .w = MAP_W, .h = MAP_H
    };
    xQueueSend(g_graphics_queue, &cmd_flush, 0);
    
    int startY = MAP_Y+MAP_H;
    int rowH = 60;
    int screenW = 320;

    // 2. Calculate Range
    int totalItems = zonesCount;
    int startIndex = worldClockPage * CLOCK_ITEMS_PER_PAGE;
    int endIndex = startIndex + CLOCK_ITEMS_PER_PAGE;
    if (endIndex > totalItems) endIndex = totalItems;

    // 3. Draw Rows
    int currentY = startY;
    
    for (int i = startIndex; i < endIndex; i++) {
        // Calculate Local Time
        int h = (timeinfo.tm_hour + zones[i].offset);
        if (h < 0) h += 24;
        if (h >= 24) h -= 24;
        
        // Row Container
        CCView* row = viewWithFrame(ccRect(0, currentY, screenW, rowH));
        // Zebra striping
        row->backgroundColor = (i % 2 == 0) ? color(0.12, 0.12, 0.12, 1) : color(0.08, 0.08, 0.08, 1);
        
        // City Name
        CCLabel* lblCity = labelWithFrame(ccRect(20, 0, 200, rowH));
        lblCity->text = ccs(zones[i].city);
        lblCity->fontSize = 20;
        lblCity->textColor = color(1, 1, 1, 1);
        lblCity->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(row, lblCity);
        
        // Time String
        char timeBuf[16];
        sprintf(timeBuf, "%02d:%02d", h, timeinfo.tm_min);
        
        CCLabel* lblTime = labelWithFrame(ccRect(220, 0, 80, rowH));
        lblTime->text = ccs(timeBuf);
        lblTime->fontSize = 24;
        lblTime->textColor = color(0.8, 0.8, 0.8, 1);
        lblTime->textAlignment = CCTextAlignmentRight;
        lblTime->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(row, lblTime);
        
        viewAddSubview(tabContentView, row);
        currentY += rowH;
    }

    // 4. Draw Pagination Buttons (Bottom of list)
    int btnY = currentY + 10;
    int btnW = 100;
    int btnH = 40;

    // PREV BUTTON
    if (worldClockPage > 0) {
        CCView* btnPrev = viewWithFrame(ccRect(20, btnY, btnW, btnH));
        btnPrev->backgroundColor = color(0.3, 0.3, 0.4, 1.0);
        btnPrev->layer->cornerRadius = 5;
        btnPrev->tag = TAG_CLOCK_PREV;
        
        CCLabel* lbl = labelWithFrame(ccRect(0,0,btnW,btnH));
        lbl->text = ccs("<< Back");
        lbl->textColor = color(1,1,1,1);
        lbl->textAlignment = CCTextAlignmentCenter;
        lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(btnPrev, lbl);
        
        viewAddSubview(tabContentView, btnPrev);
    }

    // NEXT BUTTON
    if (endIndex < totalItems) {
        CCView* btnNext = viewWithFrame(ccRect(screenW - 20 - btnW, btnY, btnW, btnH));
        btnNext->backgroundColor = color(0.2, 0.5, 0.2, 1.0);
        btnNext->layer->cornerRadius = 5;
        btnNext->tag = TAG_CLOCK_NEXT;

        CCLabel* lbl = labelWithFrame(ccRect(0,0,btnW,btnH));
        lbl->text = ccs("Next >>");
        lbl->textColor = color(1,1,1,1);
        lbl->textAlignment = CCTextAlignmentCenter;
        lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        viewAddSubview(btnNext, lbl);
        
        viewAddSubview(tabContentView, btnNext);
    }
}

// Tags for touch handling
#define TAG_ALARM_H_UP   501
#define TAG_ALARM_H_DOWN 502
#define TAG_ALARM_M_UP   503
#define TAG_ALARM_M_DOWN 504
#define TAG_ALARM_TOGGLE 505

void draw_alarm_ui(void) {
    
    CCLabel* lblTime = labelWithFrame(ccRect(0, 80, 320, 80));
    lblTime->text = ccs("test");
    lblTime->fontSize = 60;
    lblTime->textAlignment = CCTextAlignmentCenter;
    lblTime->textColor = alarmEnabled ? color(1,1,1,1) : color(0.5,0.5,0.5,1);
    viewAddSubview(tabContentView, lblTime);
    
    // 1. Alarm Time Display (Big Center Text)
    /*char buf[16];
    formatTimeStr(buf, alarmHour, alarmMinute, false);
    
    CCLabel* lblTime = labelWithFrame(ccRect(0, 80, 320, 80));
    lblTime->text = ccs(buf);
    lblTime->fontSize = 60;
    lblTime->textAlignment = CCTextAlignmentCenter;
    lblTime->textColor = alarmEnabled ? color(1,1,1,1) : color(0.5,0.5,0.5,1);
    viewAddSubview(tabContentView, lblTime);

    // 2. Control Buttons (Row of 4 buttons)
    int btnY = 200;
    int btnW = 60;
    int gap = 15;
    int startX = (320 - (4*btnW + 3*gap)) / 2;

    // Helper to make button
    void makeBtn(int idx, char* txt, int tag) {
        CCView* btn = viewWithFrame(ccRect(startX + idx*(btnW+gap), btnY, btnW, 50));
        btn->backgroundColor = color(0.2, 0.2, 0.2, 1);
        btn->layer->cornerRadius = 10;
        btn->tag = tag;
        
        CCLabel* l = labelWithFrame(ccRect(0,0,btnW,50));
        l->text = ccs(txt); l->textAlignment = CCTextAlignmentCenter; l->textVerticalAlignment = CCTextVerticalAlignmentCenter;
        l->textColor = color(1,1,1,1);
        viewAddSubview(btn, l);
        viewAddSubview(tabContentView, btn);
    }

    makeBtn(0, "H-", TAG_ALARM_H_DOWN);
    makeBtn(1, "H+", TAG_ALARM_H_UP);
    makeBtn(2, "M-", TAG_ALARM_M_DOWN);
    makeBtn(3, "M+", TAG_ALARM_M_UP);

    // 3. Enable Toggle
    CCView* btnToggle = viewWithFrame(ccRect(100, 300, 120, 50));
    btnToggle->backgroundColor = alarmEnabled ? color(0, 0.6, 0, 1) : color(0.6, 0.2, 0.2, 1);
    btnToggle->layer->cornerRadius = 25;
    btnToggle->tag = TAG_ALARM_TOGGLE;
    
    CCLabel* lTog = labelWithFrame(ccRect(0,0,120,50));
    lTog->text = ccs(alarmEnabled ? "ON" : "OFF");
    lTog->textAlignment = CCTextAlignmentCenter; lTog->textVerticalAlignment = CCTextVerticalAlignmentCenter;
    lTog->textColor = color(1,1,1,1);
    viewAddSubview(btnToggle, lTog);
    viewAddSubview(tabContentView, btnToggle);*/
}

#define TAG_TIMER_START 601
#define TAG_TIMER_RESET 602
#define TAG_TIMER_ADD_1MIN 603

void draw_timer_ui(void) {
    // 1. Countdown Display
    int m = timerSecondsLeft / 60;
    int s = timerSecondsLeft % 60;
    char buf[16];
    sprintf(buf, "%02d:%02d", m, s);

    CCLabel* lblTimer = labelWithFrame(ccRect(0, 80, 320, 80));
    lblTimer->text = ccs(buf);
    lblTimer->fontSize = 60;
    lblTimer->textAlignment = CCTextAlignmentCenter;
    lblTimer->textColor = timerRunning ? color(1, 0.8, 0, 1) : color(1,1,1,1);
    viewAddSubview(tabContentView, lblTimer);

    // 2. Add Time Button (+1 Min)
    CCView* btnAdd = viewWithFrame(ccRect(110, 180, 100, 40));
    btnAdd->backgroundColor = color(0.2, 0.2, 0.2, 1);
    btnAdd->tag = TAG_TIMER_ADD_1MIN;
    CCLabel* lAdd = labelWithFrame(ccRect(0,0,100,40));
    lAdd->text = ccs("+1 Min"); lAdd->textAlignment=CCTextAlignmentCenter; lAdd->textVerticalAlignment=CCTextVerticalAlignmentCenter;
    lAdd->textColor = color(1,1,1,1);
    viewAddSubview(btnAdd, lAdd);
    viewAddSubview(tabContentView, btnAdd);

    // 3. Start/Stop Button
    CCView* btnStart = viewWithFrame(ccRect(40, 260, 100, 60));
    btnStart->backgroundColor = timerRunning ? color(0.6, 0.2, 0, 1) : color(0, 0.6, 0, 1);
    btnStart->tag = TAG_TIMER_START;
    CCLabel* lStart = labelWithFrame(ccRect(0,0,100,60));
    lStart->text = ccs(timerRunning ? "Stop" : "Start"); lStart->textAlignment=CCTextAlignmentCenter; lStart->textVerticalAlignment=CCTextVerticalAlignmentCenter;
    lStart->textColor = color(1,1,1,1);
    viewAddSubview(btnStart, lStart);
    viewAddSubview(tabContentView, btnStart);

    // 4. Reset Button
    CCView* btnReset = viewWithFrame(ccRect(180, 260, 100, 60));
    btnReset->backgroundColor = color(0.3, 0.3, 0.3, 1);
    btnReset->tag = TAG_TIMER_RESET;
    CCLabel* lReset = labelWithFrame(ccRect(0,0,100,60));
    lReset->text = ccs("Reset"); lReset->textAlignment=CCTextAlignmentCenter; lReset->textVerticalAlignment=CCTextVerticalAlignmentCenter;
    lReset->textColor = color(1,1,1,1);
    viewAddSubview(btnReset, lReset);
    viewAddSubview(tabContentView, btnReset);
}

void handle_clock_touch(int x, int y, int type) {
    if (type != 1) return; // Only on Touch Up
    
    printf("handle_clock_touch %d %d", x, y);

    // Tab Bar Logic
    if (y > 420) {
        int tabWidth = 320 / 4;
        int index = x / tabWidth;
        if (index >= 0 && index <= 3 && index != currentClockTab) {
            printf("switch_clock_tab %d", index);
            switch_clock_tab((ClockTab)index);
        }
        return;
    }

    // Tab Specific Logic
    // Helper to find clicked view by tag
    CCView* clicked = find_subview_at_point(tabContentView, x, y);
    if (!clicked) return;

    if (currentClockTab == TAB_ALARM) {
        if (clicked->tag == TAG_ALARM_H_UP) alarmHour = (alarmHour + 1) % 24;
        else if (clicked->tag == TAG_ALARM_H_DOWN) alarmHour = (alarmHour - 1 + 24) % 24;
        else if (clicked->tag == TAG_ALARM_M_UP) alarmMinute = (alarmMinute + 5) % 60;
        else if (clicked->tag == TAG_ALARM_M_DOWN) alarmMinute = (alarmMinute - 5 + 60) % 60;
        else if (clicked->tag == TAG_ALARM_TOGGLE) alarmEnabled = !alarmEnabled;
        
        switch_clock_tab(TAB_ALARM); // Redraw to update text
    }
    else if (currentClockTab == TAB_TIMER) {
        if (clicked->tag == TAG_TIMER_ADD_1MIN) {
            timerSecondsLeft += 60;
            switch_clock_tab(TAB_TIMER);
        }
        else if (clicked->tag == TAG_TIMER_RESET) {
            timerRunning = false;
            timerSecondsLeft = 0;
            switch_clock_tab(TAB_TIMER);
        }
        else if (clicked->tag == TAG_TIMER_START) {
            timerRunning = !timerRunning;
            if (timerRunning) lastTimerTick = esp_timer_get_time() / 1000;
            switch_clock_tab(TAB_TIMER);
        }
    }
    
    if (currentClockTab == TAB_WORLD_CLOCK) {
        if (clicked->tag == TAG_CLOCK_PREV) {
            if (worldClockPage > 0) {
                worldClockPage--;
                switch_clock_tab(TAB_WORLD_CLOCK); // Refresh
            }
        }
        else if (clicked->tag == TAG_CLOCK_NEXT) {
            // Safety check to ensure we don't go past end
            if ((worldClockPage + 1) * CLOCK_ITEMS_PER_PAGE < zonesCount) {
                worldClockPage++;
                switch_clock_tab(TAB_WORLD_CLOCK); // Refresh
            }
        }
    }
}

void switch_clock_tab(ClockTab tab) {
    currentClockTab = tab;
    
    // Clear previous content
    viewRemoveFromSuperview(tabContentView);
    freeViewHierarchy(tabContentView);
    tabContentView = viewWithFrame(ccRect(0, 0, 320, 420));
    viewAddSubview(clockMainContainer, tabContentView);

    // Draw new content
    if (tab == TAB_WORLD_CLOCK) draw_world_clock_ui();
    else if (tab == TAB_CLOCK_FACE) draw_analog_face_ui();
    else if (tab == TAB_ALARM) draw_alarm_ui();
    else if (tab == TAB_TIMER) draw_timer_ui();
    
    // Trigger update
    update_full_ui();
}



// Context struct to pass data into the decoder callback
typedef struct {
    ImageTexture *texture; // Your texture struct
    int outputX;           // Where to draw it on the texture
    int outputY;
} JpegDev;

static UINT tjd_input(JDEC* jd, BYTE* buff, UINT nd) {
    FILE *f = (FILE *)jd->device;
    if (buff) {
        size_t read = fread(buff, 1, nd, f);
        // If we hit EOF, log it but return what we got
        if (read < nd && feof(f)) {
            // ESP_LOGW("MJPEG", "Input: EOF hit unexpectedly"); // Comment out if spammy
        }
        return (UINT)read;
    } else {
        if (fseek(f, nd, SEEK_CUR) == 0) return nd;
        return 0;
    }
}

// 2. OUTPUT FUNC: Receives decoded RGB888 blocks and converts to RGBA
static UINT tjd_output(JDEC* jd, void* bitmap, JRECT* rect) {
    JpegDev *dev = (JpegDev *)jd->device;
    ImageTexture *tex = dev->texture;

    // 'bitmap' is an array of RGB bytes (R, G, B, R, G, B...)
    uint8_t *rgb = (uint8_t *)bitmap;
    
    // Loop through the rectangular block provided by the decoder
    for (int y = rect->top; y <= rect->bottom; y++) {
        for (int x = rect->left; x <= rect->right; x++) {
            
            // Safety check: Don't write outside your texture memory
            if (x < tex->width && y < tex->height) {
                
                // Calculate target index in your RGBA array
                int index = y * tex->width + x;
                
                // Convert RGB (3 bytes) to RGBA (4 bytes)
                tex->data[index].r = rgb[0];
                tex->data[index].g = rgb[1];
                tex->data[index].b = rgb[2];
                tex->data[index].a = 255; // Fully opaque
            }
            // Move source pointer forward 3 bytes (R,G,B)
            rgb += 3;
        }
    }
    return 1; // Continue decoding
}

#define READ_CHUNK_SIZE 4096

// Update to match your screen resolution
#define VID_W 480
#define VID_H 320

void video_player_task(void *pvParam) {
    // 1. Correct Path
    const char *filename = "/sdcard/output.mjpeg";
    FILE *f = fopen(filename, "rb");
    
    if (!f) {
        ESP_LOGE(TAG, "Failed to open %s", filename);
        vTaskDelete(NULL);
    }

    // 2. Get File Size for EOF checks
    fseek(f, 0, SEEK_END);
    long fileSize = ftell(f);
    fseek(f, 0, SEEK_SET);
    ESP_LOGI(TAG, "File Size: %ld", fileSize);

    // 3. Allocate 480x320 Buffers
    ImageTexture *frameBuffer = heap_caps_malloc(sizeof(ImageTexture), MALLOC_CAP_SPIRAM);
    frameBuffer->width = VID_W;
    frameBuffer->height = VID_H;
    
    // 480x320x4 bytes = ~600KB. Ensure your ESP32 has PSRAM enabled.
    frameBuffer->data = heap_caps_malloc(VID_W * VID_H * sizeof(ColorRGBA), MALLOC_CAP_SPIRAM);
    
    if (!frameBuffer->data) {
        ESP_LOGE(TAG, "Failed to allocate video memory! Check PSRAM.");
        fclose(f);
        vTaskDelete(NULL);
    }

    char *work = heap_caps_malloc(4096, MALLOC_CAP_INTERNAL);
    uint8_t *chunkBuf = heap_caps_malloc(READ_CHUNK_SIZE, MALLOC_CAP_INTERNAL);

    JDEC jdec;
    JpegDev dev;
    dev.texture = frameBuffer;

    ESP_LOGI(TAG, "Starting 480x320 Video...");

    while (1) {
        bool foundFrame = false;
        long frameStartOffset = 0;
        
        // --- SCANNING ---
        while (!foundFrame) {
            long currentPos = ftell(f);
            size_t bytesRead = fread(chunkBuf, 1, READ_CHUNK_SIZE, f);
            
            // EOF Check
            if (bytesRead < 2) {
                clearerr(f); fseek(f, 0, SEEK_SET); continue;
            }

            for (int i = 0; i < bytesRead - 1; i++) {
                if (chunkBuf[i] == 0xFF && chunkBuf[i+1] == 0xD8) {
                    foundFrame = true;
                    frameStartOffset = currentPos + i;
                    break;
                }
            }
            if (!foundFrame) vTaskDelay(1);
        }

        // --- DECODE ---
        
        // Safety: If we found a frame very close to the end (e.g. last 1KB), it's likely truncated.
        if (fileSize - frameStartOffset < 1024) {
            ESP_LOGW(TAG, "Skipping truncated frame at end of file.");
            clearerr(f);
            fseek(f, 0, SEEK_SET);
            continue;
        }

        clearerr(f);
        fseek(f, frameStartOffset, SEEK_SET);

        JRESULT res = jd_prepare(&jdec, tjd_input, work, 4096, f);
        
        if (res == JDR_OK) {
            jdec.device = &dev;
            res = jd_decomp(&jdec, tjd_output, 0);
            
            if (res == JDR_OK) {
                // SUCCESS
                //ESP_LOGI("GFX", "Sent Pixel Buffer Command");
                GraphicsCommand cmd = {
                    .cmd = CMD_DRAW_PIXEL_BUFFER,
                    .x = 0, .y = 0,
                    .w = VID_W, .h = VID_H, // 480x320
                    .pixelBuffer = frameBuffer->data
                };
                xQueueSend(g_graphics_queue, &cmd, portMAX_DELAY);
                
                GraphicsCommand cmd_flush = {
                    .cmd = CMD_UPDATE_AREA,
                    .x = 0, .y = 0, .w = VID_W, .h = VID_H
                };
                xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
            }
            else {
                //ESP_LOGI("GFX", "Decode Failed");
                // Decode Failed (Likely EOF hit unexpectedly)
                // If we fail, assume this frame is bad/truncated.
                // If we are near the end of the file (>90%), just restart.
                if (frameStartOffset > fileSize * 0.9) {
                     ESP_LOGW(TAG, "EOF hit near end. Restarting video.");
                     fseek(f, 0, SEEK_SET);
                     continue;
                }
            }
        }
        else {
             // Prepare failed.
             if (frameStartOffset > fileSize * 0.9) {
                 fseek(f, 0, SEEK_SET);
                 continue;
            }
        }

        // Advance Ptr
        clearerr(f);
        fseek(f, frameStartOffset + 2, SEEK_SET);

        // Frame Pacing (480x320 takes longer to transfer, so 30ms might be optimistic)
        // If it feels slow, decrease this to 10 or 0.
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}



typedef struct {
    FILE* f;
    CCImage* img;
} ThumbContext;

static UINT tjd_input_thumb(JDEC* jd, BYTE* buff, UINT nd) {
    ThumbContext *ctx = (ThumbContext *)jd->device;
    if (buff) return (UINT)fread(buff, 1, nd, ctx->f);
    return fseek(ctx->f, nd, SEEK_CUR) == 0 ? nd : 0;
}

static UINT tjd_output_thumb(JDEC* jd, void* bitmap, JRECT* rect) {
    ThumbContext *ctx = (ThumbContext *)jd->device;
    CCImage *img = ctx->img; // Get the image from context
    uint8_t *rgb = (uint8_t *)bitmap;

    for (int y = rect->top; y <= rect->bottom; y++) {
        for (int x = rect->left; x <= rect->right; x++) {
            if (x < img->size->width && y < img->size->height) {
                int index = y * img->size->width + x;
                ColorRGBA *pixel = &((ColorRGBA*)img->imageData)[index];
                pixel->r = rgb[0];
                pixel->g = rgb[1];
                pixel->b = rgb[2];
                pixel->a = 255;
            }
            rgb += 3;
        }
    }
    return 1;
}

static UINT tjd_input_one_frame(JDEC* jd, BYTE* buff, UINT nd) {
    FILE *f = (FILE *)jd->device;
    if (buff) return (UINT)fread(buff, 1, nd, f);
    return fseek(f, nd, SEEK_CUR) == 0 ? nd : 0;
}

// Output: Writes RGBA to CCImage buffer
static UINT tjd_output_one_frame(JDEC* jd, void* bitmap, JRECT* rect) {
    // We pass the CCImage* as the device context
    CCImage *img = (CCImage *)jd->device;
    uint8_t *rgb = (uint8_t *)bitmap;

    for (int y = rect->top; y <= rect->bottom; y++) {
        for (int x = rect->left; x <= rect->right; x++) {
            if (x < img->size->width && y < img->size->height) {
                int index = y * img->size->width + x;
                ColorRGBA *pixel = &((ColorRGBA*)img->imageData)[index];
                
                pixel->r = rgb[0];
                pixel->g = rgb[1];
                pixel->b = rgb[2];
                pixel->a = 255; // Opaque
            }
            rgb += 3;
        }
    }
    return 1;
}




// Corrected Main Function Call
CCImage* imageWithVideoFrame(const char *path, int width, int height) {
    FILE *f = fopen(path, "rb");
    if (!f) {
        ESP_LOGE("IMG", "Could not open video: %s", path);
        return NULL;
    }
    
    ESP_LOGI("IMG", "Could open video: %s", path);

    // 1. Allocate CCImage in PSRAM
    CCImage *img = image();
    img->size->width = width;
    img->size->height = height;
    img->imageData = heap_caps_malloc(width * height * sizeof(ColorRGBA), MALLOC_CAP_SPIRAM);
    
    // 2. Scan for First Frame (FF D8)
    // We scan the first 4KB. If the frame isn't there, it's not a valid MJPEG.
    uint8_t *chunk = cc_safe_alloc(1, 4096);
    size_t read = fread(chunk, 1, 4096, f);
    long headerOffset = -1;

    for (int i = 0; i < read - 1; i++) {
        if (chunk[i] == 0xFF && chunk[i+1] == 0xD8) {
            headerOffset = i;
            break;
        }
    }
    free(chunk);

    if (headerOffset == -1) {
        ESP_LOGE("IMG", "No frame found in video header");
        fclose(f);
        // Return a blank/error image or NULL
        return img;
    }

    // 3. Decode
    fseek(f, headerOffset, SEEK_SET); // Jump to the Start of Image
    
    char *work = cc_safe_alloc(1, 4096);
    JDEC jdec;


    // Context holds both the File and the Target Image
    ThumbContext ctx;
    ctx.f = f;
    ctx.img = img;

    // Use specific helpers for this context
    if (jd_prepare(&jdec, tjd_input_thumb, work, 4096, &ctx) == JDR_OK) {
        jd_decomp(&jdec, tjd_output_thumb, 0);
    }
    
    // --- DEBUG: LOG PIXEL DATA (FIXED) ---
    ESP_LOGI("IMG", "--- PIXEL DATA DUMP ---");

    // 1. Cast the raw bytes to a Pixel Array
    ColorRGBA *pixels = (ColorRGBA *)img->imageData;
    
    // Print First 5 Pixels (Top Left)
    for (int i = 0; i < 5; i++) {
        ColorRGBA p = pixels[i]; // Now this works
        ESP_LOGI("IMG", "Pixel[%d]: R=%d G=%d B=%d A=%d", i, p.r, p.g, p.b, p.a);
    }

    // Print Center Pixel
    int centerIdx = (height/2) * width + (width/2);
    ColorRGBA p = pixels[centerIdx];
    ESP_LOGI("IMG", "Pixel[Center]: R=%d G=%d B=%d A=%d", p.r, p.g, p.b, p.a);

    free(work);
    fclose(f);
    return img;
}

typedef struct {
    FILE *f;
    CCImageView *view;
    uint8_t *chunkBuf; // Buffer for scanning
    char *workBuf;     // Buffer for decoder
    CCImage *imgBuffer; // Reusable image memory
} VideoState;

void next_frame_task(void *pvParam) {
    int64_t t3 = esp_timer_get_time();
    
    CCImageView *targetView = (CCImageView *)pvParam;
    const char *path = "/sdcard/output.mjpeg";

    // 1. Setup State
    FILE *f = fopen(path, "rb");
    if (!f) {
        ESP_LOGE("VID", "Cannot open %s", path);
        vTaskDelete(NULL);
    }

    // Allocate reusable buffers (Don't malloc inside the loop!)
    uint8_t *chunkBuf = heap_caps_malloc(4096, MALLOC_CAP_INTERNAL);
    char *workBuf = heap_caps_malloc(4096, MALLOC_CAP_INTERNAL);
    
    // Create the image buffer ONCE. We will just overwrite its pixels.
    CCImage *img = image();
    img->size->width = 320;
    img->size->height = 480;
    img->imageData = heap_caps_malloc(480 * 320 * sizeof(ColorRGBA), MALLOC_CAP_SPIRAM);
    
    // Assign this image to the view immediately
    targetView->image = img;

    JDEC jdec;
    ThumbContext ctx; // Reuse our context helper from before
    ctx.f = f;
    ctx.img = img;

    ESP_LOGI("VID", "Starting Animation Loop...");

    while (1) {
        bool foundFrame = false;
        long frameStart = 0;

        // --- 2. Scan for Next Frame ---
        while (!foundFrame) {
            long pos = ftell(f);
            size_t read = fread(chunkBuf, 1, 4096, f);
            
            // Loop video at EOF
            if (read < 2) {
                clearerr(f); fseek(f, 0, SEEK_SET); continue;
            }

            for (int i = 0; i < read - 1; i++) {
                if (chunkBuf[i] == 0xFF && chunkBuf[i+1] == 0xD8) {
                    foundFrame = true;
                    frameStart = pos + i;
                    break;
                }
            }
        }

        // --- 3. Decode Frame ---
        clearerr(f);
        fseek(f, frameStart, SEEK_SET);

        if (jd_prepare(&jdec, tjd_input_thumb, workBuf, 4096, &ctx) == JDR_OK) {
            if (jd_decomp(&jdec, tjd_output_thumb, 0) == JDR_OK) {
                
                // --- 4. Trigger Redraw ---
                // We don't send pixel data here. We tell the Graphics Task
                // "The view has changed, please redraw the tree."
                
                // Note: The graphics task reads targetView->image->imageData,
                // which we just overwrote with new pixels.
                
                GraphicsCommand cmd = {
                    .cmd = CMD_DRAW_PIXEL_BUFFER, // Or your custom view redraw command
                    .x = 0, .y = 0, .w = 320, .h = 480,
                    .pixelBuffer = img->imageData
                };
                xQueueSend(g_graphics_queue, &cmd, 0);
                
                //update_view_only(targetView);
                
                GraphicsCommand cmd_flush = {
                    .cmd = CMD_UPDATE_AREA,
                    .x = 0, .y = 0, .w = 320, .h = 480
                };
                xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
                
                
            }
        }

        // --- 5. Advance & Wait ---
        clearerr(f);
        fseek(f, frameStart + 2, SEEK_SET);
        
        // 0.1s Delay (100ms)
        //vTaskDelay(pdMS_TO_TICKS(25));
        
        int64_t t4 = esp_timer_get_time();

        ESP_LOGI(TAG, "nxt frame Time:  %lld us", (t4 - t3));
    }
}

int currentFrame = 0;

void next_frame_task1(void *pvParam) {
    
    while (1) {
        
        CCImageView *targetView = (CCImageView *)pvParam;
        CCString* path = stringWithFormat("/sdcard/frames/img_%d.png", currentFrame);
        
        targetView->image = imageWithFile(path);
        
        /*GraphicsCommand cmd = {
            .cmd = CMD_DRAW_PIXEL_BUFFER, // Or your custom view redraw command
            .x = 0, .y = 0, .w = 320, .h = 480,
            .pixelBuffer = targetView->image->imageData
        };
        xQueueSend(g_graphics_queue, &cmd, 0);*/
        
        //update_view_only(targetView);
        
        GraphicsCommand cmd_flush = {
            .cmd = CMD_UPDATE_AREA,
            .x = 0, .y = 0, .w = 320, .h = 480
        };
        xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
        
        currentFrame++;
        
        // 0.1s Delay (100ms)
        vTaskDelay(pdMS_TO_TICKS(50));
        
    }
    
}

CCImageView* videoView = NULL;

void setup_video_preview_ui(void) {
    // 1. Create the Image View
    videoView = imageViewWithFrame(ccRect(0, 0, 320, 480));
    
    // 2. Load JUST the first frame (480x320 resolution)
    // Note: This might take ~200-500ms, blocking the UI briefly.
    videoView->image = imageWithVideoFrame("/sdcard/output.mjpeg", 320, 480);
    
    ESP_LOGI("IMG", "imageWithVideoFrame");
    
    // 3. Add to screen
    viewAddSubview(mainWindowView, videoView);
    
    
}

void setup_video_preview_ui1(void) {
    // 1. Create the Image View
    videoView = imageViewWithFrame(ccRect(0, 0, 320, 480));
    
    // 2. Load JUST the first frame (480x320 resolution)
    // Note: This might take ~200-500ms, blocking the UI briefly.
    videoView->image = imageWithFile(ccs("/sdcard/frames/img_1.png"));
    
    ESP_LOGI("IMG", "imageWithVideoFrame");
    
    // 3. Add to screen
    viewAddSubview(mainWindowView, videoView);
    
    
}


// CONSTANTS FROM FFMPEG SETUP
#define VIDEO_WIDTH  320  // Transposed width
#define VIDEO_HEIGHT 480  // Transposed height

// ... (Keep your Framebuffer struct and yuv2rgb helpers here) ...

typedef struct {
    Framebuffer *fb;
    const char *filepath;
} VideoTaskParams;


void vVideoDecodeTask(void *pvParameters) {
    VideoTaskParams *params = (VideoTaskParams *)pvParameters;
    Framebuffer *fb = params->fb;
    const char *path = params->filepath;

    ESP_LOGI(TAG, "Opening video file: %s", path);
    FILE *f = fopen(path, "rb");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file");
        vTaskDelete(NULL);
        return;
    }

    // 1. INITIAL SETUP
    esp_h264_dec_cfg_sw_t cfg = { .pic_type = ESP_H264_RAW_FMT_I420 };
    esp_h264_dec_handle_t dec_handle = NULL;
    esp_h264_dec_sw_new(&cfg, &dec_handle);
    esp_h264_dec_open(dec_handle);

    // 2. ALLOCATE BUFFER
    const int BUFFER_CAPACITY = 1024 * 512;
        
        // Verify we have enough PSRAM
        uint8_t *file_buffer = (uint8_t *)heap_caps_malloc(BUFFER_CAPACITY, MALLOC_CAP_SPIRAM);
        
        if (file_buffer == NULL) {
            ESP_LOGE(TAG, "Failed to allocate 512KB buffer! Trying 256KB...");
            // Fallback if your specific board is low on RAM
            file_buffer = (uint8_t *)heap_caps_malloc(1024 * 256, MALLOC_CAP_SPIRAM);
        }
    
    size_t current_data_len = 0;
    esp_h264_dec_in_frame_t in_frame = { 0 };
    esp_h264_dec_out_frame_t out_frame = { 0 };

    ESP_LOGI(TAG, "Starting decode loop...");

    while (1) {
        // A. READ DATA
        size_t space_available = BUFFER_CAPACITY - current_data_len;
        if (space_available < 1024) {
            current_data_len = 0;
            space_available = BUFFER_CAPACITY;
        }

        size_t bytes_read = fread(file_buffer + current_data_len, 1, space_available, f);
        if (bytes_read == 0) {
            fseek(f, 0, SEEK_SET);
            current_data_len = 0;
            vTaskDelay(pdMS_TO_TICKS(10));
            continue;
        }
        current_data_len += bytes_read;

        // B. DECODE LOOP
        in_frame.raw_data.buffer = file_buffer;
        in_frame.raw_data.len = current_data_len;
        in_frame.dts = 0;
        in_frame.pts = 0;

        while (in_frame.raw_data.len > 0) {
            
            esp_h264_err_t ret = esp_h264_dec_process(dec_handle, &in_frame, &out_frame);

            // --- CASE 1: SUCCESS ---
            if (ret == ESP_H264_ERR_OK) {
                if (out_frame.outbuf != NULL) {
                     // Draw Frame (Optimized YUV->RGB)
                    int width = VIDEO_WIDTH;
                    int height = VIDEO_HEIGHT;
                    uint8_t *y_plane = out_frame.outbuf;
                    uint8_t *u_plane = y_plane + (width * height);
                    uint8_t *v_plane = u_plane + (width * height / 4);
                    int u_stride = width / 2;

                    for (int y = 0; y < height; y += 2) {
                        for (int x = 0; x < width; x += 2) {
                            if (y >= fb->displayHeight || x >= fb->displayWidth) continue;
                            
                            int uv_off = (y>>1)*u_stride + (x>>1);
                            int d = u_plane[uv_off] - 128;
                            int e = v_plane[uv_off] - 128;
                            int rc = (409*e+128);
                            int gc = (-100*d-208*e+128);
                            int bc = (516*d+128);

                            int y_idx[4] = { y*width+x, y*width+x+1, (y+1)*width+x, (y+1)*width+x+1 };
                            int fb_idx[4] = {
                                (y*fb->displayWidth+x)*3, (y*fb->displayWidth+x+1)*3,
                                ((y+1)*fb->displayWidth+x)*3, ((y+1)*fb->displayWidth+x+1)*3
                            };

                            for(int k=0; k<4; k++) {
                                int py = y+(k/2), px = x+(k%2);
                                if(py>=fb->displayHeight || px>=fb->displayWidth) continue;
                                int y_term = 298*(y_plane[y_idx[k]]-16);
                                uint8_t *p = (uint8_t*)fb->pixelData + fb_idx[k];
                                int r = (y_term+rc)>>8; p[0] = (r>255)?255:(r<0?0:r);
                                int g = (y_term+gc)>>8; p[1] = (g>255)?255:(g<0?0:g);
                                int b = (y_term+bc)>>8; p[2] = (b>255)?255:(b<0?0:b);
                            }
                        }
                    }
                }
                
                if (in_frame.consume > 0) {
                    in_frame.raw_data.buffer += in_frame.consume;
                    in_frame.raw_data.len    -= in_frame.consume;
                } else {
                    break; // Need more data
                }
            }
            
            // --- CASE 2: FATAL ERROR (RESTART VIDEO) ---
            else {
                ESP_LOGE(TAG, "Decoder Error %d. RESTARTING VIDEO to restore sync...", ret);
                
                // 1. Destroy Corrupted Decoder
                esp_h264_dec_close(dec_handle);
                esp_h264_dec_del(dec_handle);
                
                // 2. REWIND FILE (The Fix)
                // We must go back to start to read SPS/PPS headers again
                fseek(f, 0, SEEK_SET);
                
                // 3. Clear Buffer
                current_data_len = 0;
                in_frame.raw_data.len = 0; // Forces break from inner loop

                // 4. Create Fresh Decoder
                dec_handle = NULL;
                esp_h264_dec_sw_new(&cfg, &dec_handle);
                esp_h264_dec_open(dec_handle);
                
                ESP_LOGW(TAG, "Video Restarted.");
                break; // Break inner loop to trigger fread from byte 0
            }
        }

        // C. PRESERVE LEFTOVERS
        size_t leftovers = in_frame.raw_data.len;
        if (leftovers > 0) {
            memmove(file_buffer, in_frame.raw_data.buffer, leftovers);
        }
        current_data_len = leftovers;

        vTaskDelay(pdMS_TO_TICKS(10));
    }

    esp_h264_dec_close(dec_handle);
    esp_h264_dec_del(dec_handle);
    free(file_buffer);
    fclose(f);
    vTaskDelete(NULL);
}

void start_video_player(Framebuffer *fb) {
    // You must allocate this structure so it persists after this function returns
    VideoTaskParams *params = cc_safe_alloc(1, sizeof(VideoTaskParams));
    params->fb = fb;
    // Assuming you have mounted SD card to /sdcard
    params->filepath = "/sdcard/output.h264";

    xTaskCreatePinnedToCore(vVideoDecodeTask, "VidTask", 16384, (void*)params, 2, NULL, 1);
}

// --- HELPER: YUV420 to RGB888 ---
// Returns a new buffer that you must free() later
uint8_t* yuv420_to_rgb888(uint8_t *y_plane, uint8_t *u_plane, uint8_t *v_plane, int width, int height) {
    // Allocate RGB Buffer (Width * Height * 3 bytes)
    uint8_t *rgb_buffer = (uint8_t *)heap_caps_malloc(width * height * 3, MALLOC_CAP_SPIRAM);
    if (!rgb_buffer) return NULL;

    int u_stride = width / 2;

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // Get Y (Luma)
            int Y_val = y_plane[y * width + x];
            
            // Get UV (Chroma) - Shared by 2x2 pixels
            int uv_idx = (y >> 1) * u_stride + (x >> 1);
            int U_val = u_plane[uv_idx];
            int V_val = v_plane[uv_idx];

            // Math
            int c = Y_val - 16;
            int d = U_val - 128;
            int e = V_val - 128;

            int r = (298 * c + 409 * e + 128) >> 8;
            int g = (298 * c - 100 * d - 208 * e + 128) >> 8;
            int b = (298 * c + 516 * d + 128) >> 8;

            // Clamp
            if (r > 255) r = 255; else if (r < 0) r = 0;
            if (g > 255) g = 255; else if (g < 0) g = 0;
            if (b > 255) b = 255; else if (b < 0) b = 0;

            // Write to buffer
            int idx = (y * width + x) * 3;
            rgb_buffer[idx] = (uint8_t)r;
            rgb_buffer[idx+1] = (uint8_t)g;
            rgb_buffer[idx+2] = (uint8_t)b;
        }
    }
    return rgb_buffer;
}

// --- MAIN FUNCTION: GET FIRST FRAME ---
// Returns: A pointer to RGB888 data (in PSRAM). Returns NULL on failure.
// Note: You must free() the result when done!
uint8_t* get_first_video_frame(const char *filepath, int width, int height) {
    ESP_LOGI("VID", "Extracting first frame from %s...", filepath);

    FILE *f = fopen(filepath, "rb");
    if (f == NULL) {
        ESP_LOGE("VID", "File not found");
        return NULL;
    }

    // 1. Setup Decoder
    esp_h264_dec_cfg_sw_t cfg = { .pic_type = ESP_H264_RAW_FMT_I420 };
    esp_h264_dec_handle_t dec_handle = NULL;
    if (esp_h264_dec_sw_new(&cfg, &dec_handle) != ESP_H264_ERR_OK) {
        fclose(f);
        return NULL;
    }
    esp_h264_dec_open(dec_handle);

    // 2. Allocate Temp Buffer (Large enough for one I-Frame)
    const int CHUNK_SIZE = 1024 * 64;
    uint8_t *file_buffer = (uint8_t *)heap_caps_malloc(CHUNK_SIZE, MALLOC_CAP_SPIRAM);
    
    uint8_t *result_pixels = NULL;
    int decoded = 0;
    
    esp_h264_dec_in_frame_t in_frame = { 0 };
    esp_h264_dec_out_frame_t out_frame = { 0 };

    // 3. Read Loop (Only runs until first frame is found)
    while (!decoded) {
        size_t bytes_read = fread(file_buffer, 1, CHUNK_SIZE, f);
        if (bytes_read == 0) break; // EOF

        in_frame.raw_data.buffer = file_buffer;
        in_frame.raw_data.len = bytes_read;
        
        while (in_frame.raw_data.len > 0) {
            esp_h264_err_t ret = esp_h264_dec_process(dec_handle, &in_frame, &out_frame);
            
            if (ret == ESP_H264_ERR_OK && out_frame.outbuf != NULL) {
                ESP_LOGI("VID", "Frame found! Converting...");
                
                // Pointers for YUV420 Planar
                uint8_t *y = out_frame.outbuf;
                uint8_t *u = y + (width * height);
                uint8_t *v = u + (width * height / 4);

                // Convert to RGB
                result_pixels = yuv420_to_rgb888(y, u, v, width, height);
                
                decoded = 1; // STOP LOOP
                break;
            }

            if (in_frame.consume > 0) {
                in_frame.raw_data.buffer += in_frame.consume;
                in_frame.raw_data.len -= in_frame.consume;
            } else {
                break; // Fetch more data
            }
        }
    }

    // 4. Cleanup
    free(file_buffer);
    esp_h264_dec_close(dec_handle);
    esp_h264_dec_del(dec_handle);
    fclose(f);

    if (result_pixels) {
        ESP_LOGI("VID", "Success. Frame extraction complete.");
    } else {
        ESP_LOGE("VID", "Failed to decode any frame.");
    }

    return result_pixels;
}

// --- MAIN FUNCTION: GET SPECIFIC FRAME ---
// frameNumber: 0-based index (0 = first frame, 100 = 101st frame)
uint8_t* get_video_frame(const char *filepath, int width, int height, int frameNumber) {
    ESP_LOGI("VID", "Seeking Frame %d in %s...", frameNumber, filepath);

    FILE *f = fopen(filepath, "rb");
    if (f == NULL) {
        ESP_LOGE("VID", "File not found");
        return NULL;
    }

    // 1. Setup Decoder
    esp_h264_dec_cfg_sw_t cfg = { .pic_type = ESP_H264_RAW_FMT_I420 };
    esp_h264_dec_handle_t dec_handle = NULL;
    if (esp_h264_dec_sw_new(&cfg, &dec_handle) != ESP_H264_ERR_OK) {
        fclose(f);
        return NULL;
    }
    esp_h264_dec_open(dec_handle);

    // 2. Allocate Buffer (64KB is usually enough for chunks)
    const int BUFFER_CAPACITY = 1024 * 64;
    uint8_t *file_buffer = (uint8_t *)heap_caps_malloc(BUFFER_CAPACITY, MALLOC_CAP_SPIRAM);
    
    uint8_t *result_pixels = NULL;
    int current_frame_index = 0;
    size_t current_data_len = 0;
    
    esp_h264_dec_in_frame_t in_frame = { 0 };
    esp_h264_dec_out_frame_t out_frame = { 0 };

    // 3. Decode Loop
    while (1) {
        // Read data into whatever space is left in buffer
        size_t space_available = BUFFER_CAPACITY - current_data_len;
        
        // Safety: If buffer full, hard reset logic (similar to your player task)
        if (space_available < 1024) {
             current_data_len = 0;
             space_available = BUFFER_CAPACITY;
        }

        size_t bytes_read = fread(file_buffer + current_data_len, 1, space_available, f);
        if (bytes_read == 0) break; // End of file

        current_data_len += bytes_read;
        in_frame.raw_data.buffer = file_buffer;
        in_frame.raw_data.len = current_data_len;
        
        while (in_frame.raw_data.len > 0) {
            esp_h264_err_t ret = esp_h264_dec_process(dec_handle, &in_frame, &out_frame);
            
            // Check if a frame was completed
            if (ret == ESP_H264_ERR_OK && out_frame.outbuf != NULL) {
                
                // Is this the frame we want?
                if (current_frame_index == frameNumber) {
                    ESP_LOGI("VID", "Found Frame %d! Converting...", frameNumber);
                    
                    uint8_t *y = out_frame.outbuf;
                    uint8_t *u = y + (width * height);
                    uint8_t *v = u + (width * height / 4);

                    // Convert ONLY this frame
                    result_pixels = yuv420_to_rgb888(y, u, v, width, height);
                    
                    // Cleanup and Exit immediately
                    goto cleanup;
                }
                
                // Not the right frame yet, keep going
                current_frame_index++;
            }

            // Advance pointers
            if (in_frame.consume > 0) {
                in_frame.raw_data.buffer += in_frame.consume;
                in_frame.raw_data.len -= in_frame.consume;
            } else {
                break; // Fetch more data
            }
        }

        // Preserve leftovers (Move to front)
        if (in_frame.raw_data.len > 0) {
            memmove(file_buffer, in_frame.raw_data.buffer, in_frame.raw_data.len);
        }
        current_data_len = in_frame.raw_data.len;
    }

cleanup:
    free(file_buffer);
    esp_h264_dec_close(dec_handle);
    esp_h264_dec_del(dec_handle);
    fclose(f);

    if (result_pixels) {
        ESP_LOGI("VID", "Frame extraction success.");
    } else {
        ESP_LOGE("VID", "Frame %d not found (Video too short?).", frameNumber);
    }

    return result_pixels;
}

void load_video_poster() {
    int w = 320;
    int h = 480;

    // 1. Get the pixels (Allocates memory in PSRAM)
    uint8_t *pixels = get_first_video_frame("/sdcard/output.h264", w, h);
    //uint8_t *pixels = get_video_frame("/sdcard/output.h264", w, h, 200);

    if (pixels != NULL) {
        // 2. Assign to your Framebuffer
        // Note: You might need to cast to (void*) depending on your struct definition
        fb = (Framebuffer){
            .displayWidth = w,
            .displayHeight = h,
            .pixelData = pixels, // The buffer we just created
            .colorMode = COLOR_MODE_BGR888
        };
        
        // 3. Draw it!
        GraphicsCommand cmd_flush = {
            .cmd = CMD_UPDATE_AREA,
            .x = 0, .y = 0,
            .w = 320, .h = 480
        };
        xQueueSend(g_graphics_queue, &cmd_flush, portMAX_DELAY);
        
        printf("load_video_poster...\n");
    }
}


void openHomeMenuItem(int tag) {
    if (tag == 0) {
        printf("open files");
        
        printf("Opening Files App...\n");
        
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        
        // B. Switch the UI
        // This function creates a new view and assigns it to global mainWindowView
        setup_files_ui();
        
        // C. Refresh the Screen
        // Since we changed the root view, we must redraw everything
        update_full_ui();
        
        //showTriangleAnimation();
        
    }
    else if (tag == 1) {
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        setup_settings_ui();
        update_full_ui();
        
    }
    else if (tag == 8) {
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        setup_calculator_ui();
        update_full_ui();
        
    }
    else if (tag == 7) {
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        setup_music_player_ui();
        update_full_ui();
        
        printf("Starting Playback...\n");
        xTaskCreatePinnedToCore(mp3_task_wrapper, "mp3_task", 32768, NULL, 5, NULL, 0);
        
    }
    else if (tag == 6) {
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        setup_gallery_ui();
        stop_recording();
        //update_full_ui();
        
    }
    else if (tag == 2) {
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        setup_text_ui();
        update_full_ui();
        
    }
    else if (tag == 5) {
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        setup_clock_app();
        update_full_ui();
        
    }
    else if (tag == 4) {
        /*GraphicsCommand cmd_clear = {
            .cmd = CMD_DRAW_RECT,
            .x = 0, .y = 0, .w = 320, .h = 480,
            .color = {0, 0, 0, 255}
        };
        xQueueSend(g_graphics_queue, &cmd_clear, 0);

        // 3. Start the Video Task
        // Stack size 8192 (8KB) is recommended for JPEG decoding
        xTaskCreatePinnedToCore(video_player_task, "video_task", 16384, NULL, 5, NULL, 1);*/
        
        printf("Opening Settings App...\n");
        openedApp = true;
        
        // A. Save the current Home Menu view
        if (mainWindowView != NULL) {
            push_view(mainWindowView);
        }
        
        currentView = CurrentViewPaint;
        //if (mainWindowView) freeViewHierarchy(mainWindowView);
        mainWindowView = viewWithFrame(ccRect(0, 0, 320, 480));
        mainWindowView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
        
        setup_video_preview_ui();
        
        //start_video_player(&fb);
        
        //load_video_poster();
        
        //setup_video_preview_ui1();
        
        update_full_ui();
        
        xTaskCreatePinnedToCore(next_frame_task, "anim_task", 16384, (void*)videoView, 5, NULL, 1);
        
        //xTaskCreatePinnedToCore(next_frame_task1, "anim_task", 16384, (void*)videoView, 5, NULL, 1);
    }
    else if (tag == 3) {
        printf("Opening Settings App...\n");
        //openedApp = true;
        
        // A. Save the current Home Menu view
        //if (mainWindowView != NULL) {
        //    push_view(mainWindowView);
        //}
        start_recording();
        //update_full_ui();
        
    }
    else if (tag == 10) {
        esp_light_sleep_start();
    }
    
    
}



void handle_wifi_touch(int x, int y) {
    // 1. Check Top-Level Controls (Toggle)
    CCView* toggle = find_subview_at_point(mainWindowView, x, y);
    
    if (x < 40 && y < 60) {
        turn_off_wifi_and_free_ram();
        close_current_app();
        return;
    }
    
    if (toggle && toggle->tag == TAG_WIFI_TOGGLE) {
        // Toggle logic...
        return;
    }

    // 2. Check List Container
    if (uiWifiListContainer) {
        CCView* target = find_subview_at_point(uiWifiListContainer, x, y);
        
        if (target) {
            int tag = target->tag;
            
            // --- PAGINATION LOGIC ---
            if (tag == TAG_WIFI_BTN_PREV) {
                if (g_wifi_page_index > 0) {
                    g_wifi_page_index--;
                    refresh_wifi_list_ui();
                    update_full_ui();
                }
            }
            else if (tag == TAG_WIFI_BTN_NEXT) {
                // Check if next page exists
                int max_pages = (g_wifi_scan_count + WIFI_ITEMS_PER_PAGE - 1) / WIFI_ITEMS_PER_PAGE;
                if (g_wifi_page_index < max_pages - 1) {
                    g_wifi_page_index++;
                    refresh_wifi_list_ui();
                    update_full_ui();
                }
            }
            // --- NETWORK SELECTION LOGIC ---
            else if (tag >= TAG_WIFI_NET_BASE) {
                int idx = tag - TAG_WIFI_NET_BASE;
                if (idx >= 0 && idx < g_wifi_scan_count) {
                    printf("Selected: %s\n", g_wifi_scan_results[idx].ssid);
                    // Open password prompt...
                }
            }
        }
    }
}

// Helper: Finds which subview is under the user's finger
// Returns: The specific child view (key, row, button), or NULL if nothing found.
CCView* find_subview_at_point(CCView* container, int globalX, int globalY) {
    if (!container || !container->subviews) return NULL;

    // 1. Calculate the Container's Global Position
    // We need this because the children's frames are relative to the Container,
    // but your touch is relative to the Screen (0,0).
    
    // (Assuming you have getAbsoluteOrigin available from scan.c,
    //  otherwise we assume container->frame->origin is screen coords if it's a top-level view)
    CCPoint containerOrigin = getAbsoluteOrigin(container);
    int containerAbsX = (int)containerOrigin.x;
    int containerAbsY = (int)containerOrigin.y;

    // 2. Convert Touch to "Container-Local" Coordinates
    int localTouchX = globalX - containerAbsX;
    int localTouchY = globalY - containerAbsY;

    // 3. Check Intersections (Iterate Backwards: Top-most view first)
    for (int i = container->subviews->count - 1; i >= 0; i--) {
        CCView* child = (CCView*)arrayObjectAtIndex(container->subviews, i);
        
        // Simple Rectangle Intersection Check
        if (localTouchX >= child->frame->origin->x &&
            localTouchX <= child->frame->origin->x + child->frame->size->width &&
            localTouchY >= child->frame->origin->y &&
            localTouchY <= child->frame->origin->y + child->frame->size->height) {
            
            return child; // Found it!
        }
    }

    return NULL; // Touched empty space/background
}

bool keyboardTouchEnabled = true;
bool appTouchEnabled = true;

static int64_t last_draw_time = 0;
const int64_t draw_interval_us = 100000; // 0.1 seconds in microseconds scrolls smoothly
//const int64_t draw_interval_us = 050000; // 0.05 seconds in microseconds has lag from sending too many requests to graphics pipeline

void updateTouch(void *pvParameter) {
    uint32_t event;
    FT6336U_TouchPointType touch_data;
    float newOffY;
    
    // This tracks the *actual* hardware state
    bool is_currently_pressed = false;
    
    while (1) {
        // Wait for an interrupt OR a 50ms timeout
        xQueueReceive(touch_intr_queue, &event, pdMS_TO_TICKS(50));
        
        esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
        if (ret != ESP_OK) {
            touch_data.touch_count = 0; // Treat scan error as a release
        }
        
        // --- ADD THIS LINE ---
        uint16_t current_y = touch_data.touch_count > 0 ? touch_data.tp[0].y : 0;
        
        // --- New State Logic ---
        if (touch_data.touch_count > 0) {
            // --- Finger is DOWN ---
            if (is_currently_pressed == false) {
                // This is a NEW press.
                is_currently_pressed = true; // 1. Set our local state
                g_drag_start_y = current_y; // Anchor the start point
                
                ESP_LOGI(TAG, "Touch Pressed: X=%u, Y=%u", touch_data.tp[0].x, touch_data.tp[0].y);
                
                // 2. Lock and post the "event" for LVGL to consume
                xSemaphoreTake(g_touch_mutex, portMAX_DELAY);
                g_last_x = touch_data.tp[0].x;
                g_last_y = touch_data.tp[0].y;
                g_last_state = LV_INDEV_STATE_PR; // Set the "event"
                xSemaphoreGive(g_touch_mutex);
                
                int text_w = 200;
                int text_h = 30;
                
                ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
                
                ESP_LOGI(TAG, "Graphics Command");
                
                int x1 = randNumberTo(320);
                int y1 = randNumberTo(480);
                
                x1 = 100;
                y1 = 410;
                
                ESP_LOGI(TAG, "Graphics Command %d %d", x1, y1);
                
                if (hasDrawnKeyboard == false) {
                    hasDrawnKeyboard = true;
                    
                    // --- 1. Send command to clear the area ---
                    GraphicsCommand cmd_clear;
                    cmd_clear.cmd = CMD_DRAW_RECT;
                    cmd_clear.x = x1;
                    cmd_clear.y = y1;
                    cmd_clear.w = text_w;
                    cmd_clear.h = text_h;
                    cmd_clear.color = blue;
                    //xQueueSend(g_graphics_queue, &cmd_clear, 0);
                    
                    ColorRGBA white2 = {.r = 255, .g = 255, .b = 255, .a = 255};
                    
                    // --- 2. Send command to draw the text ---
                    GraphicsCommand cmd_text;
                    cmd_text.cmd = CMD_DRAW_TEXT;
                    cmd_text.x = x1;
                    cmd_text.y = y1; // Your renderText needs to handle Y as baseline
                    cmd_text.text = strdup("Tapped!");
                    //xQueueSend(g_graphics_queue, &cmd_text, 0);
                    
                    // --- 2. Send command to draw the text ---
                    GraphicsCommand cmd_text1;
                    cmd_text1.cmd = CMD_DRAW_TEXT;
                    cmd_text1.x = 20;
                    cmd_text1.y = 380; // Your renderText needs to handle Y as baseline
                    cmd_text1.text = strdup(" Q  W  E  R  T  Y  U  I  O  P");
                    cmd_text1.color = white2;
                    xQueueSend(g_graphics_queue, &cmd_text1, 0);
                    
                    // --- 2. Send command to draw the text ---
                    GraphicsCommand cmd_text12;
                    cmd_text12.cmd = CMD_DRAW_TEXT;
                    cmd_text12.x = 20;
                    cmd_text12.y = 420; // Your renderText needs to handle Y as baseline
                    cmd_text12.text = strdup("   A  S  D  F  G  H  J  K  L");
                    cmd_text12.color = white2;
                    xQueueSend(g_graphics_queue, &cmd_text12, 0);
                    
                    // --- 2. Send command to draw the text ---
                    GraphicsCommand cmd_text13;
                    cmd_text13.cmd = CMD_DRAW_TEXT;
                    cmd_text13.x = 20;
                    cmd_text13.y = 460; // Your renderText needs to handle Y as baseline
                    cmd_text13.text = strdup("     Z  X  C  V  B  N  M");
                    cmd_text13.color = white2;
                    xQueueSend(g_graphics_queue, &cmd_text13, 0);
                    
                    const char *long_text = "The quick brown fox jumps over the lazy dog. "
                    "This text needs to demonstrate word-wrapping and line-breaking "
                    "within a confined area for our new scroll view. \n\n" // Explicit newline
                    "This final sentence should appear on a completely new line.";
                    
                    ColorRGBA text_color = {.r = 255, .g = 255, .b = 255, .a = 255}; // White
                    
                    // --- 3. Define and Initialize the TextFormat Struct ---
                    TextFormat text_format = {
                        // Property 1: Center the text horizontally within the clip_w area
                        .alignment = TEXT_ALIGN_CENTER,
                        
                        // Property 2: Never split words mid-line (will move "boundaries" to next line)
                        .wrapMode = TEXT_WRAP_MODE_WHOLE_WORD,
                        
                        // Property 3: Add 10 extra pixels of space between the calculated line height
                        .lineSpacing = -2,
                        
                        // Property 4: Use FreeType's kerning metrics for a polished look
                        .glyphSpacing = -1
                        
                        
                    };
                    
                    ColorRGBA black_alpha = {.r = 0, .g = 0, .b = 0, .a = 255};
                    GraphicsCommand cmd_text2;
                    cmd_text2.cmd = CMD_DRAW_TEXT_BOX;
                    cmd_text2.x = 20;
                    cmd_text2.y = 150; // Your renderText needs to handle Y as baseline
                    cmd_text2.w = 280;
                    cmd_text2.h = 300;
                    cmd_text2.color = text_color;
                    cmd_text2.textFormat = text_format;
                    cmd_text2.fontSize = 12;
                    cmd_text2.text = strdup(long_text);
                    xQueueSend(g_graphics_queue, &cmd_text2, 0);
                    
                    GraphicsCommand cmd_star;
                    cmd_star.cmd = CMD_DRAW_STAR;
                    cmd_star.x = 20;
                    cmd_star.y = 460; // Your renderText needs to handle Y as baseline
                    //xQueueSend(g_graphics_queue, &cmd_star, 0);
                    
                    GraphicsCommand cmd_update;
                    cmd_update.cmd = CMD_UPDATE_AREA;
                    cmd_update.x = 0;
                    cmd_update.y = 0; // A rough bounding box
                    cmd_update.w = 320;
                    cmd_update.h = 480; // A bit larger to be safe
                    xQueueSend(g_graphics_queue, &cmd_update, 0);
                    
                    
                    
                    if (!addedCursor) {
                        addedCursor = true;
                        if (setup_cursor_buffers() != ESP_OK) {
                            ESP_LOGE(TAG, "FATAL: Failed to set up cursor backup buffers. Halting task.");
                            vTaskDelete(NULL);
                            return;
                        }
                        ESP_LOGI(TAG, "Cursor backup buffer allocated.");
                        
                        // 2. Build the CMD_CURSOR_SETUP command
                        GraphicsCommand cmd_cursor_setup;
                        cmd_cursor_setup.cmd = CMD_CURSOR_SETUP;
                        
                        // Set the exact location here:
                        cmd_cursor_setup.x = 100;
                        cmd_cursor_setup.y = 100;
                        
                        // Pass the cursor's fixed size in case the graphics task needs it
                        cmd_cursor_setup.w = CURSOR_W;
                        cmd_cursor_setup.h = CURSOR_H;
                        
                        // 3. Send the command to initiate the blink cycle
                        //xQueueSend(g_graphics_queue, &cmd_cursor_setup, 0);
                    }
                    
                    
                    
                    /*ESP_LOGI(TAG, "Graphics Command testGraphics");
                     for (int i = 0; i < 10; i++){
                     x1 = 200;
                     y1 = (int)(30 * i);
                     
                     
                     ESP_LOGI(TAG, "Graphics Command %d %d", x1, y1);
                     
                     // --- 1. Send command to clear the area ---
                     GraphicsCommand cmd_clear;
                     cmd_clear.cmd = CMD_DRAW_RECT;
                     cmd_clear.x = x1;
                     cmd_clear.y = y1;
                     cmd_clear.w = text_w;
                     cmd_clear.h = text_h;
                     cmd_clear.color = blue;
                     //xQueueSend(g_graphics_queue, &cmd_clear, 0);
                     
                     // --- 2. Send command to draw the text ---
                     GraphicsCommand cmd_text;
                     cmd_text.cmd = CMD_DRAW_TEXT;
                     cmd_text.x = x1;
                     cmd_text.y = y1; // Your renderText needs to handle Y as baseline
                     strncpy(cmd_text.text, "Tapped!", 50);
                     xQueueSend(g_graphics_queue, &cmd_text, 0);
                     
                     }*/
                    
                    
                    
                    // --- 3. Send command to update the screen ---
                    /*GraphicsCommand cmd_update;
                     cmd_update.cmd = CMD_UPDATE_AREA;
                     cmd_update.x = x1;
                     cmd_update.y = y1 - text_h; // A rough bounding box
                     cmd_update.w = text_w;
                     cmd_update.h = text_h * 2; // A bit larger to be safe
                     xQueueSend(g_graphics_queue, &cmd_update, 0);*/
                    
                    
                }
                else {
                    ColorRGBA black_alpha = {.r = 0, .g = 0, .b = 0, .a = 255};
                    GraphicsCommand cmd_text;
                    cmd_text.cmd = CMD_DRAW_TEXT;
                    cmd_text.x = 30+keyboardCursorPosition*20;
                    cmd_text.y = 53; // Your renderText needs to handle Y as baseline
                    cmd_text.color = black_alpha;
                    cmd_text.text = strdup(letterForCoordinate());
                    //xQueueSend(g_graphics_queue, &cmd_text, 0);
                    
                    keyboardCursorPosition++;
                    
                    /*GraphicsCommand cmd_update;
                     cmd_update.cmd = CMD_UPDATE_AREA;
                     cmd_update.x = cmd_text.x;
                     cmd_update.y = cmd_text.y; // A rough bounding box
                     cmd_update.w = 30;
                     cmd_update.h = 30; // A bit larger to be safe
                     xQueueSend(g_graphics_queue, &cmd_update, 0);*/
                    
                    GraphicsCommand cmd_update;
                    cmd_update.cmd = CMD_UPDATE_AREA;
                    cmd_update.x = 0;
                    cmd_update.y = 0; // A rough bounding box
                    cmd_update.w = 320;
                    cmd_update.h = 100; // A bit larger to be safe
                    //xQueueSend(g_graphics_queue, &cmd_update, 0);
                    
                    if (!setupui) {
                        
                        // 1. Create the high-level View Objects (RAM only)
                        // Make sure setup_ui_demo assigns to the global 'mainWindowView'
                        setup_ui_demo();
                        
                        // 2. Trigger the first render (Generates commands)
                        //update_full_ui();
                        
                        setupui = true;
                    }
                    
                    
                    
                    ESP_LOGI(TAG, "app_main() finished. Tasks are running.");
                }
                
            }
            else {
                ESP_LOGI(TAG, "touch 3");
                int curr_y = touch_data.tp[0].y;
                if (myDemoTextView != NULL) {
                    g_active_scrollview = myDemoTextView->scrollView;
                    
                    if (notFirstTouch == false) {
                        notFirstTouch = true;
                        g_touch_last_y = curr_y;
                        lastOffY = g_active_scrollview->contentOffset->y;
                        ESP_LOGI(TAG, "set lastOffY %d", lastOffY);
                    }
                    
                    if (g_active_scrollview) {
                        
                        int delta = g_touch_last_y - curr_y; // Drag Up = Positive Delta
                        ESP_LOGI(TAG, "g_active_scrollview %d", delta);
                        
                        // Calculate new offset
                        newOffY = lastOffY + (float)delta;
                        
                        float yValue = (float)lastOffY;
                        
                        CCPoint offsetPt;
                        offsetPt.x = 0;
                        offsetPt.y = (float)lastOffY;
                        
                        lastOffY = delta;
                        
                        CCPoint targetPoint = {
                            .x = 0,
                            .y = lastOffY // Correctly assigns the float value
                        };
                        
                        // Apply offset (This function handles clamping and moving the view)
                        //CCPoint offsetPt = {.x = 0, .y = yValue};
                        ESP_LOGI(TAG, "g_active_scrollview1 %f %f %f", lastOffY, newOffY, yValue);
                        scrollViewSetContentOffset(g_active_scrollview, &targetPoint);//ccPoint(0, lastOffY)
                        ESP_LOGI(TAG, "g_active_scrollview contentOffset %f", g_active_scrollview->contentOffset->y);
                        
                        // 2. THROTTLING CHECK: Only draw if 0.2s has passed
                        int64_t now = esp_timer_get_time();
                        
                        if ((now - last_draw_time) > draw_interval_us) {
                            
                            // Send the expensive command
                            update_view_only(g_active_scrollview->view);
                            
                            ESP_LOGI(TAG, "g_active_scrollview contentOffset %f", g_active_scrollview->contentOffset->y);
                            
                            // Reset the timer
                            last_draw_time = now;
                        }
                        
                    }
                }
                
                
                
            }
            // If finger is still down, we do nothing.
            
        } else {
            
            // --- Finger is UP ---
            if (is_currently_pressed == true) {
                // This is a NEW release.
                is_currently_pressed = false; // 1. Set our local state
                notFirstTouch = false;
                if (g_active_scrollview) ESP_LOGI(TAG, "Touch Released");
                //update_view_only(g_active_scrollview->view);
                /*if (touch_data.tp[0].y < 50 && touch_data.tp[0].y > 0 && g_active_scrollview) {
                 CCPoint offsetPt = {.x = 0, .y = newOffY};
                 scrollViewSetContentOffset(g_active_scrollview, &offsetPt);
                 update_view_only(g_active_scrollview->view);
                 ESP_LOGI(TAG, "g_active_scrollview contentOffset %f", g_active_scrollview->contentOffset->y);
                 }*/
            }
        }
        
       /* if (touch_data.touch_count > 0 && setupui == true) {
            ESP_LOGI(TAG, "g_active_scrollview0");
            int curr_y = touch_data.tp[0].y;
            
            if (myDemoTextView != NULL) {
                g_active_scrollview = myDemoTextView->scrollView;
                ESP_LOGI(TAG, "g_active_scrollview2");
            } else {
                g_active_scrollview = NULL; // Safe default
            }
            
            if (!is_currently_pressed) {
                // --- FINGER DOWN ---
                is_currently_pressed = true;
                g_touch_last_y = curr_y;
                
                // Hit Test logic (Pseudo-code)
                // CCView* touched = hitTest(mainWindowView, x, y);
                // Check if 'touched' is inside our specific 'myDemoTextView->scrollView'
                
                // SAFETY CHECK: Only assign if the object exists
                ESP_LOGI(TAG, "g_active_scrollview1");
                
            }
            else {
                // --- FINGER DRAG ---
                if (g_active_scrollview) {
                    ESP_LOGI(TAG, "g_active_scrollview");
                    int delta = g_touch_last_y - curr_y; // Drag Up = Positive Delta
                    
                    // Calculate new offset
                    float currentOffY = g_active_scrollview->contentOffset->y;
                    float newOffY = currentOffY + delta;
                    
                    // Apply offset (This function handles clamping and moving the view)
                    CCPoint offsetPt = {0, newOffY};
                    scrollViewSetContentOffset(g_active_scrollview, &offsetPt);
                    
                    ESP_LOGI(TAG, "scrollViewSetContentOffset %f", newOffY);
                    
                    // Trigger Screen Refresh
                    update_view_only(g_active_scrollview->view);
                    
                    g_touch_last_y = curr_y;
                }
            }
        } else {
            // --- FINGER UP ---
            is_currently_pressed = false;
            g_active_scrollview = NULL;
        }*/
        
        //is_currently_pressed = false; //changed dec13 11-17a
        
        if (touchEnabled) {
            if (touch_data.touch_count > 0 && setupui == true) {
                int touchX = touch_data.tp[0].x;
                int touchY = touch_data.tp[0].y;
                // If keyboard is visible (check if uiKeyboardView != NULL)
                if (uiKeyboardView != NULL && keyboardTouchEnabled == true) {
                    if (touchY > uiKeyboardView->frame->origin->y) {
                        keyboardTouchEnabled = false;
                        handle_keyboard_touch(touchX, touchY);
                        printf("handle handle_keyboard_touch");
                        //return; // Swallow touch so nothing underneath gets clicked
                    }
                    
                }
            }
            else {
                keyboardTouchEnabled = true;
            }
            
            //printf("touch3");
            
            if (currentView == CurrentViewHome) {
                
                if (touch_data.touch_count > 0 && setupui == true) {
                    int curr_x = touch_data.tp[0].x;
                    int curr_y = touch_data.tp[0].y;
                    
                    checkAvailableMemory();
                    
                    CCView* touchedView = find_grid_item_at(curr_x, curr_y);
                    CCPoint parentAbs = getAbsoluteOrigin(touchedView);
                    
                    if (touchedView && touchedView != g_last_touched_icon && openedApp == false) {
                        if (g_last_touched_icon != NULL) {
                            if (g_last_touched_icon->backgroundColor) free(g_last_touched_icon->backgroundColor);
                            g_last_touched_icon->backgroundColor = color(0,0,0,0);
                            update_view_area_via_parent(g_last_touched_icon);
                        }
                        
                        if (touchedView->backgroundColor) free(touchedView->backgroundColor);
                        touchedView->backgroundColor = color(0.0, 0.0, 0.0, 0.2);
                        update_view_only(touchedView);
                        printf("updatevie1w");
                        openHomeMenuItem(touchedView->tag);
                        g_last_touched_icon = touchedView;
                    }
                }
                else {
                    // --- FINGER UP (Release) ---
                    if (g_last_touched_icon != NULL && openedApp == false) {
                        if (g_last_touched_icon->backgroundColor) free(g_last_touched_icon->backgroundColor);
                        g_last_touched_icon->backgroundColor = color(0,0,0,0);
                        update_view_area_via_parent(g_last_touched_icon);
                        g_last_touched_icon = NULL;
                    }
                    is_currently_pressed = false;
                }
            }
            else if (currentView == CurrentViewFiles || currentView == CurrentViewSettings  || currentView == CurrentViewCalculator  || currentView == CurrentViewMusic  || currentView == CurrentViewPhotos || currentView == CurrentViewText || currentView == CurrentViewClock) {
                //else if (currentView == CurrentViewFiles || currentView == CurrentViewSettings)
                if (touch_data.touch_count > 0 && setupui == true) {
                    int x = touch_data.tp[0].x;
                    int y = touch_data.tp[0].y;
                    
                    checkAvailableMemory();
                    
                    if (x < 40 && y < 60) {
                        close_current_app();
                    }
                    
                    if (currentView == CurrentViewSettings) {
                        /*CCView* row = find_subview_at_point(mainWindowView, x, y);
                        if (row) {
                            printf("open row %d", row->tag);
                            if (row->tag == 0) {
                                
                            }
                            else if (row->tag == 1) {
                                
                            }
                            else if (row->tag == 2) {
                                
                            }
                            else if (row->tag == 3) {
                                if (mainWindowView != NULL) {
                                    push_view(mainWindowView);
                                }
                                showTriangleAnimation();
                                setup_wifi_ui();
                            }
                            else if (row->tag == 4) {
                                
                            }
                        }*/
                        handle_settings_touch(x, y, 1);
                    }
                    if (currentView == CurrentViewPhotos) {
                        CCView* row = find_subview_at_point(mainWindowView, x, y);
                        if (row) {
                            handle_gallery_touch(row->tag);
                        }
                    }
                    if (currentView == CurrentViewCalculator) {
                        CCView* row = find_subview_at_point(mainWindowView, x, y);
                        if (row) {
                            handle_calculator_input(row->tag);
                        }
                        
                    }
                    if (currentView == CurrentViewClock) {
                        printf("CurrentViewClock");
                        handle_clock_touch(x, y, 1);
                    }
                    
                }
                else {
                    
                }
            }
            else if (currentView == CurrentViewWifi) {
                if (touch_data.touch_count > 0 && setupui == true) {
                    int x = touch_data.tp[0].x;
                    int y = touch_data.tp[0].y;
                    
                    checkAvailableMemory();
                    
                    if (appTouchEnabled == true) {
                        appTouchEnabled = false;
                        handle_wifi_touch(x, y);
                    }
                    
                }
                else {
                    appTouchEnabled = true;
                }
            }
            
            
        }
        

        
        
        
        
        
    }
}

// ----------------------------------------------------------------------
// INITIALIZATION FUNCTION
// Call this function from your app_main
// ----------------------------------------------------------------------
/*void initialize_touch()
 {
 ESP_LOGI(TAG, "Initializing I2C Master for touch panel");
 
 // --- 1. Create the new I2C Master Bus ---
 i2c_master_bus_config_t bus_cfg = {
 .i2c_port = I2C_HOST,
 .scl_io_num = PIN_NUM_I2C_SCL,
 .sda_io_num = PIN_NUM_I2C_SDA,
 .clk_source = I2C_CLK_SRC_DEFAULT,
 .glitch_ignore_cnt = 7,
 .flags.enable_internal_pullup = true,
 };
 i2c_master_bus_handle_t bus_handle;
 ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus_handle));
 
 // --- 2. Create the I2C Panel IO Handle (THE MISSING STEP) ---
 ESP_LOGI(TAG, "Creating I2C panel IO handle");
 esp_lcd_panel_io_handle_t io_handle = NULL;
 esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_FT6x36_CONFIG();
 
 io_config.scl_speed_hz = 400000;
 
 // This is the line you were missing.
 // It creates the actual IO device handle.
 ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(bus_handle, &io_config, &io_handle));
 
 // --- 3. Configure and Initialize the Touch Controller ---
 esp_lcd_touch_config_t tp_cfg = {
 .x_max = LCD_H_RES,
 .y_max = LCD_V_RES,
 .rst_gpio_num = -1,//PIN_NUM_TOUCH_RST,
 .int_gpio_num = PIN_NUM_TOUCH_INT,
 .levels = {
 .reset = 0,
 .interrupt = 0,
 },
 .flags = {
 .swap_xy = 0,
 .mirror_x = 0,
 .mirror_y = 0,
 },
 };
 
 esp_lcd_touch_handle_t tp; // <-- Your handle is named 'tp'
 // Now you are passing the *valid* io_handle
 esp_err_t err = esp_lcd_touch_new_i2c_ft6x36(io_handle, &tp_cfg, &tp);
 
 if (err != ESP_OK) {
 ESP_LOGE(TAG, "Touch panel init failed, continuing without touch...");
 } else {
 ESP_LOGI(TAG, "Touch panel initialized successfully");
 
 // --- 4. Register Touch Input with LVGL ---
 ESP_LOGI(TAG, "Registering touch input with LVGL");
 
 lvgl_touch_indev = lv_indev_create();
 lv_indev_set_type(lvgl_touch_indev, LV_INDEV_TYPE_POINTER);
 lv_indev_set_read_cb(lvgl_touch_indev, lvgl_touch_cb);
 
 // *** FIX 2 (Typo) ***
 // Pass the handle you just created, which is 'tp', not 'tp_handle'
 lv_indev_set_user_data(lvgl_touch_indev, tp);
 
 // (Rest of your LVGL code...)
 }
 }*/

//Joystick

// Define the GPIOs and their corresponding ADC Channels
#define JOY_X_PIN  ADC_CHANNEL_3 // GPIO4 corresponds to ADC1 Channel 3
#define JOY_Y_PIN  ADC_CHANNEL_4 // GPIO5 corresponds to ADC1 Channel 4

// ADC handle and configuration structures
adc_oneshot_unit_handle_t adc1_handle;

#define DEFAULT_SCAN_LIST_SIZE CONFIG_EXAMPLE_SCAN_LIST_SIZE

#ifdef CONFIG_EXAMPLE_USE_SCAN_CHANNEL_BITMAP
#define USE_CHANNEL_BITMAP 1
#define CHANNEL_LIST_SIZE 3
static uint8_t channel_list[CHANNEL_LIST_SIZE] = {1, 6, 11};
#endif /*CONFIG_EXAMPLE_USE_SCAN_CHANNEL_BITMAP*/



static void print_auth_mode(int authmode)
{
    switch (authmode) {
        case WIFI_AUTH_OPEN:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_OPEN");
            break;
        case WIFI_AUTH_OWE:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_OWE");
            break;
        case WIFI_AUTH_WEP:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WEP");
            break;
        case WIFI_AUTH_WPA_PSK:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA_PSK");
            break;
        case WIFI_AUTH_WPA2_PSK:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_PSK");
            break;
        case WIFI_AUTH_WPA_WPA2_PSK:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA_WPA2_PSK");
            break;
        case WIFI_AUTH_ENTERPRISE:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_ENTERPRISE");
            break;
        case WIFI_AUTH_WPA3_PSK:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA3_PSK");
            break;
        case WIFI_AUTH_WPA2_WPA3_PSK:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_WPA3_PSK");
            break;
        case WIFI_AUTH_WPA3_ENTERPRISE:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA3_ENTERPRISE");
            break;
        case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_WPA3_ENTERPRISE");
            break;
        case WIFI_AUTH_WPA3_ENT_192:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA3_ENT_192");
            break;
        default:
            ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_UNKNOWN");
            break;
    }
}

static void print_cipher_type(int pairwise_cipher, int group_cipher)
{
    switch (pairwise_cipher) {
        case WIFI_CIPHER_TYPE_NONE:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_NONE");
            break;
        case WIFI_CIPHER_TYPE_WEP40:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_WEP40");
            break;
        case WIFI_CIPHER_TYPE_WEP104:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_WEP104");
            break;
        case WIFI_CIPHER_TYPE_TKIP:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_TKIP");
            break;
        case WIFI_CIPHER_TYPE_CCMP:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_CCMP");
            break;
        case WIFI_CIPHER_TYPE_TKIP_CCMP:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_TKIP_CCMP");
            break;
        case WIFI_CIPHER_TYPE_AES_CMAC128:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_AES_CMAC128");
            break;
        case WIFI_CIPHER_TYPE_SMS4:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_SMS4");
            break;
        case WIFI_CIPHER_TYPE_GCMP:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_GCMP");
            break;
        case WIFI_CIPHER_TYPE_GCMP256:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_GCMP256");
            break;
        default:
            ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_UNKNOWN");
            break;
    }
    
    switch (group_cipher) {
        case WIFI_CIPHER_TYPE_NONE:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_NONE");
            break;
        case WIFI_CIPHER_TYPE_WEP40:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_WEP40");
            break;
        case WIFI_CIPHER_TYPE_WEP104:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_WEP104");
            break;
        case WIFI_CIPHER_TYPE_TKIP:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_TKIP");
            break;
        case WIFI_CIPHER_TYPE_CCMP:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_CCMP");
            break;
        case WIFI_CIPHER_TYPE_TKIP_CCMP:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_TKIP_CCMP");
            break;
        case WIFI_CIPHER_TYPE_SMS4:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_SMS4");
            break;
        case WIFI_CIPHER_TYPE_GCMP:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_GCMP");
            break;
        case WIFI_CIPHER_TYPE_GCMP256:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_GCMP256");
            break;
        default:
            ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_UNKNOWN");
            break;
    }
}

#ifdef USE_CHANNEL_BITMAP
static void array_2_channel_bitmap(const uint8_t channel_list[], const uint8_t channel_list_size, wifi_scan_config_t *scan_config) {
    
    for(uint8_t i = 0; i < channel_list_size; i++) {
        uint8_t channel = channel_list[i];
        scan_config->channel_bitmap.ghz_2_channels |= (1 << channel);
    }
}
#endif /*USE_CHANNEL_BITMAP*/


/* Initialize Wi-Fi as sta and set scan method */
static void wifi_scan(void)
{
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif);
    
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    
    uint16_t number = DEFAULT_SCAN_LIST_SIZE;
    wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE];
    uint16_t ap_count = 0;
    memset(ap_info, 0, sizeof(ap_info));
    
    
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());
    
#ifdef USE_CHANNEL_BITMAP
    wifi_scan_config_t *scan_config = (wifi_scan_config_t *)calloc(1,sizeof(wifi_scan_config_t));
    if (!scan_config) {
        ESP_LOGE(TAG, "Memory Allocation for scan config failed!");
        return;
    }
    array_2_channel_bitmap(channel_list, CHANNEL_LIST_SIZE, scan_config);
    esp_wifi_scan_start(scan_config, true);
    free(scan_config);
    
#else
    esp_wifi_scan_start(NULL, true);
#endif /*USE_CHANNEL_BITMAP*/
    
    ESP_LOGI(TAG, "Max AP number ap_info can hold = %u", number);
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info));
    ESP_LOGI(TAG, "Total APs scanned = %u, actual AP number ap_info holds = %u", ap_count, number);
    for (int i = 0; i < number; i++) {
        ESP_LOGI(TAG, "SSID \t\t%s", ap_info[i].ssid);
        ESP_LOGI(TAG, "RSSI \t\t%d", ap_info[i].rssi);
        print_auth_mode(ap_info[i].authmode);
        if (ap_info[i].authmode != WIFI_AUTH_WEP) {
            print_cipher_type(ap_info[i].pairwise_cipher, ap_info[i].group_cipher);
        }
        ESP_LOGI(TAG, "Channel \t\t%d", ap_info[i].primary);
    }
}




#define TARGET_APP_MAIN_ADDRESS 0x4200aec8

// Function pointer to the loaded program entry
void (*loaded_program_entry)(void);

// This is an *oversimplified* attempt and likely only works if the
// binary is specifically compiled as Position-Independent Code (PIC)
// or if you manually calculate the needed offsets.

void print_heap_info() {
    size_t largest_free_block = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM|MALLOC_CAP_EXEC);// |MALLOC_CAP_EXEC|MALLOC_CAP_32BIT
    printf("Largest contiguous block for EXEC/SPIRAM: %zu bytes\n", largest_free_block);
}

long get_file_size(const char *filename) {
    FILE *f = fopen(filename, "rb");
    if (f == NULL) return 0;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fclose(f);
    return size;
}

void load_and_execute_program(const char* file_path) {
    
    // 1. Determine the size of the file
    struct stat st;
    if (stat(file_path, &st) != 0) {
        printf("Failed to get file size for %s.\n", file_path);
        return;
    }
    size_t file_size = st.st_size;
    printf("File size to load: %zu bytes\n", file_size);
    
    // 2. Allocate memory for the program from Executable PSRAM
    // MALLOC_CAP_SPIRAM: Use External RAM (PSRAM)
    // MALLOC_CAP_EXEC: Required for executing code from this memory
    // MALLOC_CAP_32BIT: Required for aligned 32-bit instructions
    print_heap_info();
    void* program_memory = heap_caps_malloc(file_size,
                                            MALLOC_CAP_SPIRAM |
                                            MALLOC_CAP_EXEC |
                                            MALLOC_CAP_32BIT);
    
    if (!program_memory) {
        printf("Memory allocation failed for program (needed %zu bytes).\n", file_size);
        return;
    }
    printf("Successfully allocated memory at address: %p\n", program_memory);
    
    // 3. Open and Read the binary into allocated memory
    FILE* program_file = fopen(file_path, "rb");
    if (!program_file) {
        printf("Failed to open program file.\n");
        heap_caps_free(program_memory);
        return;
    }
    
    size_t bytes_read = fread(program_memory, 1, file_size, program_file);
    fclose(program_file);
    
    if (bytes_read != file_size) {
        printf("Error reading file! Read %zu of %zu bytes.\n", bytes_read, file_size);
        heap_caps_free(program_memory);
        return;
    }
    
    // 4. Point and Jump to the loaded program (app_main is at the start)
    // NOTE: This only works if 'app_main' is the *very first instruction*
    // in your raw .bin file, which is highly unlikely for a full ESP-IDF bin.
    // The REAL RELOCATION PROBLEM still exists after this step.
    loaded_program_entry = (void (*)(void))program_memory;
    
    printf("Jumping to loaded program...\n");
    loaded_program_entry();
    
    // 5. Clean up
    heap_caps_free(program_memory);
    printf("Returned to main program after running loaded program.\n");
}



/**
 * @brief Lists all files and directories in the given mount point.
 * * @param mount_point The base path where the filesystem is mounted (e.g., "/fat").
 */
void list_directory_contents(const char *mount_point) {
    DIR *dir = NULL;
    struct dirent *ent;
    char full_path[128];
    struct stat st;
    
    ESP_LOGI(TAG, "--- Files in %s partition: ---", mount_point);
    
    // 1. Open the directory stream
    dir = opendir(mount_point);
    if (!dir) {
        ESP_LOGE(TAG, "Failed to open directory: %s", mount_point);
        return;
    }
    
    // 2. Read entries one by one
    while ((ent = readdir(dir)) != NULL) {
        // Skip '.' (current dir) and '..' (parent dir) entries which are common in POSIX
        if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
            continue;
        }
        
        // Construct the full path to get file size
        snprintf(full_path, sizeof(full_path), "%s/%s", mount_point, ent->d_name);
        
        // 3. Get file status (size and type)
        if (stat(full_path, &st) == 0) {
            if (ent->d_type == DT_DIR) {
                // Directories
                printf("  [DIR]  %s\n", ent->d_name);
            } else {
                // Regular Files
                printf("  [FILE] %s (Size: %ld bytes)\n", ent->d_name, st.st_size);
            }
        } else {
            ESP_LOGW(TAG, "Failed to stat file/dir: %s", full_path);
        }
    }
    
    // 4. Close the directory stream
    closedir(dir);
    ESP_LOGI(TAG, "--- Listing Complete ---");
}

/**
 * @brief Initializes and mounts the FATFS partition using the Wear Leveling driver.
 */
void mount_fatfs(void) {
    // Configuration for the mount (Ensure CONFIG_WL_SECTOR_SIZE is set in menuconfig)
    /*const esp_vfs_fat_mount_config_t mount_config = {
     .max_files = 10000,
     .format_if_mount_failed = false, // Format if initial mount fails (enables writing)
     .allocation_unit_size = CONFIG_WL_SECTOR_SIZE
     };*/
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,
        .max_files = 100
    };
    
    ESP_LOGI(TAG, "Mounting FATFS partition '%s' at %s...", PARTITION_LABEL, MOUNT_POINT);
    
    // Mount the FAT partition with Read/Write (RW) and Wear Leveling (WL) support
    /*esp_err_t ret = esp_vfs_fat_spiflash_mount_rw_wl(
     MOUNT_POINT,        // Base path (e.g., /fat)
     PARTITION_LABEL,    // Partition Label (e.g., fat_data)
     &mount_config,
     &wl_handle          // Wear Leveling handle (output)
     );*/
    
    esp_err_t ret = esp_vfs_fat_spiflash_mount_ro("/spiflash", "storage", &mount_config);
    
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "FATFS mounted successfully.");
    }
}


void checkAvailableMemory() {
    size_t free_heap_size = heap_caps_get_free_size(MALLOC_CAP_8BIT);
    printf("Free heap size: %zu bytes\n", free_heap_size);
    size_t free_heap_size1 = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
    printf("Free heap size spiram: %zu bytes\n", free_heap_size1);
    size_t free_heap_size2 = heap_caps_get_free_size(MALLOC_CAP_EXEC);
    printf("Free heap size MALLOC_CAP_EXEC: %zu bytes\n", free_heap_size2);
    // Optionally, you can print other heap info
    size_t free_internal_heap_size = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
    printf("Free internal heap size: %zu bytes\n", free_internal_heap_size);
    printf("Free heap size: %zu bytes\n", heap_caps_get_free_size(MALLOC_CAP_8BIT));
    
    ESP_LOGI(TAG, "Free heap size: %zu bytes\n", free_heap_size);
    ESP_LOGI(TAG, "Free internal heap size: %zu bytes\n", free_internal_heap_size);
    
    //ESP_LOGI(TAG, "EXECUTABLE 2\n");
    
}

// Function to draw a block of pixels (240x240)
void raw_drawing_task(void *pvParameter)
{
    ESP_LOGI(TAG, "Starting raw drawing demo.");
    
    // Create a pixel buffer for the entire screen (240 * 240 pixels * 2 bytes/pixel)
    // This is a large buffer, ensure your PSRAM is enabled/available.
    // If you don't have enough RAM/PSRAM, reduce the size or use a small tile.
    uint16_t *color_data = (uint16_t *)heap_caps_malloc(LCD_H_RES * LCD_V_RES * sizeof(uint16_t), MALLOC_CAP_DMA);
    if (!color_data) {
        ESP_LOGE(TAG, "Failed to allocate display buffer!");
        vTaskDelete(NULL);
        return;
    }
    
    // Fill the buffer with the color red (0xF800)
    for (int i = 0; i < LCD_H_RES * LCD_V_RES; i++) {
        color_data[i] = LCD_COLOR_RED;
    }
    
    // Draw the bitmap once per second
    while (1) {
        // Function: esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data)
        ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(
                                                  panel_handle,
                                                  0,                 // x_start
                                                  0,                 // y_start
                                                  LCD_H_RES,         // x_end (Exclusive, so 240 is 0-239)
                                                  LCD_V_RES,         // y_end (Exclusive, so 240 is 0-239)
                                                  color_data
                                                  ));
        
        ESP_LOGI(TAG, "Drew a red screen.");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
    // Note: In a real app, you would free the memory before deleting the task.
}

/*void lvgl_ui_task(void *pvParameter) {
 // *** LVGL UI Creation Code Goes Here ***
 lv_obj_t *label = lv_label_create(lv_screen_active());
 lv_label_set_text(label, "LVGL is Running!");
 lv_obj_center(label);
 
 // LVGL main loop handler
 while(1) {
 lv_timer_handler(); // Processes all LVGL tasks and redrawing
 vTaskDelay(pdMS_TO_TICKS(5)); // Wait 5ms
 }
 }*/


// Style for small, normal weight text (e.g., 18px)
static lv_style_t style_normal;

// Style for large, bold/heavy text (e.g., 36px)
static lv_style_t style_heavy;

// Style for medium, light/thin text (e.g., 24px)
static lv_style_t style_light;

/**
 * @brief Initialize all custom LVGL styles.
 */
static void init_custom_styles()
{
    // --- Normal Style (Montserrat 18) ---
    lv_style_init(&style_normal);
    // Use the built-in Montserrat font with size 18 and a medium/normal weight.
    lv_style_set_text_font(&style_normal, &lv_font_montserrat_8);
    lv_style_set_text_color(&style_normal, lv_color_white());
    
    // --- Heavy Style (Montserrat 36) ---
    lv_style_init(&style_heavy);
    // Use the largest, heaviest weight available (Montserrat 36)
    lv_style_set_text_font(&style_heavy, &lv_font_montserrat_10);
    lv_style_set_text_color(&style_heavy, lv_color_hex(0xFFDD00)); // Yellowish
    
    // --- Light Style (Montserrat 24) ---
    lv_style_init(&style_light);
    // Use Montserrat 24. Since LVGL doesn't have explicit 'light' weight files
    // for all sizes, we use the default and often customize it further if needed.
    // However, 24px is a distinct size from 18 and 36.
    lv_style_set_text_font(&style_light, &lv_font_montserrat_12);
    lv_style_set_text_color(&style_light, lv_color_hex(0xAAAAAA)); // Gray
}


// -----------------------------------------------------------
// 2. UI TASK FUNCTION
// -----------------------------------------------------------

/**
 * @brief LVGL UI creation and handler task.
 */
void lvgl_ui_task(void *pvParameter)
{
    ESP_LOGI(TAG, "LVGL UI Task started.");
    
    // Lock the LVGL mutex before accessing any LVGL objects
    lvgl_port_lock(0);
    
    // 1. Initialize styles
    init_custom_styles();
    
    lv_obj_t *scr = lv_screen_active();
    lv_obj_set_style_bg_color(scr, lv_color_hex(0x002244), 0); // Dark Blue background
    
    // --- LABEL 1: Large & Heavy ---
    lv_obj_t *label1 = lv_label_create(scr);
    lv_label_set_text(label1, "SYSTEM ONLINE");
    lv_obj_add_style(label1, &style_heavy, 0); // Apply the heavy style
    lv_obj_align(label1, LV_ALIGN_TOP_MID, 0, 10);
    
    // --- LABEL 2: Medium & Light ---
    lv_obj_t *label2 = lv_label_create(scr);
    lv_label_set_text(label2, "Meet with sales team");
    lv_obj_add_style(label2, &style_light, 0); // Apply the light style
    // Align below Label 1
    lv_obj_align_to(label2, label1, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
    
    // --- LABEL 3: Small & Normal ---
    lv_obj_t *label3 = lv_label_create(scr);
    lv_label_set_text(label3, "Powered by ESP-IDF and LVGL");
    lv_obj_add_style(label3, &style_normal, 0); // Apply the normal style
    // Align below Label 2
    lv_obj_align_to(label3, label2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
    
    // Unlock the LVGL mutex
    lvgl_port_unlock();
    
    // -----------------------------------------------------------
    // LVGL Main Loop
    // -----------------------------------------------------------
    while(1) {
        // Must protect lv_timer_handler() with lock/unlock
        lvgl_port_lock(0);
        lv_timer_handler(); // Processes all LVGL tasks, redrawing, and animations
        lvgl_port_unlock();
        
        vTaskDelay(pdMS_TO_TICKS(5));
    }
}


// Styles (reusing simple styles, assume default LVGL fonts are available)
static lv_style_t style_main_bg;

/**
 * @brief Initializes base styles for the demo.
 */
static void init_kb_styles()
{
    lv_style_init(&style_main_bg);
    // Set background color for the screen (or container)
    lv_style_set_bg_color(&style_main_bg, lv_color_hex(0xEEEEEE));
    lv_style_set_text_color(&style_main_bg, lv_color_black());
}

// Global variables to store the latest joystick values (access must be thread-safe)
volatile int g_joystick_x = 0;
volatile int g_joystick_y = 0;

// Mutex to protect access to the global variables
SemaphoreHandle_t joystick_mutex;

lv_obj_t *g_cursor_obj;
volatile lv_coord_t g_cursor_x_pos; // Manual cursor position tracker
volatile lv_coord_t g_cursor_y_pos; // Manual cursor position tracker

// Joystick ADC Range
#define JOY_X_MIN 1000
#define JOY_X_MAX 3200
#define JOY_Y_MIN 600
#define JOY_Y_MAX 3600

#define JOY_X_LEFT 1100 //200 to 1100, move left if less than 1100
#define JOY_X_NEUTRAL 1600 //1500 to 2400, do not move cursor on x axis if joystick x between these values
#define JOY_X_RIGHT 2600 //2800 to 3600, move right if greater than 2800
#define JOY_Y_DOWN 1400 //600 to 1200, move down if less than 1200
#define JOY_Y_NEUTRAL 1800 //1700 to 2600, do not move cursor on y axis if joystick y between these values
#define JOY_Y_UP 2800 //2800 to 4000, move up if greater than 2800

// Screen Resolution (Adjust these to your actual display size)
#define SCREEN_WIDTH 240  // Example: 320 pixels wide
#define SCREEN_HEIGHT 320 // Example: 240 pixels high

/**
 * @brief LVGL UI creation and handler task, focusing on the keyboard.
 */
void lvgl_ui_task1(void *pvParameter)
{
    ESP_LOGI(TAG, "LVGL Keyboard Demo Task started.");
    
    //lvgl_port_lock(0);
    lv_lock();
    init_kb_styles();
    
    lv_obj_t *scr = lv_screen_active();
    lv_obj_add_style(scr, &style_main_bg, 0);
    
    // 1. Create a Text Area (Input Field)
    // This is where the user's input will appear.
    lv_obj_t *ta = lv_textarea_create(scr);
    lv_textarea_set_text_selection(ta, true);
    
    // Set the size and position of the text area
    lv_obj_set_width(ta, lv_pct(90));
    lv_obj_set_height(ta, LV_SIZE_CONTENT);
    lv_obj_align(ta, LV_ALIGN_TOP_MID, 0, 10);
    
    lv_textarea_set_placeholder_text(ta, "Tap here to type...");
    lv_textarea_set_one_line(ta, true); // Keep it simple
    
    // 2. Create the Keyboard
    // This creates the virtual keyboard widget.
    lv_obj_t *kb = lv_keyboard_create(scr);
    
    // Set the size: full width and align it to the bottom
    lv_obj_set_width(kb, LV_PCT(100));
    lv_obj_set_height(kb, lv_pct(50));
    lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0);
    
    // 3. Link the Keyboard to the Text Area
    // This tells the keyboard where to send its input.
    lv_keyboard_set_textarea(kb, ta);
    
    // 4. Set Initial Focus
    // Set the text area as the initially focused object so the keyboard is ready.
    lv_obj_add_state(ta, LV_STATE_FOCUSED);
    
    // Global handle for the cursor object (declared outside of the function)
    g_cursor_obj = NULL;
    
    // --- Cursor Style ---
    lv_style_t style_cursor;
    lv_style_init(&style_cursor);
    lv_style_set_bg_color(&style_cursor, lv_color_white()); // Small white circle
    lv_style_set_border_width(&style_cursor, 2);
    lv_style_set_border_color(&style_cursor, lv_color_black()); // Black border
    lv_style_set_radius(&style_cursor, LV_RADIUS_CIRCLE); // Makes it a perfect circle
    lv_style_set_pad_all(&style_cursor, 0); // No padding
    
    // --- Cursor Object Creation ---
    g_cursor_obj = lv_obj_create(scr); // Create as a simple object on the screen
    lv_obj_set_size(g_cursor_obj, 10, 10); // Set size (e.g., 10x10 pixels)
    lv_obj_add_style(g_cursor_obj, &style_cursor, 0);
    lv_obj_set_pos(g_cursor_obj, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); // Start at the center
    lv_obj_move_foreground(g_cursor_obj); // Ensures the cursor is drawn on top
    
    
    //lvgl_port_unlock();
    lv_unlock();
    
    //vTaskDelete(NULL);
    
    // LVGL Main Loop
    while(1) {
        lv_lock();
        lv_timer_handler();
        lv_unlock();
        
        vTaskDelay(pdMS_TO_TICKS(5));
    }
    
    // LVGL Main Loop
    // LVGL Main Loop
    //while(1) {
    /*
     // --- 1. Read Joystick Position Safely (RAW VALUES) ---
     int raw_x = g_joystick_x; // Initialize with current global value
     int raw_y = g_joystick_y;
     
     // Safely retrieve the latest global values
     if (xSemaphoreTake(joystick_mutex, portMAX_DELAY) == pdTRUE) {
     raw_x = g_joystick_x;
     raw_y = g_joystick_y;
     xSemaphoreGive(joystick_mutex);
     }
     
     // --- 2. Rate Control Logic (Analog to Digital Step) ---
     int move_x = 0;
     int move_y = 0;
     
     // --- X-Axis (Horizontal) ---
     if (raw_x < JOY_X_LEFT) {
     move_x = -1; // Move Left
     } else if (raw_x > JOY_X_RIGHT) {
     move_x = 1;  // Move Right
     }
     
     // --- Y-Axis (Vertical) ---
     // Note: LVGL Y=0 is TOP. Y_UP (high ADC) should move cursor toward Y=0 (move_y = -1).
     if (raw_y > JOY_Y_UP) {
     move_y = -1; // Move UP (towards Y=0)
     } else if (raw_y < JOY_Y_DOWN) {
     move_y = 1;  // Move DOWN (towards Y=HEIGHT)
     }
     
     // --- 3. Acquire the LVGL Lock, Update Position, and Handle Tick ---
     lvgl_port_lock(0);
     
     if (g_cursor_obj != NULL) {
     
     // Check if movement is needed
     if (move_x != 0 || move_y != 0) {
     
     // Update the cursor's internal tracked position
     g_cursor_x_pos += move_x;
     g_cursor_y_pos += move_y;
     
     // Clamp position to screen boundaries (ensures cursor stays visible)
     const int cursor_center_offset = 5; // For a 10x10 cursor
     if (g_cursor_x_pos < cursor_center_offset) g_cursor_x_pos = cursor_center_offset;
     if (g_cursor_x_pos > SCREEN_WIDTH - cursor_center_offset) g_cursor_x_pos = SCREEN_WIDTH - cursor_center_offset;
     if (g_cursor_y_pos < cursor_center_offset) g_cursor_y_pos = cursor_center_offset;
     if (g_cursor_y_pos > SCREEN_HEIGHT - cursor_center_offset) g_cursor_y_pos = SCREEN_HEIGHT - cursor_center_offset;
     
     // Set the LVGL object position
     lv_obj_set_pos(g_cursor_obj, g_cursor_x_pos - cursor_center_offset, g_cursor_y_pos - cursor_center_offset);
     }
     }
     
     lv_timer_handler(); // Handle the LVGL tick
     
     lvgl_port_unlock();
     
     // Control cursor speed by yielding (5ms delay = 200 updates per second)
     vTaskDelay(pdMS_TO_TICKS(5));*/
    
    //lv_timer_handler();
    //vTaskDelay(pdMS_TO_TICKS(5));
    //}
    
}



void joystick_task(void *pvParameter)
{
    // --- 1. ADC Unit Initialization ---
    adc_oneshot_unit_init_cfg_t init_config = {
        .unit_id = ADC_UNIT_1, // Use ADC1
        .clk_src = ADC_DIGI_CLK_SRC_DEFAULT,
        .ulp_mode = ADC_ULP_MODE_DISABLE,
    };
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc1_handle));
    
    // --- 2. ADC Channel Configuration (X-Axis: GPIO4) ---
    adc_oneshot_chan_cfg_t chan_config_x = {
        .atten = ADC_ATTEN_DB_11, // Attenuation for 0V ~ 3.3V range
        .bitwidth = ADC_BITWIDTH_DEFAULT, // Default is 12-bit (0-4095)
    };
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, JOY_X_PIN, &chan_config_x));
    
    // --- 3. ADC Channel Configuration (Y-Axis: GPIO5) ---
    adc_oneshot_chan_cfg_t chan_config_y = {
        .atten = ADC_ATTEN_DB_11, // Same attenuation
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, JOY_Y_PIN, &chan_config_y));
    
    joystick_mutex = xSemaphoreCreateMutex();
    
    while (1) {
        int adc_raw_x = 0;
        int adc_raw_y = 0;
        
        // Read the joystick values
        // Note: Check for ESP_OK error return here for production code
        adc_oneshot_read(adc1_handle, JOY_X_PIN, &adc_raw_x);
        adc_oneshot_read(adc1_handle, JOY_Y_PIN, &adc_raw_y);
        
        // =======================================================
        // ✨ NEW LINE ADDED HERE TO GENERATE LOG OUTPUT
        // =======================================================
        ESP_LOGI(TAG, "X-Axis Raw: %d | Y-Axis Raw: %d", adc_raw_x, adc_raw_y);
        // =======================================================
        
        // Safely update global variables
        if (xSemaphoreTake(joystick_mutex, portMAX_DELAY) == pdTRUE) {
            g_joystick_x = adc_raw_x;
            g_joystick_y = adc_raw_y;
            xSemaphoreGive(joystick_mutex);
        }
        
        // Delay to prevent the task from consuming too much CPU time
        vTaskDelay(pdMS_TO_TICKS(50)); // Read the joystick every 50ms
    }
}

void initializeUIST7789(void) {
    // 1. Initialize SPI Bus
    const spi_bus_config_t bus_config = {
        .mosi_io_num = PIN_NUM_MOSI,
        .miso_io_num = -1,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 0, // Set to 0 to allow max size transfer
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO));
    
    // 2. Initialize LCD Panel I/O (Interface to the ST7789)
    const esp_lcd_panel_io_spi_config_t io_config = {
        .cs_gpio_num = PIN_NUM_CS,
        .dc_gpio_num = PIN_NUM_DC,
        .spi_mode = 0,
        .lcd_cmd_bits = 8,
        .lcd_param_bits = 8,
        .trans_queue_depth = 10,
        .on_color_trans_done = NULL,
        .pclk_hz = LCD_PIXEL_CLOCK_HZ, // Corrected clock field name
    };
    esp_lcd_panel_io_handle_t io_handle = NULL;
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &io_handle));
    
    // 3. Initialize LCD Driver
    const esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = PIN_NUM_RST,
        .bits_per_pixel = 16,
        .color_space = ESP_LCD_COLOR_SPACE_BGR,
    };
    
    const size_t ili9488_buffer_size = 0;
    // Conditional compilation for the driver initialization
#if defined(USE_ILI9488)
    ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(io_handle, &panel_config, ili9488_buffer_size, &panel_handle));
    
#elif defined(USE_ST7789)
    // The ST7789 function does not take the buffer_size argument
    ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
#endif
    
    // 4. Panel Initialization (Reset and basic setup commands)
    esp_lcd_panel_reset(panel_handle);
    esp_lcd_panel_init(panel_handle);
    esp_lcd_panel_swap_xy(panel_handle, true); // Often needed for orientation
    esp_lcd_panel_set_gap(panel_handle, 0, 0);
#if defined(USE_ST7789)
    esp_lcd_panel_invert_color(panel_handle, true);
#endif
    esp_lcd_panel_disp_on_off(panel_handle, true);
    
    
#define LVGL_DISP_BUF_SIZE  (LCD_H_RES * LCD_V_RES / 10) // E.g., 1/10th of screen
    
    // 1. Initialize LVGL Core
    const lvgl_port_cfg_t lvgl_cfg = ESP_LVGL_PORT_INIT_CONFIG();
    // Use the port init function to set up the LVGL task and tick source
    lvgl_port_init(&lvgl_cfg);
    
    // 2. Register Display with LVGL Porting Layer
    const lvgl_port_display_cfg_t disp_cfg = {
        .io_handle = io_handle,
        .panel_handle = panel_handle,
        .control_handle = NULL, // Generally unused for basic SPI display
        
        // --- Memory Configuration ---
        // LVGL will allocate the buffer internally based on these fields.
        .buffer_size = LVGL_DISP_BUF_SIZE,
        .double_buffer = true, // We will allocate two buffers of the size above
        .trans_size = 0,       // Optional: Set to 0 if not using a dedicated transaction buffer
        
        // --- Flags (Crucial for Byte Ordering) ---
        .flags.buff_dma = 1,
        // Set this flag to 1 (true) to swap the high and low bytes of the 16-bit color.
        // This often resolves the "fuzziness" and color mixing issues on SPI displays.
        
#if defined(USE_ILI9488)
        .flags.swap_bytes = 0,
#elif defined(USE_ST7789)
        .flags.swap_bytes = 1,
#endif
        
        // --- Display Resolution ---
        .hres = LCD_V_RES,
        .vres = LCD_H_RES,
        .monochrome = false,
        
        // --- LVGL v9 Configuration ---
        .color_format = LV_COLOR_FORMAT_RGB565, // Use RGB565 for 16-bit color
        
        // --- Flags (Crucial for DMA and memory location) ---
        .flags.buff_dma = 1,    // 1: Allocate buffers in DMA-capable memory (DRAM)
        .flags.buff_spiram = 0, // 0: Do NOT allocate in PSRAM
        .flags.sw_rotate = 0,   // 0: Use hardware/driver rotation (faster)
        .flags.full_refresh = 0, // 0: Use partial refresh (more efficient)
        .flags.direct_mode = 0, // 0: Use standard buffered mode
        
        .rotation = {0}, // Use default rotation (0 degrees)
    };
    
    // Use the registration function to wire it up
    lv_display_t *disp = lvgl_port_add_disp(&disp_cfg);
    
    
    
    // 3. Start your LVGL UI Task
    xTaskCreate(lvgl_ui_task1, "LVGL_UI_Task", 8192, NULL, 5, NULL);
}

#include "driver/i2c_master.h" // <-- Use this new header

// Make sure these are defined correctly for your board
#define I2C_MASTER_SCL_IO   PIN_NUM_I2C_SCL
#define I2C_MASTER_SDA_IO   PIN_NUM_I2C_SDA
#define I2C_MASTER_NUM      I2C_HOST // Use the same I2C port

/**
 * @brief i2c master scanner (using new "next-gen" driver)
 */
static void i2c_scan_ng(void)
{
    ESP_LOGI(TAG, "Starting I2C scan (NG Driver)...");
    
    // 1. Create the bus configuration
    i2c_master_bus_config_t bus_cfg = {
        .i2c_port = I2C_MASTER_NUM,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,
    };
    i2c_master_bus_handle_t bus_handle;
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus_handle));
    
    ESP_LOGI(TAG, "Scanning for I2C devices...");
    
    for (uint8_t address = 1; address < 127; address++) {
        // 2. Probe the address. This function sends the address and checks for an ACK.
        // 50ms timeout is more than enough.
        esp_err_t ret = i2c_master_probe(bus_handle, address, 50);
        
        if (ret == ESP_OK) {
            ESP_LOGI(TAG, "I2C device found at address 0x%02X", address);
        } else if (ret != ESP_ERR_TIMEOUT && ret != ESP_ERR_NOT_FOUND) {
            // Log other errors if they occur
            ESP_LOGW(TAG, "Error 0x%X checking address 0x%02X", ret, address);
        }
    }
    
    // 3. Clean up the bus
    ESP_ERROR_CHECK(i2c_del_master_bus(bus_handle));
    ESP_LOGI(TAG, "I2C scan complete.");
}



/**
 * @brief GPIO Interrupt Service Routine
 * This is called *every* time the INT_N_PIN goes LOW.
 */
static void IRAM_ATTR gpio_isr_handler(void *arg) {
    uint32_t gpio_num = (uint32_t)arg;
    if (gpio_num == INT_N_PIN) {
        // Send an event to the touch task
        // A non-zero value is posted to wake up the task
        static uint32_t event = 1;
        xQueueSendFromISR(touch_intr_queue, &event, NULL);
    }
}

/**
 * @brief Task to process touch events
 * This task waits for an event from the ISR queue.
 */
static void touch_task(void *arg) {
    uint32_t event;
    FT6336U_TouchPointType touch_data;
    
    while (1) {
        // Wait for an interrupt event from the queue
        if (xQueueReceive(touch_intr_queue, &event, portMAX_DELAY)) {
            
            // Interrupt received, scan the touch controller
            esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
            if (ret != ESP_OK) {
                ESP_LOGE(TAG, "Failed to scan touch: %s", esp_err_to_name(ret));
                continue;
            }
            
            // --- LVGL BRIDGE: UPDATE STATE ---
            // Instead of logging, we now update the static variables
            if (touch_data.touch_count == 0) {
                g_last_state = LV_INDEV_STATE_REL; // Released
            } else {
                // Use the first touch point
                g_last_x = touch_data.tp[0].x;
                g_last_y = touch_data.tp[0].y;
                g_last_state = LV_INDEV_STATE_PR; // Pressed
            }
            // --- END LVGL BRIDGE ---
            
            if (touch_data.touch_count == 0) {
                // ESP_LOGI(TAG, "Touch Released");
            } else {
                for (int i = 0; i < touch_data.touch_count; i++) {
                    ESP_LOGI(TAG, "Touch %d: (%s) X=%u, Y=%u",
                             i,
                             (touch_data.tp[i].status == touch) ? "NEW" : "STREAM",
                             touch_data.tp[i].x,
                             touch_data.tp[i].y);
                }
            }
        }
    }
}

/**
 * @brief Initialize the I2C master bus
 */
/*esp_err_t i2c_master_init(void) {
 i2c_master_bus_config_t i2c_bus_config = {
 .i2c_port = I2C_HOST,
 .sda_io_num = I2C_SDA_PIN,
 .scl_io_num = I2C_SCL_PIN,
 .clk_source = I2C_CLK_SRC_DEFAULT,
 .glitch_ignore_cnt = 7, // Corrected: Was .glitch_ignore_cfg.flags.val = 0
 .intr_priority = 0,     // Use 0 for default priority
 .flags.enable_internal_pullup = true // Enable internal pullups
 // Removed non-existent fields:
 // .scl_pulse_period_us
 // .sda_pulse_period_us
 // .intr_flags
 };
 
 esp_err_t ret = i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
 if (ret != ESP_OK) {
 ESP_LOGE(TAG, "Failed to create I2C master bus: %s", esp_err_to_name(ret));
 }
 return ret;
 }
 */

/**
 * @brief Initializes just the FT6336U chip
 */
esp_err_t ft6336u_driver_init(void) {
    // 1. Initialize I2C Bus
    ESP_ERROR_CHECK(i2c_master_init());
    
    // 2. Initialize FT6336U Driver
    ESP_ERROR_CHECK(ft6336u_init(&touch_dev, i2c_bus_handle, RST_N_PIN, INT_N_PIN));
    
    touch_intr_queue = xQueueCreate(10, sizeof(uint32_t));
    
    ESP_LOGI(TAG, "Touch driver initialized successfully.");
    
    // 5. Configure GPIO interrupt for INT_N_PIN
    // We do this *after* the driver is init (which configures the pin)
    // We re-configure to add the interrupt
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << INT_N_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE, // Trigger on FALLING edge
    };
    ESP_ERROR_CHECK(gpio_config(&io_conf));
    
    // 6. Install GPIO ISR service and add handler
    ESP_ERROR_CHECK(gpio_install_isr_service(0)); // 0 = default flags
    ESP_ERROR_CHECK(gpio_isr_handler_add(INT_N_PIN, gpio_isr_handler, (void *)INT_N_PIN));
    
    ESP_LOGI(TAG, "Interrupt handler installed. Ready for touch.");
    return ESP_OK;
}

/* * This function should be called ONCE during your setup,
 * right after lv_init() and after your display driver is registered.
 */
void lvgl_touch_driver_init(void) {
    // 1. Create a new input device
    lv_indev_t *indev_touchpad = lv_indev_create();
    
    // 2. Set its type to "Pointer" (e.g., a mouse or touchscreen)
    lv_indev_set_type(indev_touchpad, LV_INDEV_TYPE_POINTER);
    
    // 3. THIS IS THE CRITICAL LINE:
    //    Register your C function (lvgl_touch_read_cb) as the
    //    official callback that LVGL will use to read input.
    lv_indev_set_read_cb(indev_touchpad, lvgl_touch_read_cb);
}

void initTouch(void) {
    ESP_LOGI(TAG, "Starting FT6336U Touch Example");
    
    // 1. Initialize I2C Bus
    ESP_ERROR_CHECK(i2c_master_init());
    
    // 2. Initialize FT6336U Driver
    // We pass the bus handle, and the driver adds itself as a device
    ESP_ERROR_CHECK(ft6336u_init(&touch_dev, i2c_bus_handle, RST_N_PIN, INT_N_PIN));
    
    ESP_LOGI(TAG, "Touch driver initialized successfully.");
    
    // 3. Create the interrupt queue
    touch_intr_queue = xQueueCreate(10, sizeof(uint32_t));
    
    // 4. Create the touch processing task
    xTaskCreate(touch_task, "touch_task", 4096, NULL, 5, NULL);
    
    // 5. Configure GPIO interrupt for INT_N_PIN
    // We do this *after* the driver is init (which configures the pin)
    // We re-configure to add the interrupt
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << INT_N_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE, // Trigger on FALLING edge
    };
    ESP_ERROR_CHECK(gpio_config(&io_conf));
    
    // 6. Install GPIO ISR service and add handler
    ESP_ERROR_CHECK(gpio_install_isr_service(0)); // 0 = default flags
    ESP_ERROR_CHECK(gpio_isr_handler_add(INT_N_PIN, gpio_isr_handler, (void *)INT_N_PIN));
    
    ESP_LOGI(TAG, "Interrupt handler installed. Ready for touch.");
}

/*void testCPUGraphicsBenchmark(void) {
    // 1. Allocate Framebuffer in PSRAM (Slow access) or SRAM (Fast access)
    // For benchmarking math, SRAM is better, but for full frame, you likely need PSRAM.
    int width = 320;
    int height = 240;
    size_t fbSize = width * height * 3;
    
    uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(fbSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    
    if (!display_buffer) {
        ESP_LOGE(TAG, "Failed to allocate memory");
        return;
    }

    Framebuffer fb = {
        .displayWidth = width,
        .displayHeight = height,
        .pixelData = display_buffer,
        .colorMode = COLOR_MODE_BGR888
    };

    Gradient grad;
        grad.angle = 45.0f * (M_PI / 180.0f);
        grad.type = GRADIENT_TYPE_LINEAR; // Initialize the new enum field
        grad.numStops = 2;

        // 1. Create the storage for the stops (on the stack)
        ColorStop stops_storage[2];

        // 2. Point the struct pointer to this storage
        grad.stops = stops_storage;

        // 3. Now it is safe to assign values
        grad.stops[0].color = (ColorRGBA){255, 0, 0, 255};
        grad.stops[0].position = 0.0f;
        
        grad.stops[1].color = (ColorRGBA){0, 0, 255, 255};
        grad.stops[1].position = 1.0f;

    // --- BENCHMARK 1: ORIGINAL (Float) ---
    // Warm up cache
    fillRectangleWithGradientExtended(&fb, 0, 0, width, height, 0, 0, width, height, &grad, NULL);
    
    int64_t t1 = esp_timer_get_time();
    fillRectangleWithGradientExtended(&fb, 0, 0, width, height, 0, 0, width, height, &grad, NULL);
    int64_t t2 = esp_timer_get_time();
    
    ESP_LOGI(TAG, "Original (Float) Time: %lld us", (t2 - t1));

    // --- BENCHMARK 2: SIMD (Fixed Point) ---
    // Warm up cache
    fillRectangleWithGradientSIMD(&fb, 0, 0, width, height, &grad);

    int64_t t3 = esp_timer_get_time();
    fillRectangleWithGradientSIMD(&fb, 0, 0, width, height, &grad);
    int64_t t4 = esp_timer_get_time();

    ESP_LOGI(TAG, "SIMD (Fixed Pt) Time:  %lld us", (t4 - t3));
    ESP_LOGI(TAG, "Speedup Factor:        %.2fx", (float)(t2 - t1) / (float)(t4 - t3));

    free(display_buffer);
}*/


void testCPUGraphics(void) {
    // 2. Allocate the single framebuffer in PSRAM
    int width = DISPLAY_HORIZONTAL_PIXELS;
    int height = DISPLAY_VERTICAL_PIXELS;
    int num_pixels = width * height;
    
    // Allocate Display buffer (BGR888, 3 bytes/pixel)
    // Size = 320 * 480 * 3 bytes = 460,800 bytes
    uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(num_pixels * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    
    if (!display_buffer) {
        ESP_LOGE(TAG, "Failed to allocate display buffer in PSRAM");
        return;
    }
    
    // 3. Initialize the CPUGraphics Framebuffer struct
    Framebuffer fb = {
        .displayWidth = width,
        .displayHeight = height,
        .pixelData = display_buffer,          // Point to our BGR buffer
        .colorMode = COLOR_MODE_BGR888, // Set the direct-write mode
        //.colors = {0}
    };
    
    ESP_LOGI(TAG, "Framebuffer allocated. Starting draw...");
    
    // 4. Perform drawing operations
    
    // "draw a blank framebuffer" (clear to dark blue)
    ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
    clearFramebuffer(&fb, blue);
    
    // "draw a rectangle" (solid red)
    ColorRGBA red = {.r = 255, .g = 0, .b = 0, .a = 255};
    drawRectangleCFramebuffer(&fb, 50, 50, 100, 100, red, true);
    
    // Draw a semi-transparent green rectangle to test blending
    ColorRGBA green_alpha = {.r = 0, .g = 255, .b = 0, .a = 128}; // 50% alpha
    drawRectangleCFramebuffer(&fb, 100, 100, 150, 150, green_alpha, true);
    
    
    ESP_LOGI(TAG, "Drawing complete. Sending to LCD...");
    
    // 5. Send the completed buffer to the LCD
    // No conversion step is needed!
    ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer));
    
    ESP_LOGI(TAG, "Buffer sent. Halting in a loop.");
    
    // 6. Clean up (or loop)
    //while (1) {
    //    vTaskDelay(pdMS_TO_TICKS(1000));
    //}
}




/**
 * @brief This task handles all drawing and screen updates.
 *
 * It runs in an infinite loop, drawing to the framebuffer and
 * sending it to the display, then delaying to feed the WDT.
 */
/*void graphics_task(void *arg)
 {
 ESP_LOGI(TAG, "Graphics task started.");
 
 // --- NEW: INITIALIZE FREETYPE *HERE* ---
 esp_err_t ft_ok = initialize_freetype();
 if (ft_ok != ESP_OK) {
 ESP_LOGE(TAG, "Failed to initialize FreeType, deleting task.");
 vTaskDelete(NULL); // Abort this task
 return;
 }
 // --- END NEW PART ---
 
 ESP_LOGI(TAG, "Graphics task started.");
 
 // 2. Allocate the single framebuffer in PSRAM
 int width = DISPLAY_HORIZONTAL_PIXELS;
 int height = DISPLAY_VERTICAL_PIXELS;
 int num_pixels = width * height;
 
 uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(num_pixels * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
 
 if (!display_buffer) {
 ESP_LOGE(TAG, "Failed to allocate display buffer in PSRAM. Deleting task.");
 vTaskDelete(NULL); // Delete this task
 return;
 }
 ESP_LOGI(TAG, "Framebuffer allocated.");
 
 // 3. Initialize the CPUGraphics Framebuffer struct
 Framebuffer fb = {
 .displayWidth = width,
 .displayHeight = height,
 .pixelData = display_buffer,    // Point to our BGR buffer
 .colorMode = COLOR_MODE_BGR888  // Set the direct-write mode
 };
 
 GraphicsCommand cmd;
 
 // This is now our main application loop
 while (1) {
 ESP_LOGI(TAG, "Starting draw...");
 
 // 4. Perform drawing operations
 
 // Clear to dark blue
 ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
 clearFramebuffer(&fb, blue);
 
 // Draw solid red
 ColorRGBA red = {.r = 255, .g = 0, .b = 0, .a = 255};
 drawRectangleCFramebuffer(&fb, 50, 50, 100, 100, red, true);
 
 // Draw blended green
 ColorRGBA green_alpha = {.r = 0, .g = 255, .b = 0, .a = 128}; // 50% alpha
 drawRectangleCFramebuffer(&fb, 100, 100, 150, 150, green_alpha, true);
 
 // 4. --- DRAW TEXT (This code is the same) ---
 ColorRGBA white = {.r = 255, .g = 255, .b = 255, .a = 255};
 renderText(&fb,         // Your framebuffer
 ft_face,     // The font face we loaded
 "Hello, World!", // The text to draw
 50,          // X position
 300,         // Y position
 white,       // Text color
 24,          // Font size in pixels
 NULL);       // No gradient
 
 ESP_LOGI(TAG, "Drawing complete. Sending to LCD...");
 
 esp_err_t err = esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer);
 if (err != ESP_OK) {
 ESP_LOGE(TAG, "Failed to draw bitmap! Error: %s", esp_err_to_name(err));
 }
 
 // 5. Send the completed buffer to the LCD
 // This is the long-running function
 //ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer));
 
 ESP_LOGI(TAG, "Buffer sent.");
 
 
 // Wait forever until a command arrives
 if (xQueueReceive(g_graphics_queue, &cmd, portMAX_DELAY) == pdTRUE) {
 
 // --- ADD THIS "RECEIVED" LOG ---
 ESP_LOGI(TAG, "Graphics task received command: %d", cmd.cmd);
 
 switch (cmd.cmd) {
 
 // --- Case 1: Draw a Rectangle ---
 case CMD_DRAW_RECT:
 ESP_LOGI(TAG, "Drawing rect to PSRAM");
 drawRectangleCFramebuffer(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color, true);
 break;
 
 // --- Case 2: Draw Text ---
 case CMD_DRAW_TEXT:
 ESP_LOGI(TAG, "Drawing text to PSRAM");
 renderText(&fb, ft_face, cmd.text, cmd.x, cmd.y, white, 24, NULL);
 break;
 
 // --- Case 3: Update the LCD ---
 case CMD_UPDATE_AREA:
 {
 ESP_LOGI(TAG, "Pushing update to LCD");
 
 // 1. Create a small, *contiguous* buffer for the update
 uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(
 cmd.w * cmd.h * 3,
 MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
 if (!temp_buffer) {
 ESP_LOGE(TAG, "Failed to alloc temp_buffer!");
 continue; // Skip this command
 }
 
 // 2. Copy the updated rectangle from PSRAM to the temp_buffer
 uint8_t* psram_ptr;
 uint8_t* temp_ptr = temp_buffer;
 for (int i = 0; i < cmd.h; i++) {
 psram_ptr = &((uint8_t*)fb.pixelData)[((cmd.y + i) * fb.displayWidth + cmd.x) * 3];
 memcpy(temp_ptr, psram_ptr, cmd.w * 3);
 temp_ptr += cmd.w * 3;
 }
 
 // 3. Send *only the small temp_buffer* to the LCD
 ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
 cmd.x, cmd.y,           // x_start, y_start
 cmd.x + cmd.w, cmd.y + cmd.h, // x_end, y_end
 temp_buffer));
 
 // 4. Free the temp buffer
 heap_caps_free(temp_buffer);
 break;
 }
 }
 }
 
 
 // 6. *** THE FIX ***
 // Delay for 10 milliseconds.
 // This feeds the Task Watchdog and allows other tasks to run.
 vTaskDelay(pdMS_TO_TICKS(100));
 }
 }*/

static const char *TAG_PNG = "PNG_DECODER";

// --- Global variables to manage PNG decoding (File and Buffer) ---
// Note: We use static variables to hold state during the png_read_data callback
// Since this is a file I/O operation, it is generally okay, but be aware of thread safety
// if you were loading multiple images simultaneously (which requires per-texture state).
static FILE *png_file_handle = NULL;
static uint8_t *png_load_buffer = NULL; // Holds the PSRAM buffer start address
static int png_load_width = 0;
static int png_load_height = 0;


// =================================================================
// PNG DECODING CALLBACKS
// =================================================================

/**
 * @brief PNG file read callback function for libpng.
 * @param png_ptr The internal PNG structure pointer.
 * @param out_data Pointer to the destination buffer (libpng's internal buffer).
 * @param length The number of bytes to read.
 */
static void png_read_data(png_structp png_ptr, png_bytep out_data, png_size_t length) {
    if (png_file_handle) {
        if (fread(out_data, 1, length, png_file_handle) != length) {
            png_error(png_ptr, "Error reading file data");
        }
    } else {
        png_error(png_ptr, "File handle is NULL");
    }
}

/**
 * @brief Custom warning handler for libpng.
 */
static void png_warning_handler(png_structp png_ptr, png_const_charp warning_message) {
    ESP_LOGW(TAG_PNG, "PNG Warning: %s", warning_message);
}

/**
 * @brief Custom error handler for libpng.
 */
static void png_error_handler(png_structp png_ptr, png_const_charp error_message) {
    ESP_LOGE(TAG_PNG, "PNG Error: %s", error_message);
    // Setting longjmp is dangerous in ESP-IDF (may bypass stack unwinding)
    // We rely on the caller checking the texture data for NULL/size.
}

typedef enum {
    IMAGE_TYPE_UNKNOWN = 0,
    IMAGE_TYPE_PNG,
    IMAGE_TYPE_JPEG
} ImageFileType;

ImageFileType get_image_type_from_path(const char* path) {
    if (!path) return IMAGE_TYPE_UNKNOWN;
    
    // Find the last dot '.' in the string
    const char *dot = strrchr(path, '.');
    
    // If no dot, or dot is the first character, it's not a valid extension
    if (!dot || dot == path) return IMAGE_TYPE_UNKNOWN;
    
    // Compare case-insensitive (handles .PNG, .png, .Png)
    if (strcasecmp(dot, ".png") == 0) {
        return IMAGE_TYPE_PNG;
    }
    if (strcasecmp(dot, ".jpg") == 0 || strcasecmp(dot, ".jpeg") == 0) {
        return IMAGE_TYPE_JPEG;
    }
    
    return IMAGE_TYPE_UNKNOWN;
}

// =================================================================
// MAIN LOADING FUNCTION
// =================================================================

ImageTexture* load_image_from_file(const char* imgPath) {
    
    // Reset global state for this decode operation
    png_file_handle = NULL;
    png_load_buffer = NULL;
    
    // 1. Open the file via VFS
    png_file_handle = fopen(imgPath, "rb");
    if (!png_file_handle) {
        ESP_LOGE(TAG_PNG, "Failed to open image file: %s", imgPath);
        return NULL;
    }
    
    // --- Check PNG Signature ---
    uint8_t header[8];
    if (fread(header, 1, 8, png_file_handle) != 8 || png_sig_cmp(header, 0, 8)) {
        ESP_LOGE(TAG_PNG, "File is not a valid PNG signature.");
        fclose(png_file_handle);
        return NULL;
    }
    
    // 2. Setup libpng structures
    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, png_error_handler, png_warning_handler);
    if (!png_ptr) {
        ESP_LOGE(TAG_PNG, "Failed to create PNG read structure.");
        fclose(png_file_handle);
        return NULL;
    }
    
    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        ESP_LOGE(TAG_PNG, "Failed to create PNG info structure.");
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        fclose(png_file_handle);
        return NULL;
    }
    
    // 3. Set custom read function and read info
    png_set_read_fn(png_ptr, NULL, png_read_data);
    png_set_sig_bytes(png_ptr, 8); // Already read 8 bytes of header
    png_read_info(png_ptr, info_ptr);
    
    // 4. Get image properties
    png_uint_32 width = png_get_image_width(png_ptr, info_ptr);
    png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
    int bit_depth = png_get_bit_depth(png_ptr, info_ptr);
    int color_type = png_get_color_type(png_ptr, info_ptr);
    
    //ESP_LOGI(TAG_PNG, "Image found: %lu x %lu, Depth: %d, Type: %d", width, height, bit_depth, color_type);
    
    // 5. Transform image data for consistent output (32-bit RGBA)
    
    // Convert 16-bit to 8-bit (if needed)
    if (bit_depth == 16) {
        png_set_strip_16(png_ptr);
    }
    // Convert palette to RGB
    if (color_type == PNG_COLOR_TYPE_PALETTE) {
        png_set_palette_to_rgb(png_ptr);
    }
    // Convert transparency (tRGB, tALPHA) to full alpha channel
    if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
        png_set_tRNS_to_alpha(png_ptr);
    }
    // Convert grayscale to RGB (for consistency)
    if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
        png_set_gray_to_rgb(png_ptr);
    }
    // Ensure we have an alpha channel (for transparency and blending)
    if (color_type != PNG_COLOR_TYPE_RGBA && color_type != PNG_COLOR_TYPE_RGB_ALPHA) {
        png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
    }
    
    png_set_bgr(png_ptr);
    // Update the image info after all transformations
    png_read_update_info(png_ptr, info_ptr);
    
    // Final check for 4 bytes per pixel (RGBA)
    size_t row_bytes = png_get_rowbytes(png_ptr, info_ptr);
    if (row_bytes != (width * 4)) {
        ESP_LOGE(TAG_PNG, "Final row size mismatch. Expected %lu, got %lu.", width * 4, row_bytes);
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(png_file_handle);
        return NULL;
    }
    
    // 6. Allocate buffer in PSRAM (Critical step!)
    png_load_width = width;
    png_load_height = height;
    size_t data_size = width * height * 4; // 4 bytes/pixel (RGBA)
    
    png_load_buffer = (uint8_t*)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM);
    if (!png_load_buffer) {
        ESP_LOGE(TAG_PNG, "Failed to allocate %u bytes in PSRAM for texture.", data_size);
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(png_file_handle);
        return NULL;
    }
    
    // 7. Read image data row by row
    png_bytep* row_pointers = (png_bytep*)cc_safe_alloc(1, height * sizeof(png_bytep));
    if (!row_pointers) {
        ESP_LOGE(TAG_PNG, "Failed to allocate row pointers.");
        heap_caps_free(png_load_buffer);
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(png_file_handle);
        return NULL;
    }
    
    // Set pointers to rows within the single PSRAM buffer
    for (int y_pos = 0; y_pos < height; y_pos++) {
        row_pointers[y_pos] = png_load_buffer + y_pos * row_bytes;
    }
    
    png_read_image(png_ptr, row_pointers);
    
    // 8. Cleanup and return ImageTexture
    ImageTexture* new_texture = (ImageTexture*)cc_safe_alloc(1, sizeof(ImageTexture));
    if (new_texture) {
        new_texture->width = width;
        new_texture->height = height;
        // Assign the PSRAM buffer to the ImageTexture struct
        new_texture->data = (ColorRGBA*)png_load_buffer;
    } else {
        heap_caps_free(png_load_buffer);
    }
    
    // Final libpng cleanup
    free(row_pointers);
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fclose(png_file_handle);
    
    //ESP_LOGI(TAG_PNG, "PNG file loaded and decoded to PSRAM successfully.");
    return new_texture;
}

static const char *TAG_JPG = "JPG_DECODER";

ImageTexture* load_jpeg_from_file(const char* imgPath) {
    ESP_LOGI(TAG_JPG, "Loading JPEG: %s", imgPath);
    
    // 1. Open File
    FILE *f = fopen(imgPath, "rb");
    if (!f) {
        ESP_LOGE(TAG_JPG, "Failed to open file: %s", imgPath);
        return NULL;
    }
    
    fseek(f, 0, SEEK_END);
    size_t file_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    
    ESP_LOGI(TAG_JPG, "File Size: %d bytes", file_size); // <--- DEBUG 1
    
    if (file_size == 0) {
        ESP_LOGE(TAG_JPG, "File size is 0");
        fclose(f);
        return NULL;
    }
    
    // Allocate Input Buffer in PSRAM
    uint8_t *jpeg_input_buf = (uint8_t *)heap_caps_malloc(file_size, MALLOC_CAP_SPIRAM);
    if (!jpeg_input_buf) {
        ESP_LOGE(TAG_JPG, "Failed to alloc input buffer");
        fclose(f);
        return NULL;
    }
    
    size_t bytes_read = fread(jpeg_input_buf, 1, file_size, f);
    fclose(f);
    
    ESP_LOGI(TAG_JPG, "Bytes Read: %d", bytes_read); // <--- DEBUG 2
    
    // --- DEBUG 3: PRINT HEADER BYTES ---
    if (bytes_read > 4) {
        ESP_LOGI(TAG_JPG, "Header Bytes: %02X %02X %02X %02X",
                 jpeg_input_buf[0], jpeg_input_buf[1], jpeg_input_buf[2], jpeg_input_buf[3]);
    }
    
    // Valid JPEG should start with: FF D8 FF ...
    
    // 2. Configure Decoder
    esp_jpeg_image_cfg_t jpeg_cfg = {
        .indata = jpeg_input_buf,
        .indata_size = bytes_read, // Use actual bytes read
        .out_format = JPEG_IMAGE_FORMAT_RGB888,
        .out_scale = JPEG_IMAGE_SCALE_0,
        .flags = { .swap_color_bytes = 0 }
    };
    
    esp_jpeg_image_output_t info;
    
    // Get width/height without decoding the whole thing yet
    esp_err_t ret = esp_jpeg_get_image_info(&jpeg_cfg, &info);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG_JPG, "Failed to parse JPEG header");
        heap_caps_free(jpeg_input_buf);
        return NULL;
    }
    
    int w = info.width;
    int h = info.height;
    ESP_LOGI(TAG_JPG, "JPEG Info: %d x %d", w, h);
    
    // --- 3. Allocate Temporary RGB Output Buffer ---
    // The decoder needs a place to dump the 3-byte RGB data
    size_t rgb_buf_size = w * h * 3;
    uint8_t *temp_rgb_buf = (uint8_t *)heap_caps_malloc(rgb_buf_size, MALLOC_CAP_SPIRAM);
    
    if (!temp_rgb_buf) {
        ESP_LOGE(TAG_JPG, "Failed to alloc temp RGB buffer");
        heap_caps_free(jpeg_input_buf);
        return NULL;
    }
    
    // Setup config for full decode
    jpeg_cfg.outbuf = temp_rgb_buf;
    jpeg_cfg.outbuf_size = rgb_buf_size;
    
    // --- 4. Decode ---
    ret = esp_jpeg_decode(&jpeg_cfg, &info);
    
    // We are done with the compressed input file now
    heap_caps_free(jpeg_input_buf);
    
    if (ret != ESP_OK) {
        ESP_LOGE(TAG_JPG, "Failed to decode JPEG");
        heap_caps_free(temp_rgb_buf);
        return NULL;
    }
    
    // --- 5. Convert to ImageTexture (RGBA) ---
    // We need to expand 3-byte RGB to 4-byte RGBA and handle the R/B swap
    // for your display driver logic.
    
    ImageTexture* new_texture = (ImageTexture*)cc_safe_alloc(1, sizeof(ImageTexture));
    if (!new_texture) {
        heap_caps_free(temp_rgb_buf);
        return NULL;
    }
    
    new_texture->width = w;
    new_texture->height = h;
    
    size_t rgba_size = w * h * sizeof(ColorRGBA);
    new_texture->data = (ColorRGBA*)heap_caps_malloc(rgba_size, MALLOC_CAP_SPIRAM);
    
    if (!new_texture->data) {
        free(new_texture);
        heap_caps_free(temp_rgb_buf);
        return NULL;
    }
    
    uint8_t *src = temp_rgb_buf;
    ColorRGBA *dst = new_texture->data;
    int num_pixels = w * h;
    
    for (int i = 0; i < num_pixels; i++) {
        // Read RGB from decoder output
        uint8_t r = *src++;
        uint8_t g = *src++;
        uint8_t b = *src++;
        
        // Write RGBA to texture (Perform R/B swap for your display)
        // Your display expects BGR logic, so we store B in Red, R in Blue
        dst->r = b;
        dst->g = g;
        dst->b = r;
        dst->a = 255; // JPEGs are opaque
        
        dst++;
    }
    
    // Free the temporary 3-byte buffer
    heap_caps_free(temp_rgb_buf);
    
    ESP_LOGI(TAG_JPG, "JPEG Loaded and Converted.");
    return new_texture;
}

void updateArea(Framebuffer fb, GraphicsCommand cmd) {
    ESP_LOGI(TAG, "Pushing update to LCD (chunked)...");
    
    // Define a chunk size (e.g., 10 rows at a time)
    int rows_per_chunk = 10;
    
    // Calculate the size of our small, safe DMA buffer
    size_t chunk_size_bytes = cmd.w * rows_per_chunk * 3;
    
    // 1. Create the small, reusable DMA buffer
    uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(
                                                       chunk_size_bytes,
                                                       MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
    if (!temp_buffer) {
        ESP_LOGE(TAG, "Failed to alloc temp_buffer for chunking!");
        return; // Skip this command
    }
    
    // 2. Loop over the update area in chunks
    for (int y_chunk = 0; y_chunk < cmd.h; y_chunk += rows_per_chunk) {
        
        // Calculate how many rows to send in this specific chunk
        int rows_to_send = cmd.h - y_chunk;
        if (rows_to_send > rows_per_chunk) {
            rows_to_send = rows_per_chunk;
        }
        
        // Calculate the start/end coordinates for this chunk
        int chunk_y_start = cmd.y + y_chunk;
        int chunk_y_end = chunk_y_start + rows_to_send;
        
        // 3. Copy this chunk from PSRAM to our internal temp_buffer
        uint8_t* psram_ptr;
        uint8_t* temp_ptr = temp_buffer;
        for (int i = 0; i < rows_to_send; i++) {
            psram_ptr = &((uint8_t*)fb.pixelData)[((chunk_y_start + i) * fb.displayWidth + cmd.x) * 3];
            memcpy(temp_ptr, psram_ptr, cmd.w * 3);
            temp_ptr += cmd.w * 3;
        }
        
        // 4. Send *only this small chunk* to the LCD
        ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
                                                  cmd.x, chunk_y_start,   // x_start, y_start
                                                  cmd.x + cmd.w, chunk_y_end, // x_end, y_end
                                                  temp_buffer));
    }
    
    // 5. Free the temp buffer
    heap_caps_free(temp_buffer);
    
    touchEnabled = true;
}

// Validated for: Framebuffer = BGR888 (3-byte), ILI9488 (3-byte SPI)

/*void updateArea(Framebuffer fb, GraphicsCommand cmd) {
 // 1. Configuration
 int bpp = 3; // BGR888 = 3 Bytes Per Pixel
 int rows_per_chunk = 20; // Safe chunk size (adjust if needed)
 
 // 2. Allocate the DMA Buffer ONCE
 // Size = Width * Lines * 3 bytes
 size_t chunk_size = cmd.w * rows_per_chunk * bpp;
 
 // Using INTERNAL | DMA memory is strictly required for ESP32 SPI DMA
 uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(chunk_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
 
 if (!temp_buffer) {
 ESP_LOGE("LCD", "Failed to allocate DMA buffer for updateArea");
 return;
 }
 
 // 3. Process the Area in Chunks
 for (int y_chunk = 0; y_chunk < cmd.h; y_chunk += rows_per_chunk) {
 
 // Handle the final chunk (might be smaller than rows_per_chunk)
 int rows_to_send = (cmd.h - y_chunk > rows_per_chunk) ? rows_per_chunk : (cmd.h - y_chunk);
 
 // --- A. THE STRIDE COPY (Fixes the "Slice" glitch) ---
 // We pack the non-contiguous framebuffer rows into our contiguous temp_buffer
 for (int i = 0; i < rows_to_send; i++) {
 // Calculate Source Address: (Row * Width + Col) * 3
 // We use (cmd.y + y_chunk + i) to get the absolute row index
 int src_offset = ((cmd.y + y_chunk + i) * fb.displayWidth + cmd.x) * bpp;
 
 // Calculate Destination Address: i * Width * 3
 int dst_offset = (i * cmd.w) * bpp;
 
 memcpy(temp_buffer + dst_offset,
 (uint8_t*)fb.pixelData + src_offset,
 cmd.w * bpp);
 }
 
 // --- B. THE DRAW ---
 // Send the packed buffer to the display
 esp_err_t ret = esp_lcd_panel_draw_bitmap(lcd_handle,
 cmd.x,
 cmd.y + y_chunk,
 cmd.x + cmd.w,
 cmd.y + y_chunk + rows_to_send,
 temp_buffer);
 if (ret != ESP_OK) {
 ESP_LOGE("LCD", "Draw bitmap failed");
 break;
 }
 
 // --- C. THE RACE CONDITION FIX ---
 // We MUST wait for the SPI transaction to finish before we loop back
 // and overwrite 'temp_buffer' with new data.
 
 // Method 1: If you have a 'flush_ready' callback using a Semaphore, wait on it here.
 // Method 2: The "Brute Force" Polling (Simplest for now)
 // Note: Check your specific esp_lcd driver initialization.
 // If you didn't set a callback, verify if the driver blocks automatically.
 // Most ESP-IDF SPI drivers are non-blocking by default.
 
 // A simple delay is crude but proves the fix.
 // Ideally, you use xSemaphoreTake(transfer_done_sem, portMAX_DELAY);
 
 // For testing, this ensures the bus is clear (assuming 40MHz SPI, 20 lines takes <1ms)
 // Better: Application-level wait logic or polling.
 // If your driver has a "wait for done" function, use it.
 
 // *CRITICAL*: If you don't have semaphores set up, the buffer will corrupt.
 // If you are unsure, UNCOMMENT the line below to allocate a NEW buffer every time (slower, but safer)
 // OR add a delay:
 vTaskDelay(pdMS_TO_TICKS(1)); // Give DMA time to breathe
 }
 
 // 4. Cleanup
 heap_caps_free(temp_buffer);
 }
 */


// Example Master Drawing Function
void draw_current_view(Framebuffer *fb, FT_Face ft_face) {
    
    // --- 2. Draw the SCROLLABLE Text Box ---
    const char *scroll_text = "The quick brown fox jumps over the lazy dog. This is the content that needs to scroll smoothly behind the viewport frame.";
    ColorRGBA white = {.r = 255, .g = 255, .b = 255, .a = 255};
    TextFormat format = {.alignment = TEXT_ALIGN_LEFT, .wrapMode = TEXT_WRAP_MODE_WHOLE_WORD, .lineSpacing = 5, .glyphSpacing = 0};
    
    // CALL RENDER TEXT BOX
    renderTextBoxScroll(
                        fb,
                        ft_face,
                        scroll_text,
                        50, // x_start
                        100, // y_start (Top of the viewport)
                        220, // clipWidth
                        g_scroll_viewport_h, // clipHeight (e.g., 300)
                        g_scroll_offset_y, // <--- 🔑 SCROLL OFFSET APPLIED HERE
                        white,
                        16,
                        &format
                        );
    
    // ... (any other static foreground elements) ...
}


#include <math.h>
#include "esp_heap_caps.h" // Ensure we use capable memory for large vertex arrays

// ==========================================================
// HELPER: Generate Gear Vertices
// ==========================================================
/**
 * @brief Generates vertices for a gear-like shape.
 * This creates a star shape with many short teeth, resembling a gear.
 */
Vector3* create_gear_vertices(float centerX, float centerY, float outerRadius, float innerRadius, int numTeeth, int *outNumVertices) {
    int total_vertices = numTeeth * 2;
    *outNumVertices = total_vertices;
    
    // Allocate memory for the vertices.
    // Using heap_caps_malloc to ensure enough general RAM is available for larger shapes.
    Vector3* vertices = (Vector3*)heap_caps_malloc(total_vertices * sizeof(Vector3), MALLOC_CAP_DEFAULT);
    if (!vertices) {
        ESP_LOGE(TAG, "Failed to allocate gear vertices.");
        return NULL;
    }
    
    // The angle step is full circle divided by total points (inner + outer)
    float angle_step = (2.0f * M_PI) / total_vertices;
    
    for (int i = 0; i < total_vertices; i++) {
        // Alternate between outer and inner radii to create teeth
        float r = (i % 2 == 0) ? outerRadius : innerRadius;
        float angle = i * angle_step;
        
        // Calculate vertex position
        vertices[i].x = centerX + r * cosf(angle);
        vertices[i].y = centerY + r * sinf(angle);
        // Assumes Vector3 has a z component, set to 0 for 2D
#ifdef VECTOR3_HAS_Z
        vertices[i].z = 0.0f;
#endif
    }
    
    return vertices;
}

/**
 * @brief Creates a gear with a "spiked" or toothed hole in the center.
 * * @param holeOuterRadius The "tip" of the inner spikes.
 * @param holeInnerRadius The "valley" of the inner spikes.
 */
Vector3* create_spiked_center_gear_vertices(float centerX, float centerY, float outerRadius, float innerRadius, float holeOuterRadius, float holeInnerRadius, int numTeeth, int *outNumVertices) {
    int gear_points = numTeeth * 2;
    int total_vertices = gear_points * 2 + 2;
    
    *outNumVertices = total_vertices;
    
    Vector3* vertices = (Vector3*)heap_caps_malloc(total_vertices * sizeof(Vector3), MALLOC_CAP_DEFAULT);
    if (!vertices) return NULL;
    
    float angle_step = (2.0f * M_PI) / gear_points;
    
    // --- 1. Trace Outer Gear (Clockwise) ---
    int v_index = 0;
    for (int i = 0; i < gear_points; i++) {
        float r = (i % 2 == 0) ? outerRadius : innerRadius;
        float angle = i * angle_step;
        
        vertices[v_index].x = centerX + r * cosf(angle);
        vertices[v_index].y = centerY + r * sinf(angle);
        vertices[v_index].z = 0.0f;
        v_index++;
    }
    
    // --- 2. Cut to Inner Spikes ---
    vertices[v_index] = vertices[v_index - 1];
    v_index++;
    
    // --- 3. Trace Inner Spikes (Counter-Clockwise) ---
    for (int i = gear_points; i >= 0; i--) {
        // Logic for inner teeth:
        // We want the inner spike to align or offset with the outer spike.
        // Using the same modulo logic keeps them aligned.
        float r = (i % 2 == 0) ? holeOuterRadius : holeInnerRadius;
        float angle = i * angle_step;
        
        vertices[v_index].x = centerX + r * cosf(angle);
        vertices[v_index].y = centerY + r * sinf(angle);
        vertices[v_index].z = 0.0f;
        v_index++;
    }
    
    return vertices;
}



// ==========================================================
// MAIN EXAMPLE FUNCTION TO CALL IN GRAPHICS TASK
// ==========================================================
void draw_complex_gear_gradient_example(Framebuffer *fb) {
    ESP_LOGI(TAG, "Starting complex polygon draw...");
    
    // --- 1. Define the Complex Geometry (48-vertex Gear) ---
    int num_vertices = 0;
    // Center the gear at (160, 240)
    // Outer radius 130px, Inner radius 110px (short, stubby teeth)
    // 24 teeth = 48 total vertices
    Vector3* gear_vertices = create_gear_vertices(160.0f, 240.0f, 130.0f, 110.0f, 24, &num_vertices);
    
    if (!gear_vertices) {
        return; // Exit if allocation failed
    }
    ESP_LOGI(TAG, "Generated %d vertices for gear.", num_vertices);
    
    
    // --- 2. Define the 10-Stop "Thermal" Gradient ---
    // We define 10 colors creating a spectrum from cold (blue) to hot (white).
    
    ColorRGBA col0 = {.r = 0,   .g = 0,   .b = 128, .a = 255}; // Deep Blue (0.0)
    ColorRGBA col1 = {.r = 0,   .g = 0,   .b = 255, .a = 255}; // Blue (0.1)
    ColorRGBA col2 = {.r = 0,   .g = 128, .b = 255, .a = 255}; // Light Blue (0.2)
    ColorRGBA col3 = {.r = 0,   .g = 255, .b = 255, .a = 255}; // Cyan (0.3)
    ColorRGBA col4 = {.r = 0,   .g = 255, .b = 0,   .a = 255}; // Green (0.4)
    ColorRGBA col5 = {.r = 255, .g = 255, .b = 0,   .a = 255}; // Yellow (0.5)
    ColorRGBA col6 = {.r = 255, .g = 128, .b = 0,   .a = 255}; // Orange (0.6)
    ColorRGBA col7 = {.r = 255, .g = 0,   .b = 0,   .a = 255}; // Red (0.7)
    ColorRGBA col8 = {.r = 128, .g = 0,   .b = 128, .a = 255}; // Purple (0.8)
    ColorRGBA col9 = {.r = 255, .g = 255, .b = 255, .a = 255}; // White Hot (1.0)
    
    ColorStop stops[10] = {
        {.color = col0, .position = 0.0f},
        {.color = col1, .position = 0.1f},
        {.color = col2, .position = 0.2f},
        {.color = col3, .position = 0.3f},
        {.color = col4, .position = 0.4f},
        {.color = col5, .position = 0.5f},
        {.color = col6, .position = 0.6f},
        {.color = col7, .position = 0.7f},
        {.color = col8, .position = 0.8f},
        {.color = col9, .position = 1.0f}
    };
    
    Gradient complex_gradient = {
        .stops = stops,
        .numStops = 10,
        // Set a 30-degree angle for the thermal sweep across the gear
        .angle = M_PI / 6.0f
    };
    
    
    // --- 3. Draw the Polygon ---
    uint64_t start_time = esp_timer_get_time();
    
    fillPolygonWithGradient(
                            fb,
                            gear_vertices,
                            num_vertices,
                            &complex_gradient,
                            NULL,       // No Transform matrix needed for this test
                            false       // Anti-aliasing off (scanline fill is usually binary)
                            );
    
    uint64_t end_time = esp_timer_get_time();
    ESP_LOGI(TAG, "Complex polygon drawn in %llu microseconds.", (end_time - start_time));
    
    
    // --- 4. Clean up memory ---
    // IMPORTANT: Free the vertex array allocated by the helper function
    free(gear_vertices);
}

void draw_star(Framebuffer *fb) {
    int num_vertices = 0;
    Vector3* star_vertices = create_star_vertices(
                                                  160.0f, // Center X (middle of 320 screen)
                                                  240.0f, // Center Y (middle of 480 screen)
                                                  100.0f, // Outer Radius
                                                  40.0f,  // Inner Radius
                                                  &num_vertices
                                                  );
    
    if (!star_vertices) {
        ESP_LOGE(TAG, "Failed to allocate star vertices.");
        return;
    }
    
    // --- 2. Define the Gradient ---
    
    // Gradient Stop 1: Bright Gold
    ColorRGBA gold = {.r = 255, .g = 215, .b = 0, .a = 255};
    // Gradient Stop 2: Deep Red-Orange
    ColorRGBA fire_red = {.r = 178, .g = 34, .b = 34, .a = 255};
    
    ColorStop stops[2] = {
        {.color = gold, .position = 0.0f},
        {.color = fire_red, .position = 1.0f}
    };
    
    Gradient star_gradient = {
        .stops = stops,
        .numStops = 2,
        .angle = M_PI_4 // 45 degrees diagonal gradient
    };
    
    // --- 3. Draw the Polygon ---
    // No transform or anti-aliasing for this example
    fillPolygonWithGradient(
                            fb,
                            star_vertices,
                            num_vertices,
                            &star_gradient,
                            NULL,       // No Transform
                            false       // No AntiAlias (since the scanline code is complex)
                            );
    
    // 4. Clean up memory
    free(star_vertices);
}

void setup_scroll_text_demo() {
    g_long_text_ptr = "This is the beginning of a very long string.\n"
    "It will demonstrate scrolling text rendering.\n\n"
    "We need to measure the total height of this text "
    "so we know how far we are allowed to scroll.\n"
    "The renderTextBoxScroll function handles the "
    "vertical offset calculation.\n\n"
    "Line 6\nLine 7\nLine 8\nLine 9\nLine 10\n"
    "End of text.";
    
    // Define formatting for measurement
    TextFormat fmt = { .alignment = TEXT_ALIGN_LEFT, .wrapMode = TEXT_WRAP_MODE_WHOLE_WORD, .lineSpacing = 5, .glyphSpacing = 0 };
    
    // Measure total height (using the helper function we wrote earlier)
    // Assuming viewport width is 200
    g_text_total_height = measureTextHeight(ft_face, g_long_text_ptr, 200, 16, &fmt);
    
    ESP_LOGI(TAG, "Total Text Height: %d px", g_text_total_height);
}

// Helper to allocate the buffer (Call this in app_main or graphics_task setup)
void init_anim_buffer() {
    g_anim_backup_buffer = (uint8_t*)heap_caps_malloc(ANIM_W * ANIM_H * 3, MALLOC_CAP_SPIRAM);
    if (!g_anim_backup_buffer) {
        ESP_LOGE(TAG, "Failed to allocate animation backup buffer!");
    } else {
        ESP_LOGI(TAG, "Animation backup buffer allocated (120KB).");
    }
}

void triangle_animation_task(void *pvParameter) {
    
    ESP_LOGI(TAG, "triangle_animation_task");
    
    
    
    // 1. Wait a moment for the main UI (Gradient/Rects) to finish drawing
    //vTaskDelay(pdMS_TO_TICKS(1000));
    
    // 2. Send command to CAPTURE the background (Snapshot the gradient behind us)
    GraphicsCommand cmd_save = { .cmd = CMD_ANIM_SAVE_BG, .x =  ANIM_X, .y = ANIM_Y, .w = ANIM_W, .h = ANIM_H};
    xQueueSend(g_graphics_queue, &cmd_save, portMAX_DELAY);
    
    float phase = 0.0f;
    const float speed = 0.1f;
    int center_x = 160;
    int center_y = 240;
    
    // Background is now handled by Restore, so we don't need a clear color
    
    while (1) {
        
        // --- STEP 1: RESTORE BACKGROUND (Erase previous triangle) ---
        // This copies the saved pixels back to the framebuffer
        GraphicsCommand cmd_restore = { .cmd = CMD_ANIM_RESTORE_BG, .x =  ANIM_X, .y = ANIM_Y, .w = ANIM_W, .h = ANIM_H };
        xQueueSend(g_graphics_queue, &cmd_restore, 0);
        
        
        // --- STEP 2: CALCULATE AND DRAW NEW TRIANGLE ---
        float radius = 50.0f + 30.0f * sinf(phase);
        phase += speed;
        
        Vector3* vertices = (Vector3*)cc_safe_alloc(1, 3 * sizeof(Vector3));
        if (vertices) {
            vertices[0].x = center_x;
            vertices[0].y = center_y - radius;
            vertices[0].z = 0;
            vertices[1].x = center_x + (radius * 0.866f);
            vertices[1].y = center_y + (radius * 0.5f);
            vertices[1].z = 0;
            vertices[2].x = center_x - (radius * 0.866f);
            vertices[2].y = center_y + (radius * 0.5f);
            vertices[2].z = 0;
            
            Gradient* triGrad = (Gradient*)cc_safe_alloc(1, sizeof(Gradient));
            triGrad->type = GRADIENT_TYPE_LINEAR;
            triGrad->angle = M_PI_2;
            triGrad->numStops = 2;
            triGrad->stops = (ColorStop*)cc_safe_alloc(1, sizeof(ColorStop) * 2);
            triGrad->stops[0].color = (ColorRGBA){255, 165, 0, 255};
            triGrad->stops[0].position = 0.0f;
            triGrad->stops[1].color = (ColorRGBA){255, 0, 0, 255};
            triGrad->stops[1].position = 1.0f;
            
            GraphicsCommand cmd_poly = {
                .cmd = CMD_DRAW_POLYGON,
                .vertices = vertices,
                .numVertices = 3,
                .gradientData = triGrad
            };
            xQueueSend(g_graphics_queue, &cmd_poly, 0);
        }
        
        // --- STEP 3: UPDATE SCREEN ---
        // Push the dirty area (Background + Triangle) to the LCD
        GraphicsCommand cmd_flush = {
            .cmd = CMD_UPDATE_AREA,
            .x = ANIM_X, .y = ANIM_Y,
            .w = ANIM_W, .h = ANIM_H
        };
        xQueueSend(g_graphics_queue, &cmd_flush, 0);
        
        vTaskDelay(pdMS_TO_TICKS(66));
    }
}

/**
 * @brief This task handles all drawing and screen updates.
 * It draws the initial UI *once*, then enters an event loop
 * to process partial updates from a queue.
 */

void graphics_task(void *arg)
{
    // ==========================================================
    // PART 1: INITIALIZATION (Runs ONCE)
    // ==========================================================
    ESP_LOGI(TAG, "Graphics task started.");
    
    // --- INITIALIZE FREETYPE ---
    esp_err_t ft_ok = initialize_freetype();
    if (ft_ok != ESP_OK) {
        ESP_LOGE(TAG, "Failed to initialize FreeType, deleting task.");
        vTaskDelete(NULL); // Abort this task
        return;
    }
    
    init_freetype_cache_system();
    
    // --- Allocate the single framebuffer in PSRAM ---
    int width = DISPLAY_HORIZONTAL_PIXELS;
    int height = DISPLAY_VERTICAL_PIXELS;
    int num_pixels = width * height;
    
    uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(num_pixels * 3, MALLOC_CAP_SPIRAM);
    
    if (!display_buffer) {
        ESP_LOGE(TAG, "Failed to allocate display buffer in PSRAM. Deleting task.");
        vTaskDelete(NULL); // Delete this task
        return;
    }
    ESP_LOGI(TAG, "Framebuffer allocated.");
    
    // --- Initialize the CPUGraphics Framebuffer struct ---
    fb = (Framebuffer){
        .displayWidth = width,
        .displayHeight = height,
        .pixelData = display_buffer,
        .colorMode = COLOR_MODE_BGR888
    };
    
    // ==========================================================
    // PART 2: DRAW INITIAL SCREEN (Runs ONCE)
    // ==========================================================
    ESP_LOGI(TAG, "Drawing initial UI...");
    
    // --- Define colors ---
    ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
    ColorRGBA red = {.r = 255, .g = 0, .b = 0, .a = 255};
    ColorRGBA green_alpha = {.r = 0, .g = 255, .b = 0, .a = 128};
    ColorRGBA white = {.r = 255, .g = 255, .b = 255, .a = 255};
    
    // --- Draw initial UI to PSRAM buffer ---
    clearFramebuffer(&fb, blue);
    drawRectangleCFramebuffer(&fb, 50, 50, 100, 100, red, true);
    drawRectangleCFramebuffer(&fb, 100, 100, 150, 150, green_alpha, true);
    //renderText(&fb, ft_face, "Hello, World!", 50, 300, white, 24, NULL);
    
    // Start Color: A deep, rich blue
    ColorRGBA deep_blue = {.r = 0, .g = 50, .b = 150, .a = 255};
    // End Color: A bright, vibrant cyan/aqua
    ColorRGBA vibrant_aqua = {.r = 0, .g = 200, .b = 255, .a = 255};
    
    // Define the positions (0.0 to 1.0)
    ColorStop stops1[2] = {
        {.color = deep_blue, .position = 0.0f},
        {.color = vibrant_aqua, .position = 1.0f}
    };
    
    // --- 2. Define the Gradient Structure ---
    Gradient aqua_gradient = {
        .stops = stops1,
        .numStops = 2,
        .angle = M_PI_2 // 90 degrees (vertical gradient, from top to bottom)
        // You could also try 0.0 for horizontal, or M_PI_4 for diagonal
    };
    
    // --- 3. Define the Draw Parameters (Full Screen) ---
    int rect_x1 = 0;
    int rect_y1 = 0;
    int rect_w1 = 320;
    int rect_h1 = 480;
    
    // No transformation needed for a full-screen background
    Matrix3x3 identity_mat1 = IdentityMatrix();
    
    // --- 4. Call the Gradient Drawing Function ---
    fillRectangleWithGradient(
                              &fb,
                              rect_x1,
                              rect_y1,
                              rect_w1,
                              rect_h1,
                              &aqua_gradient,
                              &identity_mat1 // No transformation
                              );
    
    /*const float rotation_angle_deg = 45.0f;
     const float rotation_angle_rad = rotation_angle_deg * (M_PI / 180.0f);
     
     const int startX = 20;
     const int startY = 20;
     const int fontSize = 36;
     
     ColorRGBA yellow = {.r = 255, .g = 255, .b = 0, .a = 255};
     
     // --- 2. Build the Rotation Matrix (R) ---
     Matrix3x3 R = RotationMatrix(rotation_angle_rad);
     
     // --- 3. Build the Full Transform Matrix (T * R * -T) ---
     // The matrix order for pivot rotation is: T(x, y) * R(angle) * T(-x, -y)
     
     // First, translate the origin to the pivot point (startX, startY)
     Matrix3x3 T_pivot = TranslationMatrix((float)startX, (float)startY);
     
     // Second, apply the rotation (R)
     Matrix3x3 T_R = MultiplyMatrix3x3(&T_pivot, &R);
     
     // Third, translate the pivot point back to the origin
     Matrix3x3 T_neg = TranslationMatrix((float)-startX, (float)-startY);
     
     // Final Transform: (T_pivot * R) * T_neg
     Matrix3x3 final_transform = MultiplyMatrix3x3(&T_R, &T_neg);
     
     
     // --- 4. Render the Text ---
     
     // Draw a small square at the pivot point for reference
     drawRectangleCFramebuffer(&fb, startX - 3, startY - 3, 6, 6, yellow, true);
     
     renderTextWithTransform(
     &fb,
     ft_face,
     "Rotated by 45°",
     startX,
     startY,
     yellow,
     fontSize,
     &final_transform, // Pass the composite rotation matrix
     NULL
     );*/
    
    // 1. Load the PNG (and decode it into PSRAM)
    const char* imgPath = "/spiflash/test.png";
    ImageTexture* png_texture = load_image_from_file(imgPath);
    if (!png_texture) {
        return;
    }
    
    // 2. Define the target area for drawing
    int draw_x = 20;
    int draw_y = 80;
    int draw_w = png_texture->width * 2; // Example: draw it scaled up by 2x
    int draw_h = png_texture->height * 2;
    
    // 3. Draw the scaled texture onto the framebuffer
    // This uses your existing drawImageTexture function
    //drawImageTexture(&fb, png_texture, draw_x, draw_y, png_texture->width, png_texture->height);
    
    // 4. Free the temporary PSRAM texture buffer
    heap_caps_free(png_texture->data);
    free(png_texture);
    
    ColorRGBA orange = {.r = 255, .g = 165, .b = 0, .a = 255}; // Opaque Orange
    ColorRGBA aqua_alpha = {.r = 0, .g = 0, .b = 255, .a = 180}; // Semi-transparent Aqua
    ColorRGBA dark_blue = {.r = 0, .g = 0, .b = 50, .a = 255}; // Background blue
    
    /*drawRoundedRectangle_AntiAliasing(
     &fb,
     40,      // X Start
     100,     // Y Start
     200,     // Width (200px)
     150,     // Height (150px)
     orange,  // Color: Opaque Orange
     30,      // Radius (30px corner radius)
     true     // Fill: True (solid rectangle)
     );
     
     // 3. Draw the second Anti-aliased Rounded Rectangle (Semi-transparent Aqua)
     // This will overlap the first, and the alpha blending will occur on the edges
     // and the overlapping area, showing a smooth transition.
     drawRoundedRectangle_AntiAliasing(
     &fb,
     160,     // X Start (overlaps the first rect)
     180,     // Y Start (overlaps the first rect)
     100,     // Width (200px)
     150,     // Height (150px)
     aqua_alpha, // Color: Semi-transparent Aqua (A=180)
     50,         // Radius (50px corner radius)
     true        // Fill: True (solid rectangle)
     );*/
    
    ColorRGBA border_alpha = {.r = 0, .g = 0, .b = 0, .a = 200};
    drawRoundedRectangle_AntiAliasing(
                                      &fb,
                                      17,      // X Start
                                      17,      // Y Start
                                      286,     // Width
                                      56,      // Height
                                      border_alpha,   // Color: White
                                      28,      // Radius
                                      false    // Fill: False (Draws only the border)
                                      );
    drawRoundedRectangle_AntiAliasing(
                                      &fb,
                                      20,      // X Start
                                      20,      // Y Start
                                      280,     // Width
                                      50,      // Height
                                      white,   // Color: White
                                      25,      // Radius
                                      false    // Fill: False (Draws only the border)
                                      );
    
    // --- Define Colors ---
    ColorRGBA white_alpha = {.r = 255, .g = 255, .b = 255, .a = 150}; // Semi-transparent White
    ColorRGBA yellow_opaque = {.r = 255, .g = 255, .b = 0, .a = 255}; // Opaque Yellow
    ColorRGBA magenta_opaque = {.r = 255, .g = 0, .b = 255, .a = 255}; // Opaque Magenta
    ColorRGBA cyan_alpha = {.r = 0, .g = 255, .b = 255, .a = 100}; // Highly transparent Cyan
    
    // Get screen dimensions for cleaner coordinates
    int w = 320;
    int h = 480;
    
    // --- 1. Draw a thin, opaque diagonal line (for precision test) ---
    /*drawLineWithThickness(&fb,
     10, 10,       // Start near top-left
     w - 10, h - 10, // End near bottom-right
     magenta_opaque,
     1); // Thickness: 1 pixel
     
     // --- 2. Draw a thick, opaque horizontal line ---
     drawLineWithThickness(&fb,
     0, 400,     // Start at left edge
     w, 400,     // End at right edge
     yellow_opaque,
     10); // Thickness: 10 pixels
     
     // --- 3. Draw a semi-transparent line over the yellow one (blending test) ---
     // This line should appear orange where it crosses the yellow line.
     drawLineWithThickness(&fb,
     w / 2, 350,   // Start near center
     w / 2, 450,   // End in the yellow line area
     white_alpha,
     15); // Thickness: 15 pixels
     
     // --- 4. Draw a highly transparent overlay (Alpha Blending Test) ---
     // This line will barely change the color of the background/lines it crosses.
     drawLineWithThickness(&fb,
     50, h / 2,
     w - 50, h / 2,
     cyan_alpha,
     5); // Thickness: 5 pixels
     */
    
    // Gradient Stop 1: Starts as opaque dark pink/magenta
    ColorRGBA stop1_color = {.r = 200, .g = 0, .b = 150, .a = 255};
    // Gradient Stop 2: Transitions to opaque yellow/green
    ColorRGBA stop2_color = {.r = 100, .g = 255, .b = 0, .a = 255};
    
    // Define the positions (0.0 to 1.0)
    ColorStop stops[2] = {
        {.color = stop1_color, .position = 0.0f},
        {.color = stop2_color, .position = 1.0f}
    };
    
    // --- 2. Define the Gradient Structure ---
    Gradient my_gradient = {
        .stops = stops,
        .numStops = 2,
        .angle = M_PI_4 // 45 degrees (M_PI_4 is a common constant for PI/4)
    };
    
    // --- 3. Define the Draw Parameters ---
    
    int rect_x = 50;
    int rect_y = 350;
    int rect_w = 250;
    int rect_h = 100;
    int corner_radius = 20;
    
    // Optional: Define a 2D rotation matrix (45 degrees clockwise rotation)
    Matrix3x3 rotation_mat = RotationMatrix(-M_PI_4);
    
    // Note: If you don't want the rectangle rotated, use an Identity Matrix:
    Matrix3x3 identity_mat = IdentityMatrix();
    
    // --- 4. Call the Gradient Drawing Function ---
    
    // This draws a large, rotated, anti-aliased, rounded rectangle
    /*fillRoundedRectangleWithGradient(
     &fb,
     rect_x,
     rect_y,
     rect_w,
     rect_h,
     &my_gradient,
     corner_radius,
     &identity_mat, // Passing the rotation matrix
     true           // AntiAlias: True
     );*/
    
    
    // --- Send the *entire* buffer to the LCD *once* ---
    /*esp_err_t err = esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer);
     if (err != ESP_OK) {
     ESP_LOGE(TAG, "Failed to draw initial bitmap! Error: %s", esp_err_to_name(err));
     }
     ESP_LOGI(TAG, "Initial UI drawn. GRAPHICS TASK READY FOR COMMANDS.");*/
    
    
    
    GraphicsCommand cmd_update;
    cmd_update.cmd = CMD_UPDATE_AREA;
    cmd_update.x = 0;
    cmd_update.y = 0; // A rough bounding box
    cmd_update.w = 320;
    cmd_update.h = 480; // A bit larger to be safe
    updateArea(fb, cmd_update);
    
    
    
    
    // ==========================================================
    // PART 3: EVENT LOOP (Runs FOREVER)
    // ==========================================================
    GraphicsCommand cmd;
    while (1) {
        // Wait forever until a command arrives
        if (xQueueReceive(g_graphics_queue, &cmd, portMAX_DELAY) == pdTRUE) {
            
            //ESP_LOGI(TAG, "Graphics task received command: %d", cmd.cmd);
            
            switch (cmd.cmd) {
                    
                    // --- Case 1: Draw a Rectangle ---
                case CMD_DRAW_RECT:
                    ESP_LOGI(TAG, "Drawing rect to PSRAM");
                    drawRectangleCFramebuffer(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color, true);
                    break;
                    
                    // --- Case 2: Draw Text ---
                case CMD_DRAW_TEXT:
                    ESP_LOGI(TAG, "Drawing text to PSRAM");
                    renderText(&fb, ft_face, cmd.text, cmd.x, cmd.y, cmd.color, 12, NULL);
                    break;
                    
                    // --- Case 3: Update the LCD (Chunked) ---
                case CMD_DRAW_TEXT_BOX:
                    // --- Define a large block of text ---
                    /*const char *long_text = "The quick brown fox jumps over the lazy dog. "
                     "This text needs to demonstrate word-wrapping and line-breaking "
                     "within a confined area for our new scroll view. \n\n" // Explicit newline
                     "This final sentence should appear on a completely new line.";
                     
                     ColorRGBA text_color = {.r = 255, .g = 255, .b = 255, .a = 255}; // White
                     
                     // Define the Clipping Area (The Scroll View Window)
                     int clip_x = cmd.x;
                     int clip_y = cmd.y;
                     int clip_w = cmd.w; // Constrain the width to 200 pixels
                     int clip_h = cmd.h; // Constrain the height to 300 pixels
                     
                     // --- 3. Define and Initialize the TextFormat Struct ---
                     TextFormat text_format = {
                     // Property 1: Center the text horizontally within the clip_w area
                     .alignment = TEXT_ALIGN_CENTER,
                     
                     // Property 2: Never split words mid-line (will move "boundaries" to next line)
                     .wrapMode = TEXT_WRAP_MODE_WHOLE_WORD,
                     
                     // Property 3: Add 10 extra pixels of space between the calculated line height
                     .lineSpacing = -2,
                     
                     // Property 4: Use FreeType's kerning metrics for a polished look
                     .glyphSpacing = -1
                     };
                     
                     // --- Render the Multi-Line Text ---
                     renderTextBox(
                     &fb,
                     ft_face,
                     long_text,
                     clip_x,
                     clip_y,
                     clip_w,
                     clip_h,
                     cmd.color,
                     16, // Font Size
                     &text_format
                     );*/
                    ESP_LOGI(TAG, "Drawing Text Box: %s", cmd.text);
                    /*renderTextBoxExtended(
                     &fb, ft_face, cmd.text,
                     cmd.x, cmd.y, cmd.w, cmd.h, // Logical Layout Box
                     // Pass the Clip Rects from the command (which come from View Hierarchy)
                     cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                     0, // ScrollY (Use this param if you have a specific scroll command)
                     cmd.color, cmd.fontSize, &cmd.textFormat
                     );*/
                    renderTextBox(
                                  &fb,
                                  ft_face,
                                  cmd.text,
                                  cmd.x, cmd.y,       // Start X, Y
                                  cmd.w, cmd.h,       // Clip Width, Clip Height
                                  cmd.color,
                                  cmd.fontSize,
                                  &cmd.textFormat     // Pass the format struct
                                  );
                    break;
                case CMD_DRAW_TEXT_BOX_CACHED: {
                    const char* myFont = "/spiflash/proximaNovaRegular.ttf";
                    renderTextBoxExtendedCached(
                                  &fb,
                                  g_ftc_manager,     // The Global Manager
                                  g_ftc_image_cache, // The Global Image Cache
                                  g_ftc_cmap_cache,  // The Global CMap Cache
                                  (FTC_FaceID)myFont, // Pass path as ID
                                  cmd.text,
                                  cmd.x, cmd.y,       // Start X, Y
                                  cmd.w, cmd.h,       // Clip Width, Clip Height
                                  cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                  0,
                                  cmd.color,
                                  cmd.fontSize,
                                  &cmd.textFormat     // Pass the format struct
                                  );
                    break;
                }
                case CMD_UPDATE_AREA:
                {
                    updateArea(fb, cmd);
                    break;
                }
                case CMD_CURSOR_SETUP:
                {    // 1. Save the background under the new cursor location
                    //    The x/y are passed in the generic cmd struct
                    ESP_LOGI(TAG, "calling function save_cursor_background");
                    save_cursor_background(&fb, cmd.x, cmd.y);
                    
                    // 2. Draw the cursor immediately (first blink ON)
                    drawRectangleCFramebuffer(&fb, cmd.x, cmd.y, CURSOR_W, CURSOR_H, white, true);
                    
                    // 3. Send the small update area to show the cursor
                    GraphicsCommand cmd_update = {.cmd = CMD_UPDATE_AREA, .x = cmd.x, .y = cmd.y, .w = CURSOR_W, .h = CURSOR_H};
                    //xQueueSend(g_graphics_queue, &cmd_update, 0);
                    updateArea(fb, cmd_update);
                    
                    xTaskCreatePinnedToCore(
                                            cursor_blink_task,  // New task function
                                            "cursor_blink",
                                            2048,
                                            NULL,
                                            4,
                                            &g_cursor_blink_handle,  // Capture the handle here
                                            0
                                            );
                    // 4. --- NEW: SEND THE NOTIFICATION TO START THE BLINKER ---
                    if (g_cursor_blink_handle != NULL) {
                        xTaskNotifyGive(g_cursor_blink_handle);
                    }
                    break;
                }
                case CMD_CURSOR_DRAW:
                {    // Draw the cursor (uses its current global x/y location)
                    drawRectangleCFramebuffer(&fb, g_cursor_x, g_cursor_y, CURSOR_W, CURSOR_H, white, true);
                    
                    // Send the update area
                    GraphicsCommand cmd_draw = {.cmd = CMD_UPDATE_AREA, .x = g_cursor_x, .y = g_cursor_y, .w = CURSOR_W, .h = CURSOR_H};
                    //xQueueSend(g_graphics_queue, &cmd_draw, 0);
                    updateArea(fb, cmd_draw);
                    break;
                }
                case CMD_CURSOR_RESTORE:
                {    // Restore the background (which also sends the update to the LCD)
                    restore_cursor_background(&fb);
                    break;
                }
                    // Inside graphics_task's while(1) loop, switch (cmd.cmd):
                case CMD_SCROLL_CONTENT:
                {
                    int delta_y = cmd.y; // The scroll delta sent from updateTouch
                    int max_scroll = g_scroll_total_height - g_scroll_viewport_h;
                    
                    // Ensure max_scroll is not negative (i.e., content fits entirely in viewport)
                    if (max_scroll < 0) max_scroll = 0;
                    
                    // 1. Update the offset
                    g_scroll_offset_y += delta_y;
                    
                    // 2. Clamp the offset (ensure it stays within boundaries)
                    if (g_scroll_offset_y < 0) {
                        g_scroll_offset_y = 0; // Cannot scroll past the top edge
                    } else if (g_scroll_offset_y > max_scroll) {
                        g_scroll_offset_y = max_scroll; // Cannot scroll past the bottom edge
                    }
                    
                    // 3. Trigger a full redraw of the viewport
                    // This uses the existing CMD_LOAD_PAGE_1 logic to redraw the whole scene *once*.
                    GraphicsCommand cmd_redraw_viewport = {.cmd = CMD_LOAD_PAGE_1};
                    xQueueSend(g_graphics_queue, &cmd_redraw_viewport, 0);
                    
                    ESP_LOGI(TAG, "Scroll updated to: %d/%d", g_scroll_offset_y, max_scroll);
                    break;
                }
                    // Inside graphics_task switch (cmd.cmd):
                case CMD_LOAD_PAGE_1:{ // Triggered by CMD_SCROLL_CONTENT
                    ESP_LOGI(TAG, "Redrawing entire view due to scroll event.");
                    
                    // 1. Redraw the entire scene, applying the new g_scroll_offset_y
                    draw_current_view(&fb, ft_face);
                    
                    // 2. Send the full buffer update to the LCD
                    // (This is triggered by the scroll event, so we push the full screen.)
                    ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
                                                              0, 0,
                                                              fb.displayWidth, fb.displayHeight,
                                                              fb.pixelData));
                    break;}
                case CMD_DRAW_STAR: {
                    draw_star(&fb);
                    
                    draw_complex_gear_gradient_example(&fb);
                    
                    
                    
                    
                }
                    // Inside graphics_task switch ...
                    
                    // Inside graphics_task while loop... switch(cmd.cmd) ...
                    
                case CMD_DRAW_ROUNDED_RECT:
                {
                    // This function handles the AA and the color mode internally
                    /*drawRoundedRectangle_AntiAliasingExtended(
                                                              &fb,
                                                              cmd.x, cmd.y, cmd.w, cmd.h,
                                                              cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                              cmd.color,
                                                              cmd.radius,
                                                              cmd.fill
                                                              );
                    
                    int64_t t1 = esp_timer_get_time();
                    
                    drawRoundedRectangle_AntiAliasingExtended(
                                                              &fb,
                                                              cmd.x, cmd.y, cmd.w, cmd.h,
                                                              cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                              cmd.color,
                                                              cmd.radius,
                                                              cmd.fill
                                                              );
                    
                    int64_t t2 = esp_timer_get_time();
                    
                    drawRoundedRectangle_AntiAliasingOptimized(
                                                              &fb,
                                                              cmd.x, cmd.y, cmd.w, cmd.h,
                                                              cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                              cmd.color,
                                                              cmd.radius,
                                                              cmd.fill
                                                              );
                    
                    int64_t t3 = esp_timer_get_time();*/
                    
                    drawRoundedRectangle_AntiAliasingOptimized(
                                                              &fb,
                                                              cmd.x, cmd.y, cmd.w, cmd.h,
                                                              cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                              cmd.color,
                                                              cmd.radius,
                                                              cmd.fill
                                                              );
                    
                    //int64_t t4 = esp_timer_get_time();

                        //ESP_LOGI(TAG, "SIMD (Fixed Pt) Time:  %lld us", (t4 - t3));
                        //ESP_LOGI(TAG, "Speedup Factor:        %.2fx", (float)(t2 - t1) / (float)(t4 - t3));
                    
                    break;
                    
                }
                    
                case CMD_DRAW_GRADIENT_ROUNDED_RECT:
                {
                    if (cmd.gradientData) {
                        fillRoundedRectangleWithGradientExtended(
                                                                 &fb,
                                                                 cmd.x, cmd.y, cmd.w, cmd.h,
                                                                 cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                                 cmd.gradientData,
                                                                 cmd.radius,
                                                                 NULL, // No transform for standard views
                                                                 true  // Anti-alias
                                                                 );
                        
                        printf("shadowRadius: %d", (int)cmd.radius);
                        
                        // CRITICAL: Clean up the memory allocated by the main task
                        if (cmd.gradientData->stops) free(cmd.gradientData->stops);
                        free(cmd.gradientData);
                    }
                    break;
                    
                }
                case CMD_DRAW_GRADIENT_RECT:
                {
                    if (cmd.gradientData) {
                        // Use the optimized function for non-rounded rectangles
                        printf("CMD_DRAW_GRADIENT_RECT: %d %d %d %d %d %d %d %d", cmd.x, cmd.y, cmd.w, cmd.h, cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH);
                        Matrix3x3 identity_mat1 = IdentityMatrix();
                        /*fillRectangleWithGradientExtended(
                                                          &fb,
                                                          cmd.x, cmd.y, cmd.w, cmd.h,
                                                          cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                          cmd.gradientData,
                                                          &identity_mat1 // No transform for standard views
                                                          );*/
                        
                        //fillRectangleWithGradientSIMD(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.gradientData);
                        
                        fillRectangleWithGradientOptimized(
                                                          &fb,
                                                          cmd.x, cmd.y, cmd.w, cmd.h,
                                                          cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
                                                          cmd.gradientData
                                                          );
                        
                        // Clean up memory
                        if (cmd.gradientData->stops) free(cmd.gradientData->stops);
                        free(cmd.gradientData);
                    }
                    break;
                }
                    // --- NEW: DRAW IMAGE FROM FILE ---
                case CMD_DRAW_IMAGE_FILE:
                {
                    ESP_LOGD(TAG, "Drawing Image: %s", cmd.imagePath);
                    
                    // Inside your Load function
                    ImageTexture* tex = NULL;
                    ImageFileType type = get_image_type_from_path(cmd.imagePath);
                    
                    if (type == IMAGE_TYPE_PNG) {
                        tex = load_image_from_file(cmd.imagePath); // LibPNG
                    } else if (type == IMAGE_TYPE_JPEG) {
                        tex = load_jpeg_from_file(cmd.imagePath);  // ESP-JPEG
                    }
                    
                    
                    if (tex) {
                        // 2. Draw (Scales to the view's width/height)
                        // Note: Your CCView frame is the destination size.
                        if (cmd.hasTransform) {
                            // A. Draw with Affine Transform (Rotation, Scale, Skew)
                            // The x, y, w, h here usually define the base rect before transformation
                            drawImageTextureWithTransform(
                                &fb,
                                tex,
                                cmd.x, cmd.y,
                                cmd.w, cmd.h,
                                &cmd.transform
                            );
                        } else {
                            // B. Standard Draw (Fast Axis-Aligned Blit)
                            drawImageTexture(
                                &fb,
                                tex,
                                cmd.x, cmd.y,
                                cmd.w, cmd.h
                            );
                        }
                        if (tex->data) heap_caps_free(tex->data);
                        free(tex);
                    }
                    break;
                }
                case CMD_DRAW_POLYGON: {
                    if (cmd.vertices && cmd.gradientData) {
                        
                        // Use your existing scanline filler
                        fillPolygonWithGradient(
                                                &fb,
                                                cmd.vertices,
                                                cmd.numVertices,
                                                cmd.gradientData,
                                                NULL,  // No extra transform needed (handled in Bridge)
                                                false  // Anti-aliasing (set to true if you implement AA in polygon fill)
                                                );
                        
                        // --- CRITICAL CLEANUP ---
                        // 1. Free the vertices array allocated in the Bridge
                        free(cmd.vertices);
                        
                        // 2. Free the gradient structure allocated in the Bridge
                        if (cmd.gradientData->stops) free(cmd.gradientData->stops);
                        free(cmd.gradientData);
                    }
                    break;
                }
                /*case CMD_DRAW_PIXEL_BUFFER: {
                    // User's custom struct handling
                    if (cmd.pixelBuffer) {
                        ESP_LOGI("GFX", "Received Pixel Buffer Command");
                        // cmd.pixelBuffer is (ColorRGBA*), fb->pixelData is (ColorRGBA*)
                        // Simple Memcpy because formats match
                        size_t bufferSize = cmd.w * cmd.h * sizeof(ColorRGBA);
                        memcpy(fb.pixelData, cmd.pixelBuffer, bufferSize);
                    }
                    break;
                }*/
                case CMD_DRAW_PIXEL_BUFFER: {
                    if (cmd.pixelBuffer) {
                        ColorRGBA *srcPixels = (ColorRGBA *)cmd.pixelBuffer;
                        
                        if (fb.colorMode == COLOR_MODE_BGR888) {
                            uint8_t *dst = (uint8_t *)fb.pixelData;
                            
                            for (int y = 0; y < cmd.h; y++) {
                                if ((cmd.y + y) >= fb.displayHeight) break;
                                
                                for (int x = 0; x < cmd.w; x++) {
                                    if ((cmd.x + x) >= fb.displayWidth) break;
                                    
                                    ColorRGBA p = srcPixels[y * cmd.w + x];
                                    int dstIdx = ((cmd.y + y) * fb.displayWidth + (cmd.x + x)) * 3;
                                    
                                    // --- VIVID FILTER (Contrast Stretch) ---
                                    // We assume video is 16-235 range. We stretch it to 0-255.
                                    // Formula: (Color - 16) * 1.16
                                    // Using Fast Integer Math: (Color - 16) * 296 / 256
                                    
                                    int r = ((int)p.r - 16) * 296 >> 8;
                                    int g = ((int)p.g - 16) * 296 >> 8;
                                    int b = ((int)p.b - 16) * 296 >> 8;
                                    
                                    // Clamp values to 0-255 (prevent overflow glitches)
                                    if (r < 0) r = 0; else if (r > 255) r = 255;
                                    if (g < 0) g = 0; else if (g > 255) g = 255;
                                    if (b < 0) b = 0; else if (b > 255) b = 255;
                                    
                                    // Write BGR (The order that made the cat Orange)
                                    dst[dstIdx + 0] = (uint8_t)b; // Blue
                                    dst[dstIdx + 1] = (uint8_t)g; // Green
                                    dst[dstIdx + 2] = (uint8_t)r; // Red
                                }
                            }
                        }
                    }
                    break;
                }
                case CMD_ANIM_SAVE_BG:
                {
                    // Capture the current screen state (clean background)
                    if (g_anim_backup_buffer) {
                        anim_save_background(&fb, g_anim_backup_buffer, cmd.x, cmd.y, cmd.w, cmd.h);
                        ESP_LOGI(TAG, "Animation background saved.");
                    }
                    break;
                }
                case CMD_ANIM_RESTORE_BG:
                {
                    // Erase whatever was drawn by putting the clean background back
                    if (g_anim_backup_buffer) {
                        anim_restore_background(&fb, g_anim_backup_buffer, cmd.x, cmd.y, cmd.w, cmd.h);
                    }
                    break;
                }
                case CMD_UI_SAVE_TO_A: {
                    if (g_ui_backup_buffer_A) {
                        anim_save_background(&fb, g_ui_backup_buffer_A, cmd.x, cmd.y, cmd.w, cmd.h);
                    }
                    break;
                }
                case CMD_UI_RESTORE_FROM_A: {
                    if (g_ui_backup_buffer_A) {
                        anim_restore_background(&fb, g_ui_backup_buffer_A, cmd.x, cmd.y, cmd.w, cmd.h);
                    }
                    break;
                }
                case CMD_UI_SAVE_TO_B: {
                    if (g_ui_backup_buffer_B) {
                        anim_save_background(&fb, g_ui_backup_buffer_B, cmd.x, cmd.y, cmd.w, cmd.h);
                    }
                    break;
                }
                case CMD_UI_COPY_B_TO_A: {
                    if (g_ui_backup_buffer_A && g_ui_backup_buffer_B) {
                        // We just promoted the new icon to be the "Active" one.
                        // Copy the data we just saved in B over to A so we can restore it later.
                        memcpy(g_ui_backup_buffer_A, g_ui_backup_buffer_B, BUFFER_SIZE);
                    }
                    break;
                }
                case CMD_DRAW_ROUNDED_HAND: {
                    // x,y = Start Point
                    // w,h = End Point (We reused these fields)
                    // radius = Thickness
                    drawRoundedHand(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.radius, cmd.color);
                    break;
                }
                case CMD_DRAW_DAY_NIGHT_OVERLAY: {
                    drawDayNightOverlay(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.radius, cmd.fontSize);
                    break;
                }
            }
        }
        // There is NO vTaskDelay here.
        // xQueueReceive handles all the waiting.
    }
}

// --- PIN CONFIGURATION ---
#define I2S_BCK_IO      GPIO_NUM_39
#define I2S_WS_IO       GPIO_NUM_40 // Also known as LRCK
#define I2S_DO_IO       GPIO_NUM_41 // Also known as DIN

void i2s_task(void *args)
{
    // 1. BUFFER SETUP
    // Create a buffer for stereo samples (Left + Right)
    // 16-bit depth * 2 channels * 1024 samples
    size_t buffer_len = 1024;
    int16_t *samples = (int16_t *)calloc(buffer_len, 2 * sizeof(int16_t));
    
    // Fill buffer with a Sine Wave
    // We pre-calculate one buffer and play it repeatedly for this test
    for (int i = 0; i < buffer_len; i++) {
        float t = (float)i / SAMPLE_RATE;
        int16_t val = (int16_t)(3000.0f * sinf(2.0f * PI * WAVE_FREQ_HZ * t));
        
        // Interleaved Stereo: [Left, Right, Left, Right...]
        samples[i * 2]     = val; // Left Channel
        samples[i * 2 + 1] = val; // Right Channel
    }

    size_t bytes_written = 0;

    while (1) {
        // Write the buffer to the I2S peripheral
        // This function blocks until the DMA buffer has space
        i2s_channel_write(tx_handle, samples, buffer_len * 2 * sizeof(int16_t), &bytes_written, portMAX_DELAY);
    }
}

void createI2STask() {
    // 2. CHANNEL CONFIGURATION
        i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
        ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));

        // 3. STANDARD I2S MODE CONFIGURATION (Philips Format)
        i2s_std_config_t std_cfg = {
            .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
            .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
            .gpio_cfg = {
                .mclk = I2S_GPIO_UNUSED, // We are grounding SCK on the PCM5102, so MCLK is not needed
                .bclk = I2S_BCK_IO,
                .ws = I2S_WS_IO,
                .dout = I2S_DO_IO,
                .din = I2S_GPIO_UNUSED,  // We are only outputting audio
                .invert_flags = {
                    .mclk_inv = false,
                    .bclk_inv = false,
                    .ws_inv = false,
                },
            },
        };

        // 4. INITIALIZE AND ENABLE
        ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
        ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));

        printf("I2S initialized on BCK:%d, LRCK:%d, DIN:%d\n", I2S_BCK_IO, I2S_WS_IO, I2S_DO_IO);

        // 5. START TASK
        xTaskCreate(i2s_task, "i2s_task", 4096, NULL, 5, NULL);
}



// Define your pins here
#define PIN_NUM_CLK  14
#define PIN_NUM_CMD  15
#define PIN_NUM_D0   16
#define PIN_NUM_D1   17
#define PIN_NUM_D2   18
#define PIN_NUM_D3   13

/*
 // --- 2. SD Card (SDIO 4-bit) Pin Definitions ---
 #define SDIO_PIN_NUM_CLK      5
 #define SDIO_PIN_NUM_CMD      6
 #define SDIO_PIN_NUM_D0       7
 #define SDIO_PIN_NUM_D1       8
 #define SDIO_PIN_NUM_D2       9
 #define SDIO_PIN_NUM_D3       10
 */

void mount_sd_card()
{
    esp_err_t ret;

    // Options for mounting the filesystem.
    // If format_if_mount_failed is set to true, SD card will be partitioned and
    // formatted in case when mounting fails.
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,
        .max_files = 5,
        .allocation_unit_size = 16 * 1024
    };

    // Use settings defined above to initialize SD card and mount FATFS.
    // Note: esp_vfs_fat_sdmmc_mount is an all-in-one convenience function.
    // Please check its source code and implement error recovery when developing production applications.
    
    sdmmc_card_t *card;
    const char mount_point[] = "/sdcard";
    
    // By default, SDMMC host frequency is set to 20MHz.
    // If your wires are long (>10cm), consider lowering this to 10MHz or 400kHz.
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    host.max_freq_khz = SDMMC_FREQ_DEFAULT;
    //host.max_freq_khz = 100;

    // This is where we configure the pins
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    //slot_config.width = 4; // Using 4-bit mode
    slot_config.width = 1; // Using 4-bit mode
    
    // MAPPING THE PINS
    slot_config.clk = (gpio_num_t)PIN_NUM_CLK;
    slot_config.cmd = (gpio_num_t)PIN_NUM_CMD;
    slot_config.d0  = (gpio_num_t)PIN_NUM_D0;
    slot_config.d1  = (gpio_num_t)PIN_NUM_D1;
    slot_config.d2  = (gpio_num_t)PIN_NUM_D2;
    slot_config.d3  = (gpio_num_t)PIN_NUM_D3;
    slot_config.gpio_cd = GPIO_NUM_21;

    // CRITICAL: Enable internal pull-ups if your breakout board doesn't have them
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

    printf("Mounting filesystem...\n");
    ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            printf("Failed to mount filesystem. \n");
        } else {
            printf("Failed to initialize the card (%s). \n", esp_err_to_name(ret));
        }
        return;
    }

    printf("Filesystem mounted\n");
    sdmmc_card_print_info(stdout, card);
}

static i2c_master_bus_handle_t audio_i2c_bus_handle;

//#include "driver/i2c_master.h"
//#include "esp_log.h"

void check_pins_1_and_2(void) {
    const char *TAG = "I2C_CHECK";
    ESP_LOGW(TAG, "--- DIAGNOSTIC: Checking Pins 1 (SCL) and 2 (SDA) ---");

    // 1. Configure the Bus specifically for these pins
    i2c_master_bus_config_t conf = {
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .i2c_port = -1,          // Auto-select port
        .scl_io_num = 1,         // <--- Pin 1
        .sda_io_num = 2,         // <--- Pin 2
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true, // Attempt to use internal pullups
    };

    i2c_master_bus_handle_t bus_handle;
    esp_err_t err = i2c_new_master_bus(&conf, &bus_handle);

    if (err != ESP_OK) {
        ESP_LOGE(TAG, "CRITICAL: Could not init I2C on Pins 1 & 2. Error: %s", esp_err_to_name(err));
        return;
    }

    // 2. Run the Scan
    int devices_found = 0;
    for (uint8_t addr = 0x01; addr < 0x7F; addr++) {
        // Probe every address
        if (i2c_master_probe(bus_handle, addr, 50) == ESP_OK) {
            ESP_LOGI(TAG, "  -> SUCCESS: Found Device at 0x%02X", addr);
            devices_found++;
        }
    }

    // 3. Report Results
    if (devices_found == 0) {
        ESP_LOGE(TAG, "FAILURE: No devices found. Pins 1 & 2 are dead or wired incorrectly.");
    } else {
        ESP_LOGI(TAG, "PASSED: Found %d device(s) on Pins 1 & 2.", devices_found);
    }

    // 4. Clean up (Release pins so your real code can use them)
    i2c_del_master_bus(bus_handle);
    ESP_LOGW(TAG, "--- DIAGNOSTIC COMPLETE ---");
}

// Add these defines if not already in your file
#define ES8311_ADDR 0x18

void initEs8311(void) {
    ESP_LOGI("AUDIO", "Initializing ES8311...");

    // 1. Create the I2C Bus Handle (Standard for IDF 5.x)
    // We recreate the bus here because the diagnostic tool closed its temporary one.
    i2c_master_bus_config_t i2c_bus_config = {
        .i2c_port = -1,
        .sda_io_num = 2,  // GPIO 2
        .scl_io_num = 1,  // GPIO 1
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
    };
    
    // Create the global handle (make sure 'audio_i2c_bus_handle' is defined at top of file)
    ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &audio_i2c_bus_handle));

    // 2. Create the Device Handle
    es8311_handle_t es_handle = es8311_create(audio_i2c_bus_handle, ES8311_ADDR);

    // 3. Configure Clock (CRITICAL FIX: frequency MUST be set)
    es8311_clock_config_t clk_cfg = {
        .mclk_from_mclk_pin = true,
        .sample_frequency = 44100,
        .mclk_frequency = 11289600 // 44100 * 256
    };

    ESP_ERROR_CHECK(es8311_init(es_handle, &clk_cfg, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));

    // 4. --- THE SILENCE FIXES ---
    
    // A. Force Mic Bias ON (Powers the microphone element)
    uint8_t reg14;
    es8311_read_reg(es_handle, 0x14, &reg14);
    reg14 |= (1 << 4);
    es8311_write_reg(es_handle, 0x14, reg14);

    // B. Force Max Gain (So we can hear even faint noise)
    es8311_write_reg(es_handle, 0x16, 0xBB); // Analog Gain (+30dB)
    es8311_write_reg(es_handle, 0x17, 0xFF); // Digital Volume (0dB / Max)

    // C. Select the Correct Input Pin (THE USUAL SUSPECT)
    // Most modules use Input 1 (0x00). Some use Input 2 (0x50).
    // We will try Input 1 first. If silent, change this to 0x50.
    //es8311_write_reg(es_handle, 0x44, 0x00);
    // C. Select Input 2 (Common for breakout boards)
    // Change 0x00 to 0x50
    es8311_write_reg(es_handle, 0x44, 0x50);

    // D. Force ADC Unmute (Register 0x15)
    // Bit 6 controls mute. We must clear it to 0.
    uint8_t reg15;
    es8311_read_reg(es_handle, 0x15, &reg15);
    reg15 &= ~(1 << 6); // Clear Bit 6
    es8311_write_reg(es_handle, 0x15, reg15);

    // D. Unmute Everything
    es8311_voice_mute(es_handle, false);
    es8311_microphone_config(es_handle, false); // False = Analog Mic

    ESP_LOGI("AUDIO", "ES8311 Init Complete. Gain set to MAX.");
}

void debug_microphone_task(void *args) {
    ESP_LOGW("MIC_DEBUG", "Starting Microphone Monitor... (Check Serial Plotter!)");

    size_t chunk_size = 1024; // Bytes
    int16_t *buffer = cc_safe_alloc(1, chunk_size);
    size_t bytes_read = 0;

    while (1) {
        // 1. Read Raw Data from I2S
        // Ensure 'rx_handle' is the global handle you created in setupHybridAudio
        if (i2s_channel_read(rx_handle, buffer, chunk_size, &bytes_read, 1000) == ESP_OK) {
            
            // 2. Process the Data (Find Peak Volume)
            int32_t sum_left = 0;
            int32_t max_left = 0;
            int32_t max_right = 0;
            
            int samples = bytes_read / 2; // Total 16-bit samples
            
            // Iterate by 2 because we are in Stereo (Left, Right, Left, Right...)
            for (int i = 0; i < samples; i += 2) {
                int16_t left_val = buffer[i];
                int16_t right_val = buffer[i+1];

                if (abs(left_val) > max_left) max_left = abs(left_val);
                if (abs(right_val) > max_right) max_right = abs(right_val);
            }

            // 3. Print Results (Use Serial Plotter for cool graphs)
            // If these numbers are > 50, your mic is working.
            // If these numbers are 0, your mic is dead.
            ESP_LOGI("MIC_DEBUG", "Left Level: %ld  |  Right Level: %ld", (long)max_left, (long)max_right);
        } else {
            ESP_LOGE("MIC_DEBUG", "I2S Read Failed!");
        }
        
        // Slow down the logs so you can read them
        vTaskDelay(pdMS_TO_TICKS(100));
    }

    free(buffer);
    vTaskDelete(NULL);
}


void initializeI2S(void) {
    // --- STEP 1: INITIALIZE I2S (The "Road") ---
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
        
        // DMA Buffer Tuning for 16-bit
        // 16-bit stereo frame = 4 bytes.
        // 1024 frames * 4 bytes = 4096 bytes (This fits in the DMA limit!)
        chan_cfg.dma_desc_num = 8;
        chan_cfg.dma_frame_num = 1023;

        tx_handle = NULL;
        ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));

        i2s_std_config_t std_cfg = {
            .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
            // --- CHANGE: Set to 16-bit ---
            .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
            .gpio_cfg = {
                .mclk = GPIO_NUM_42,
                .bclk = GPIO_NUM_39,
                .ws = GPIO_NUM_40,
                .dout = GPIO_NUM_41,
                .din = GPIO_NUM_38,
            },
        };

        ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
        ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
    ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}

void testGraphics(void) {
    
}

// --- Configuration ---
#define LEDC_TIMER              LEDC_TIMER_0
#define LEDC_MODE               LEDC_LOW_SPEED_MODE // S3 only uses Low Speed Mode
#define LEDC_OUTPUT_IO          (47)                // Your requested Pin
#define LEDC_CHANNEL            LEDC_CHANNEL_0
#define LEDC_DUTY_RES           LEDC_TIMER_13_BIT   // 8192 discrete levels of brightness
#define LEDC_FREQUENCY          (4000)              // 4kHz (Safe for CN5711)

// --- Initialization Function ---
void example_ledc_init(void)
{
    // 1. Prepare and then apply the LEDC PWM timer configuration
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_MODE,
        .timer_num        = LEDC_TIMER,
        .duty_resolution  = LEDC_DUTY_RES,
        .freq_hz          = LEDC_FREQUENCY,  // Set frequency of PWM signal
        .clk_cfg          = LEDC_AUTO_CLK
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

    // 2. Prepare and then apply the LEDC PWM channel configuration
    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_MODE,
        .channel        = LEDC_CHANNEL,
        .timer_sel      = LEDC_TIMER,
        .intr_type      = LEDC_INTR_DISABLE,
        .gpio_num       = LEDC_OUTPUT_IO,
        .duty           = 0, // Set duty to 0% initially
        .hpoint         = 0
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}

// --- The Dedicated Task ---
void pwm_fade_task(void *pvParameters)
{
    uint32_t max_duty = 8191; // 2^13 - 1
    int fade_step = 100;      // How much to jump per cycle
    int delay_ms = 10;        // Speed of the fade

    ESP_LOGI(TAG, "Starting PWM Fade Loop...");

    while (1) {
        // Fade UP
        for (int duty = 0; duty <= max_duty; duty += fade_step) {
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(delay_ms));
        }

        // Fade DOWN
        for (int duty = max_duty; duty >= 0; duty -= fade_step) {
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(delay_ms));
        }

        // Wait at bottom (Off) for 1 second
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// At top of file
volatile float g_battery_level = 0.0f;

// --- CONFIGURATION ---
#define ADC_UNIT          ADC_UNIT_2
#define ADC_CHANNEL       ADC_CHANNEL_7    // GPIO 34 on original ESP32
#define ADC_ATTEN         ADC_ATTEN_DB_12
#define VOLTAGE_DIVIDER   2.0f

static const char *TAGBattery = "BATTERY_TASK";

// Represents a point on the battery curve: {Voltage, Percentage}
typedef struct {
    float voltage;
    int percentage;
} BatteryPoint;

// Standard Li-Ion Discharge Curve (approximate)
// You can tweak these voltages based on your real-world testing
static const BatteryPoint curve[] = {
    {4.20, 100},
    {4.10, 90},
    {4.00, 80},
    {3.90, 70},
    {3.80, 60},
    {3.70, 50}, // Nominal voltage is usually around 50%
    {3.65, 40},
    {3.60, 30}, // Drops quickly after this
    {3.50, 20},
    {3.40, 10},
    {3.20, 0}   // Cutoff
};

#define POINTS_COUNT (sizeof(curve) / sizeof(curve[0]))

uint8_t get_battery_percentage(float voltage)
{
    // 1. Clamp extremely high/low values
    if (voltage >= curve[0].voltage) return 100;
    if (voltage <= curve[POINTS_COUNT - 1].voltage) return 0;

    // 2. Loop through the table to find the range we are in
    for (int i = 0; i < POINTS_COUNT - 1; i++) {
        // Find the two points sandwiching our current voltage
        if (voltage <= curve[i].voltage && voltage > curve[i + 1].voltage) {
            
            float v1 = curve[i].voltage;
            float v2 = curve[i + 1].voltage;
            int p1 = curve[i].percentage;
            int p2 = curve[i + 1].percentage;

            // 3. Linear Interpolation (Math to smooth the gap)
            // This prevents the percentage from jumping 60% -> 50% instantly.
            // It will give you 58%, 57%, etc.
            float percent = p1 + ((voltage - v1) * (p2 - p1) / (v2 - v1));
            
            return (uint8_t)percent;
        }
    }

    return 0; // Should not reach here
}

// 1. The Task Function
// This contains the setup AND the infinite loop for this specific job
void battery_monitor_task(void *pvParameters)
{
    // --- SETUP PHASE (Runs once) ---
    
    // 1. Init ADC Unit (Same as before)
    adc_oneshot_unit_handle_t adc_handle;
    adc_oneshot_unit_init_cfg_t init_config = {
        .unit_id = ADC_UNIT,
    };
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle));

    // 2. Configure Channel (Same as before)
    adc_oneshot_chan_cfg_t config = {
        .bitwidth = ADC_BITWIDTH_DEFAULT,
        .atten = ADC_ATTEN,
    };
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, ADC_CHANNEL, &config));

    // 3. Setup Calibration (CHANGED FOR S3)
    adc_cali_handle_t adc_cali_handle = NULL;
    
    // S3 uses "Curve Fitting", not "Line Fitting"
    adc_cali_curve_fitting_config_t cali_config = {
        .unit_id = ADC_UNIT,
        .atten = ADC_ATTEN,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };

    bool do_calibration = false;
    // Note: Function name is also different (create_scheme_curve_fitting)
    if (adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle) == ESP_OK) {
        do_calibration = true;
        ESP_LOGI(TAG, "ADC Calibration Initialized");
    } else {
        ESP_LOGW(TAG, "ADC Calibration Failed");
    }

    // --- LOOP PHASE (Runs forever) ---
    while (1) {
        //int adc_raw = 0;
        int voltage_mv_pin = 0;
        float battery_voltage = 0;

        // Read Raw
        //ESP_ERROR_CHECK(adc_oneshot_read(adc_handle, ADC_CHANNEL, &adc_raw));

        /*if (do_calibration) {
            // Convert to Millivolts
            ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv_pin));
            
            // Apply Divider Math
            battery_voltage = (voltage_mv_pin * VOLTAGE_DIVIDER) / 1000.0f;
            
            ESP_LOGI(TAGBattery, "Battery: %.2f V (%d mV)", battery_voltage, voltage_mv_pin);
        } else {
            ESP_LOGW(TAGBattery, "Raw: %d (No Calib)", adc_raw);
        }*/
        
        uint32_t raw_sum = 0;
        const int SAMPLES = 64; // Take 64 samples to smooth out noise

        // 1. Burst Read
        for (int i = 0; i < SAMPLES; i++) {
            int temp_raw = 0;
            adc_oneshot_read(adc_handle, ADC_CHANNEL, &temp_raw);
            raw_sum += temp_raw;
            // Tiny delay helps separate noise spikes
            vTaskDelay(pdMS_TO_TICKS(100));
        }

        // 2. Average
        int adc_raw = raw_sum / SAMPLES;

        if (do_calibration) {
            // Convert the AVERAGED raw value to voltage
            ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv_pin));
            
            battery_voltage = (voltage_mv_pin * 2.0f) / 1000.0f;
            uint8_t pct = get_battery_percentage(battery_voltage);
            
            ESP_LOGI(TAG, "Battery (Avg): %.2f V | %d%%", battery_voltage, pct);
        }
        
        // Inside Task loop
        g_battery_level = battery_voltage;

        // Inside any other task
        printf("Current Level: %.2f", g_battery_level);
        
        // 3. Convert to Percentage
        uint8_t pct = get_battery_percentage(battery_voltage);
            
        ESP_LOGI(TAG, "Battery: %.2f V | %d%%", battery_voltage, pct);

        // Delay for 5 seconds (5000ms)
        // This frees up the CPU for other tasks
        vTaskDelay(pdMS_TO_TICKS(100));

        
    }
    
    // Tasks should never return. If they do, they must delete themselves.
    vTaskDelete(NULL);
}

void app_main(void)
{
    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );
    
    esp_log_level_set("CPUGraphics", ESP_LOG_VERBOSE);
    
    srand(time(NULL));
    
    //wifi_scan();
    
    CCString* testPath = ccs("/Users/chrisgalzerano/Desktop");
    
    checkAvailableMemory();
    
    mount_fatfs();
    
    list_directory_contents(MOUNT_POINT);
    
    heap_caps_print_heap_info(MALLOC_CAP_EXEC);
    
    // 2. List the contents of the mounted filesystem
    if (wl_handle != WL_INVALID_HANDLE) {
        list_directory_contents(MOUNT_POINT);
        
        // --- Example: Test writing a new file ---
        FILE *f = fopen("/fat/new_file.txt", "w");
        if (f == NULL) {
            ESP_LOGE(TAG, "Failed to open file for writing (Is WL mounted? Check partition size)");
        } else {
            fprintf(f, "This file was created by the ESP32 firmware!");
            fclose(f);
            ESP_LOGI(TAG, "Successfully wrote a test file.");
            
            // List again to show the new file
            list_directory_contents(MOUNT_POINT);
        }
    }
    
    
    //load_and_execute_program("/spiflash/test.bin");
    
#if defined(USE_ILI9488)
    //display_brightness_init();
    //display_brightness_set(0);
    //////////initialize_spi();
    //////////initialize_display();
    initialize_spi();
    initialize_display();
    //initRgbDisplay();
    g_touch_mutex = xSemaphoreCreateMutex();
    ////////ft6336u_driver_init();
    ft6336u_driver_init();
    //initialize_lvgl();
    //lvgl_touch_driver_init();
    ///////xTaskCreate(updateTouch, "updateTouch", 8192, NULL, 5, NULL);
    xTaskCreate(updateTouch, "updateTouch", 8192, NULL, 5, NULL);
    
    g_graphics_queue = xQueueCreate(40, sizeof(GraphicsCommand));
    if (g_graphics_queue == NULL) {
        ESP_LOGE(TAG, "Failed to create graphics queue!");
        return; // Halt if queue creation fails
    }
    ESP_LOGI(TAG, "Graphics queue created.");
    
    // 2. Create the graphics task
    // This creates the task, gives it a 4KB stack, pins it to Core 1
    // (so Core 0 can be used for Wi-Fi/Bluetooth if needed)
   
    
    ///////////////
    xTaskCreatePinnedToCore(
                            graphics_task,      // Function to implement the task
                            "graphics_task",    // Name of the task (for debugging)
                            8192,               // Stack size in words (4096 * 4 bytes)
                            NULL,               // Task parameters (not used)
                            5,                  // Task priority
                            NULL,               // Task handle (not used)
                            1                   // Pin to Core 1
                            );
    
    //i2c_scan_ng();
    //initialize_touch();
    //initTouch();
    
    mount_sd_card();
    
    //createI2STask();
    
    //initEs8311();
    
    //check_pins_1_and_2();
    
    initializeI2S();
    
    // 1. Setup Hardware
    example_ledc_init();

    // 2. Create the Task
    // xTaskCreate( Function, Name, Stack Size, Parameter, Priority, Handle );
    xTaskCreate(pwm_fade_task, "pwm_fade_task", 2048, NULL, 5, NULL);

    // 3. app_main is done.
    // The scheduler will now automatically switch to pwm_fade_task.
    ESP_LOGI(TAG, "Main complete, task created.");
    
    xTaskCreate(
            battery_monitor_task,   // Function to call
            "Battery_Monitor",      // Name for debugging
            4096,                   // Stack size (bytes) - 4KB is safe for printf/float
            NULL,                   // Parameter to pass (not used here)
            5,                      // Priority (1 is low, 24 is high)
            NULL                    // Task Handle (not needed unless you want to delete it later)
        );
    
    vTaskDelay(pdMS_TO_TICKS(2500));
    
    // Start the debug task
    //xTaskCreatePinnedToCore(debug_microphone_task, "mic_debug", 4096, NULL, 5, NULL, 1);

    //testCPUGraphicsBenchmark();
    
    //printf("Starting Playback...\n");
    //xTaskCreatePinnedToCore(mp3_task_wrapper, "mp3_task", 32768, NULL, 5, NULL, 0);
    //play_mp3_file("/sdcard/frair.mp3", tx_handle);
    
    vTaskDelay(pdMS_TO_TICKS(2500));
    
    /*ESP_LOGI(TAG, "Creating LVGL handler task");
     xTaskCreate(
     lvgl_task_handler, // Task function
     "lvgl_handler",    // Name of the task
     8192,              // Stack size in bytes (8KB)
     NULL,              // Task parameter
     5,                 // Task priority
     NULL               // Task handle (optional)
     );*/
    // 3. Start your LVGL UI Task
    //xTaskCreate(lvgl_ui_task1, "LVGL_UI_Task", 8192, NULL, 5, NULL);
    
#elif defined(USE_ST7789)
    initializeUIST7789();
#endif
    
    
    /*init_anim_buffer();
     vTaskDelay(pdMS_TO_TICKS(1000));
     ESP_LOGI(TAG, "CMD_ANIM_SAVE_BG");
     
     // 5. Send the Save Command
     // This tells graphics_task: "Call anim_save_background(&fb, ...) now!"
     GraphicsCommand cmd_save = {
     .cmd = CMD_ANIM_SAVE_BG,
     .x =  ANIM_X, .y = ANIM_Y, .w = ANIM_W, .h = ANIM_H
     };
     xQueueSend(g_graphics_queue, &cmd_save, portMAX_DELAY);
     ESP_LOGI(TAG, "Sent command to snapshot background.");
     
     // --- CRITICAL STEP: SYNC ---
     // We must wait for the graphics task to finish drawing the gradient
     // before we snapshot it. 500ms is plenty.
     vTaskDelay(pdMS_TO_TICKS(1000));
     
     // --- 3. NEW: Start Animation Task (Core 0) ---
     xTaskCreatePinnedToCore(
     triangle_animation_task,
     "anim_task",
     4096,
     NULL,
     4,    // Slightly lower priority than graphics/touch
     NULL,
     0     // Run on Core 0 to leave Core 1 free for rendering
     );*/
    
    /*
     // Optional: LVGL task creation is handled inside lvgl_port_init()
     joystick_mutex = xSemaphoreCreateMutex();
     
     // --- 2. Create the Joystick Reading Task ---
     xTaskCreate(
     joystick_task,            // Function that implements the task
     "JoystickReadTask",       // Text name for the task
     4096,                     // Stack size (adjust as needed)
     NULL,                     // Parameter to pass to the task
     5,                        // Priority (5 is a good default for peripherals)
     NULL                      // Task handle (not used here)
     );
     */
    
}