// src/main.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <algorithm>
#include <type_traits>
#include <filesystem>
#include <ctime>
#include <SDL3/SDL.h>
#include <glad/glad.h>
#include "jail_audio.h"

#include "defines.hpp"

// Simple logger compatible con el estilo que usas
struct Logger {
    static void info(const std::string& s) { std::cout << "[INFO] " << s << '\n'; }
    static void error(const std::string& s) { std::cerr << "[ERROR] " << s << '\n'; }
};

// Opciones mínimas parecidas a las tuyas
struct VideoOptions {
    bool fullscreen = false;
    bool vsync = true;
} Options_video;

// Estructura para guardar info del display
struct DisplayMonitor {
    std::string name;
    int width = 0;
    int height = 0;
    int refresh_rate = 0;
};

// Forward declarations of structs
struct ShaderMetadata {
    std::string name;
    std::string author;
    std::string iChannel0;  // "BufferA", "BufferB", "none", etc.
    std::string iChannel1;
    std::string iChannel2;
    std::string iChannel3;
};

struct ShaderBuffer {
    GLuint program = 0;      // Shader program for this buffer
    GLuint fbo = 0;          // Framebuffer object
    GLuint texture = 0;      // Output texture
    std::string name;        // "BufferA", "BufferB", etc.
};

struct ShaderPass {
    std::string shaderName;          // Base name (e.g., "water")
    std::string displayName;         // Custom name from metadata
    std::string author;              // Author from metadata
    GLuint imageProgram = 0;         // Main image shader program
    std::vector<ShaderBuffer> buffers;  // BufferA, BufferB, etc.
    ShaderMetadata metadata;         // iChannel configuration
};

// Globales simplificados (tu proyecto puede integrarlo en clases)
static DisplayMonitor display_monitor_;
static SDL_Window* window_ = nullptr;

// Sistema de shaders (legacy - kept for backward compatibility with single-pass shaders)
static std::vector<std::filesystem::path> shader_list_;
static std::vector<std::string> shader_names_;    // Custom names from "// Name: XXX" comments
static std::vector<std::string> shader_authors_;  // Custom authors from "// Author: XXX" comments
static size_t current_shader_index_ = 0;
static std::filesystem::path shaders_directory_;
static GLuint current_program_ = 0;
static Uint32 shader_start_ticks_ = 0;

// Multi-pass shader system
static std::vector<ShaderPass> shader_passes_;
static int current_window_width_ = 0;
static int current_window_height_ = 0;

// Self-feedback system (for shaders that use their own output as input)
static GLuint feedback_fbo_ = 0;
static GLuint feedback_texture_ = 0;
static bool current_shader_uses_feedback_ = false;
static int feedback_channel_ = -1;  // Which iChannel (0-3) is used for feedback

// FPS tracking
static Uint32 fps_frame_count_ = 0;
static Uint32 fps_last_update_ticks_ = 0;
static float current_fps_ = 0.0f;

// Sistema de música
static std::vector<std::filesystem::path> music_list_;
static size_t current_music_index_ = 0;
static JA_Music_t* current_music_ = nullptr;

// Vertex shader embebido
static const char* vertexShaderSrc = R"glsl(
#version 330 core
layout(location = 0) in vec2 aPos;
out vec2 vUV;
void main() {
    vUV = aPos * 0.5 + 0.5;
    gl_Position = vec4(aPos, 0.0, 1.0);
}
)glsl";

// Helpers de carga
static bool loadFileToString(const std::filesystem::path& path, std::string& out) {
    std::ifstream ifs(path, std::ios::in | std::ios::binary);
    if (!ifs) return false;
    std::ostringstream ss;
    ss << ifs.rdbuf();
    out = ss.str();
    return true;
}

static std::string trimString(const std::string& str) {
    size_t start = str.find_first_not_of(" \t\r\n");
    size_t end = str.find_last_not_of(" \t\r\n");
    if (start != std::string::npos && end != std::string::npos) {
        return str.substr(start, end - start + 1);
    }
    return "";
}

static ShaderMetadata extractShaderMetadata(const std::string& shaderSource) {
    ShaderMetadata metadata;
    metadata.iChannel0 = "none";
    metadata.iChannel1 = "none";
    metadata.iChannel2 = "none";
    metadata.iChannel3 = "none";

    std::istringstream stream(shaderSource);
    std::string line;
    int lineCount = 0;
    const int maxLinesToCheck = 30;

    while (std::getline(stream, line) && lineCount < maxLinesToCheck) {
        lineCount++;

        // Look for "// XXX: YYY" patterns (case-insensitive)
        size_t pos = line.find("//");
        if (pos != std::string::npos) {
            std::string comment = line.substr(pos + 2);
            std::string commentLower = comment;
            std::transform(commentLower.begin(), commentLower.end(), commentLower.begin(), ::tolower);

            // Check for Name:
            if (commentLower.find("name:") != std::string::npos) {
                metadata.name = trimString(comment.substr(comment.find(":") + 1));
            }
            // Check for Author:
            else if (commentLower.find("author:") != std::string::npos) {
                metadata.author = trimString(comment.substr(comment.find(":") + 1));
            }
            // Check for iChannel0-3:
            else if (commentLower.find("ichannel0:") != std::string::npos) {
                metadata.iChannel0 = trimString(comment.substr(comment.find(":") + 1));
            }
            else if (commentLower.find("ichannel1:") != std::string::npos) {
                metadata.iChannel1 = trimString(comment.substr(comment.find(":") + 1));
            }
            else if (commentLower.find("ichannel2:") != std::string::npos) {
                metadata.iChannel2 = trimString(comment.substr(comment.find(":") + 1));
            }
            else if (commentLower.find("ichannel3:") != std::string::npos) {
                metadata.iChannel3 = trimString(comment.substr(comment.find(":") + 1));
            }
        }
    }

    return metadata;
}

static std::vector<std::filesystem::path> scanShaderDirectory(const std::filesystem::path& directory) {
    std::vector<std::filesystem::path> shaders;

    if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
        Logger::error("Shader directory does not exist: " + directory.string());
        return shaders;
    }

    for (const auto& entry : std::filesystem::directory_iterator(directory)) {
        if (entry.is_regular_file()) {
            auto ext = entry.path().extension().string();
            if (ext == ".glsl") {
                shaders.push_back(entry.path());
            }
        }
    }

    // Ordenar alfabéticamente
    std::sort(shaders.begin(), shaders.end());

    Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string());

    // Initialize shader metadata vectors with empty strings (will be filled when shaders are loaded)
    shader_names_.resize(shaders.size(), "");
    shader_authors_.resize(shaders.size(), "");

    return shaders;
}

static std::vector<std::filesystem::path> scanMusicDirectory(const std::filesystem::path& directory) {
    std::vector<std::filesystem::path> music_files;

    if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
        Logger::info("Music directory does not exist: " + directory.string());
        return music_files;
    }

    for (const auto& entry : std::filesystem::directory_iterator(directory)) {
        if (entry.is_regular_file()) {
            auto ext = entry.path().extension().string();
            if (ext == ".ogg") {
                music_files.push_back(entry.path());
            }
        }
    }

    // Ordenar alfabéticamente
    std::sort(music_files.begin(), music_files.end());

    Logger::info("Found " + std::to_string(music_files.size()) + " music file(s) in " + directory.string());
    return music_files;
}

static void playRandomMusic() {
    if (music_list_.empty()) return;

    // Liberar música anterior si existe
    if (current_music_) {
        JA_DeleteMusic(current_music_);
        current_music_ = nullptr;
    }

    // Elegir índice aleatorio
    current_music_index_ = rand() % music_list_.size();

    // Cargar y reproducir música (sin loop, loop=0)
    const auto& music_path = music_list_[current_music_index_];
    current_music_ = JA_LoadMusic(music_path.string().c_str());

    if (current_music_) {
        JA_PlayMusic(current_music_, 0);  // 0 = no loop, se reproduce una vez
        Logger::info("Now playing: " + music_path.filename().string());
    } else {
        Logger::error("Failed to load music: " + music_path.string());
    }
}

// ===== Multi-pass FBO/Texture Management =====

static bool createBufferFBO(ShaderBuffer& buffer, int width, int height) {
    // Create texture
    glGenTextures(1, &buffer.texture);
    glBindTexture(GL_TEXTURE_2D, buffer.texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_2D, 0);

    // Create FBO
    glGenFramebuffers(1, &buffer.fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, buffer.fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, buffer.texture, 0);

    // Check FBO completeness
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    if (status != GL_FRAMEBUFFER_COMPLETE) {
        Logger::error("FBO creation failed for " + buffer.name + ": " + std::to_string(status));
        return false;
    }

    Logger::info("Created FBO for " + buffer.name + " (" + std::to_string(width) + "x" + std::to_string(height) + ")");
    return true;
}

static void destroyBuffer(ShaderBuffer& buffer) {
    if (buffer.fbo != 0) {
        glDeleteFramebuffers(1, &buffer.fbo);
        buffer.fbo = 0;
    }
    if (buffer.texture != 0) {
        glDeleteTextures(1, &buffer.texture);
        buffer.texture = 0;
    }
    if (buffer.program != 0) {
        glDeleteProgram(buffer.program);
        buffer.program = 0;
    }
}

static void destroyShaderPass(ShaderPass& pass) {
    if (pass.imageProgram != 0) {
        glDeleteProgram(pass.imageProgram);
        pass.imageProgram = 0;
    }
    for (auto& buffer : pass.buffers) {
        destroyBuffer(buffer);
    }
    pass.buffers.clear();
}

static bool resizeBuffersIfNeeded(ShaderPass& pass, int width, int height) {
    if (current_window_width_ == width && current_window_height_ == height) {
        return false;  // No resize needed
    }

    Logger::info("Resizing buffers: " + std::to_string(width) + "x" + std::to_string(height));

    // Destroy and recreate all buffers with new size
    for (auto& buffer : pass.buffers) {
        // Keep program, destroy FBO/texture only
        if (buffer.fbo != 0) glDeleteFramebuffers(1, &buffer.fbo);
        if (buffer.texture != 0) glDeleteTextures(1, &buffer.texture);
        buffer.fbo = 0;
        buffer.texture = 0;

        if (!createBufferFBO(buffer, width, height)) {
            return false;
        }
    }

    current_window_width_ = width;
    current_window_height_ = height;
    return true;
}

// ===== Self-Feedback System =====

static bool createFeedbackFBO(int width, int height) {
    // Delete existing if any
    if (feedback_fbo_ != 0) glDeleteFramebuffers(1, &feedback_fbo_);
    if (feedback_texture_ != 0) glDeleteTextures(1, &feedback_texture_);

    // Create texture
    glGenTextures(1, &feedback_texture_);
    glBindTexture(GL_TEXTURE_2D, feedback_texture_);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    // Clear to black initially
    std::vector<float> black(width * height * 4, 0.0f);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, black.data());
    glBindTexture(GL_TEXTURE_2D, 0);

    // Create FBO
    glGenFramebuffers(1, &feedback_fbo_);
    glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, feedback_texture_, 0);

    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    if (status != GL_FRAMEBUFFER_COMPLETE) {
        Logger::error("Feedback FBO creation failed: " + std::to_string(status));
        return false;
    }

    Logger::info("Created feedback FBO (" + std::to_string(width) + "x" + std::to_string(height) + ")");
    return true;
}

static void destroyFeedbackFBO() {
    if (feedback_fbo_ != 0) {
        glDeleteFramebuffers(1, &feedback_fbo_);
        feedback_fbo_ = 0;
    }
    if (feedback_texture_ != 0) {
        glDeleteTextures(1, &feedback_texture_);
        feedback_texture_ = 0;
    }
    current_shader_uses_feedback_ = false;
    feedback_channel_ = -1;
}

static int detectFeedbackChannel(const ShaderMetadata& metadata) {
    // Check which iChannel uses "self" feedback
    if (metadata.iChannel0 == "self") return 0;
    if (metadata.iChannel1 == "self") return 1;
    if (metadata.iChannel2 == "self") return 2;
    if (metadata.iChannel3 == "self") return 3;
    return -1;  // No feedback
}

// ===== Multi-pass Shader Loading =====

// Forward declarations
static GLuint compileShader(GLenum type, const char* src);
static GLuint linkProgram(GLuint vs, GLuint fs);

static std::vector<std::string> findBufferFiles(const std::filesystem::path& imagePath) {
    std::vector<std::string> buffers;
    std::filesystem::path dir = imagePath.parent_path();
    std::string basename = imagePath.stem().stem().string();  // Remove .image.glsl -> get base name

    // Check for BufferA, BufferB, BufferC, BufferD
    std::vector<std::string> bufferNames = {"bufferA", "bufferB", "bufferC", "bufferD"};
    for (const auto& bufName : bufferNames) {
        std::filesystem::path bufferPath = dir / (basename + "." + bufName + ".glsl");
        if (std::filesystem::exists(bufferPath)) {
            buffers.push_back(bufName);
            Logger::info("Found buffer: " + bufferPath.string());
        }
    }

    return buffers;
}

static GLuint loadMultiPassShader(const std::filesystem::path& imagePath, ShaderPass& outPass, int width, int height) {
    std::string basename = imagePath.stem().stem().string();
    outPass.shaderName = basename;

    // Load and compile Image shader
    std::string imageSrc;
    if (!loadFileToString(imagePath, imageSrc)) {
        Logger::error("Failed to load image shader: " + imagePath.string());
        return 0;
    }

    // Extract metadata from Image shader
    outPass.metadata = extractShaderMetadata(imageSrc);
    outPass.displayName = outPass.metadata.name.empty() ? basename : outPass.metadata.name;
    outPass.author = outPass.metadata.author;

    GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
    GLuint fs = compileShader(GL_FRAGMENT_SHADER, imageSrc.c_str());

    if (!vs || !fs) {
        if (vs) glDeleteShader(vs);
        if (fs) glDeleteShader(fs);
        return 0;
    }

    outPass.imageProgram = linkProgram(vs, fs);
    glDeleteShader(vs);
    glDeleteShader(fs);

    if (!outPass.imageProgram) {
        return 0;
    }

    // Find and load buffer shaders
    std::vector<std::string> bufferNames = findBufferFiles(imagePath);
    std::filesystem::path dir = imagePath.parent_path();

    for (const auto& bufName : bufferNames) {
        ShaderBuffer buffer;
        buffer.name = bufName;

        // Load buffer shader source
        std::filesystem::path bufferPath = dir / (basename + "." + bufName + ".glsl");
        std::string bufferSrc;
        if (!loadFileToString(bufferPath, bufferSrc)) {
            Logger::error("Failed to load buffer: " + bufferPath.string());
            continue;
        }

        // Compile buffer shader
        GLuint bufVs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
        GLuint bufFs = compileShader(GL_FRAGMENT_SHADER, bufferSrc.c_str());

        if (!bufVs || !bufFs) {
            if (bufVs) glDeleteShader(bufVs);
            if (bufFs) glDeleteShader(bufFs);
            continue;
        }

        buffer.program = linkProgram(bufVs, bufFs);
        glDeleteShader(bufVs);
        glDeleteShader(bufFs);

        if (!buffer.program) {
            continue;
        }

        // Create FBO and texture for this buffer
        if (!createBufferFBO(buffer, width, height)) {
            glDeleteProgram(buffer.program);
            continue;
        }

        outPass.buffers.push_back(buffer);
        Logger::info("Loaded buffer: " + bufName);
    }

    Logger::info("Multi-pass shader loaded: " + outPass.displayName + " (" + std::to_string(outPass.buffers.size()) + " buffers)");
    return outPass.imageProgram;
}

static void updateWindowTitle() {
    if (!window_ || shader_list_.empty()) return;

    // Use custom shader name if available, otherwise fallback to filename
    std::string shaderName;
    if (!shader_names_.empty() && !shader_names_[current_shader_index_].empty()) {
        shaderName = shader_names_[current_shader_index_];
    } else {
        shaderName = shader_list_[current_shader_index_].filename().string();
    }

    // Add author if available
    if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) {
        shaderName += " by " + shader_authors_[current_shader_index_];
    }

    std::string title = std::string(APP_NAME) + " (" + shaderName + ")";

    if (current_fps_ > 0.0f) {
        title += " - " + std::to_string(static_cast<int>(current_fps_ + 0.5f)) + " FPS";
    }

    title += Options_video.vsync ? " [VSync ON]" : " [VSync OFF]";

    SDL_SetWindowTitle(window_, title.c_str());
}

static GLuint compileShader(GLenum type, const char* src) {
    GLuint s = glCreateShader(type);
    glShaderSource(s, 1, &src, nullptr);
    glCompileShader(s);
    GLint ok = 0;
    glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        GLint len = 0;
        glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len);
        std::string log(len > 0 ? len : 1, ' ');
        glGetShaderInfoLog(s, len, nullptr, &log[0]);
        Logger::error("Shader compile error: " + log);
        glDeleteShader(s);
        return 0;
    }
    return s;
}

static GLuint linkProgram(GLuint vs, GLuint fs) {
    GLuint p = glCreateProgram();
    glAttachShader(p, vs);
    glAttachShader(p, fs);
    glLinkProgram(p);
    GLint ok = 0;
    glGetProgramiv(p, GL_LINK_STATUS, &ok);
    if (!ok) {
        GLint len = 0;
        glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len);
        std::string log(len > 0 ? len : 1, ' ');
        glGetProgramInfoLog(p, len, nullptr, &log[0]);
        Logger::error("Program link error: " + log);
        glDeleteProgram(p);
        return 0;
    }
    return p;
}

static GLuint loadAndCompileShader(size_t index) {
    if (index >= shader_list_.size()) {
        Logger::error("Invalid shader index: " + std::to_string(index));
        return 0;
    }

    const auto& shaderPath = shader_list_[index];
    Logger::info("Loading shader: " + shaderPath.string());

    std::string fragSrc;
    if (!loadFileToString(shaderPath, fragSrc)) {
        Logger::error("Failed to load shader file: " + shaderPath.string());
        return 0;
    }

    // Extract custom shader metadata (name, author, iChannels) from source code
    ShaderMetadata metadata = extractShaderMetadata(fragSrc);
    if (!metadata.name.empty()) {
        shader_names_[index] = metadata.name;
        Logger::info("Shader name: " + metadata.name);
    }
    if (!metadata.author.empty()) {
        shader_authors_[index] = metadata.author;
        Logger::info("Shader author: " + metadata.author);
    }

    // Detect self-feedback
    feedback_channel_ = detectFeedbackChannel(metadata);
    current_shader_uses_feedback_ = (feedback_channel_ >= 0);

    if (current_shader_uses_feedback_) {
        Logger::info("Shader uses self-feedback on iChannel" + std::to_string(feedback_channel_));
    }

    GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
    GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str());

    if (!vs || !fs) {
        if (vs) glDeleteShader(vs);
        if (fs) glDeleteShader(fs);
        Logger::error("Shader compilation failed for: " + shaderPath.string());
        return 0;
    }

    GLuint program = linkProgram(vs, fs);
    glDeleteShader(vs);
    glDeleteShader(fs);

    if (!program) {
        Logger::error("Program linking failed for: " + shaderPath.string());
        return 0;
    }

    Logger::info("Shader loaded successfully: " + shaderPath.filename().string());
    return program;
}

// --- Funciones basadas en tu código ---
void getDisplayInfo() {
    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    if (displays != nullptr && num_displays > 0) {
        for (int i = 0; i < num_displays; ++i) {
            SDL_DisplayID instance_id = displays[i];
            const char* name = SDL_GetDisplayName(instance_id);
            Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown"));
        }

        const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(displays[0]);
        const char* first_display_name = SDL_GetDisplayName(displays[0]);
        display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
        if (dm) {
            display_monitor_.width = static_cast<int>(dm->w);
            display_monitor_.height = static_cast<int>(dm->h);
            display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
        } else {
            Logger::info("SDL_GetCurrentDisplayMode returned null");
        }
    } else {
        Logger::info("No displays found or SDL_GetDisplays failed");
    }
}

void setFullscreenMode() {
    if (!window_) return;

    if (Options_video.fullscreen) {
        // Intentar fullscreen
        if (!SDL_SetWindowFullscreen(window_, true)) {
            // Fallback: volver a modo ventana si falla
            Logger::error(std::string("Failed to set fullscreen: ") + SDL_GetError());
            Logger::info("Fallback to windowed mode 800x800");
            SDL_SetWindowFullscreen(window_, false);
            SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
            Options_video.fullscreen = false;
            SDL_ShowCursor();  // Show cursor on fallback to windowed
        } else {
            SDL_HideCursor();  // Hide cursor in fullscreen
        }
    } else {
        // Volver a modo ventana 800x800
        SDL_SetWindowFullscreen(window_, false);
        SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
        SDL_ShowCursor();  // Show cursor in windowed mode
    }
}

void toggleFullscreen() {
    Options_video.fullscreen = !Options_video.fullscreen;
    setFullscreenMode();
}

void toggleVSync() {
    Options_video.vsync = !Options_video.vsync;
    int result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0);

    if (result == 0) {
        Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled");
    } else {
        Logger::error(std::string("Failed to set VSync: ") + SDL_GetError());
    }
}

void switchShader(int direction) {
    if (shader_list_.empty()) return;

    // Calcular nuevo índice con wrap-around cíclico
    size_t new_index = current_shader_index_;
    if (direction > 0) {
        new_index = (current_shader_index_ + 1) % shader_list_.size();
    } else if (direction < 0) {
        new_index = (current_shader_index_ == 0) ? shader_list_.size() - 1 : current_shader_index_ - 1;
    }

    // Intentar cargar el nuevo shader
    GLuint new_program = loadAndCompileShader(new_index);
    if (new_program == 0) {
        Logger::error("Failed to switch shader, keeping current one");
        return;
    }

    // Éxito: eliminar programa anterior y actualizar
    if (current_program_ != 0) {
        glDeleteProgram(current_program_);
    }

    // Destroy feedback FBO from previous shader
    destroyFeedbackFBO();

    current_program_ = new_program;
    current_shader_index_ = new_index;
    shader_start_ticks_ = SDL_GetTicks();

    updateWindowTitle();
}

// Manejo de teclas
void handleDebugEvents(const SDL_Event& event) {
    // evitar repetición de teclas: event.key.repeat disponible en SDL3
    if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
        switch (event.key.key) {
            case SDLK_F3: {
                toggleFullscreen();
                break;
            }
            case SDLK_F4: {
                toggleVSync();
                break;
            }
            case SDLK_LEFT: {
                switchShader(-1);
                break;
            }
            case SDLK_RIGHT: {
                switchShader(+1);
                break;
            }
            default:
                break;
        }
    }
}

// --- main (integra todo y soporta -F y argv[1] para shader path) ---
int main(int argc, char** argv) {
    std::string shaderPath;
    bool fullscreenFlag = false;
    for (int i = 1; i < argc; ++i) {
        std::string a = argv[i];
        if (a == "-F" || a == "--fullscreen") { fullscreenFlag = true; continue; }
        if (shaderPath.empty()) shaderPath = a;
    }
    if (shaderPath.empty()) shaderPath = "test.frag.glsl";
    Options_video.fullscreen = fullscreenFlag;

    // Inicializar SDL3
    auto initResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    if constexpr (std::is_same_v<decltype(initResult), bool>) {
        if (!initResult) { Logger::error(SDL_GetError()); return -1; }
    } else {
        if (initResult != 0) { Logger::error(SDL_GetError()); return -1; }
    }

    // Obtener información del display antes de crear ventana
    getDisplayInfo();

    // Atributos GL
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    // Crear ventana
    window_ = SDL_CreateWindow(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!window_) { Logger::error(std::string("SDL_CreateWindow error: ") + SDL_GetError()); SDL_Quit(); return -1; }

    // Aplicar fullscreen si el flag estaba activado
    setFullscreenMode();

    // Crear contexto GL
    SDL_GLContext glContext = SDL_GL_CreateContext(window_);
    if (!glContext) {
        Logger::error(std::string("SDL_GL_CreateContext error: ") + SDL_GetError());
        SDL_DestroyWindow(window_);
        SDL_Quit();
        return -1;
    }

    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
        Logger::error("Failed to initialize GL loader");
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window_);
        SDL_Quit();
        return -1;
    }

    // Set initial vsync state
    int vsync_result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0);
    if (vsync_result == 0) {
        Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled");
    } else {
        Logger::error(std::string("Failed to set initial VSync: ") + SDL_GetError());
    }

    // Inicializar jail_audio
    JA_Init(48000, SDL_AUDIO_S16LE, 2);

    // Obtener directorio de recursos
    std::string resources_dir = getResourcesDirectory();

    // Inicializar generador de números aleatorios
    srand(static_cast<unsigned int>(time(nullptr)));

    // Escanear directorio de música
    std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music";
    music_list_ = scanMusicDirectory(music_directory);

    // Reproducir primera canción aleatoria
    if (!music_list_.empty()) {
        playRandomMusic();
    } else {
        Logger::info("No music files found in " + music_directory.string());
    }

    // Determinar carpeta de shaders
    std::filesystem::path shaderFile(shaderPath);
    if (shaderFile.has_parent_path()) {
        shaders_directory_ = shaderFile.parent_path();
    } else {
        shaders_directory_ = std::filesystem::path(resources_dir) / "shaders";
    }

    // Escanear carpeta de shaders
    shader_list_ = scanShaderDirectory(shaders_directory_);
    if (shader_list_.empty()) {
        Logger::error("No shaders found in directory: " + shaders_directory_.string());
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window_);
        SDL_Quit();
        return -1;
    }

    // Determinar shader inicial
    size_t initial_index = 0;
    bool found_shader = false;

    // Intentar encontrar el shader especificado
    std::filesystem::path target_shader = shaderFile.has_parent_path() ? shaderFile : (shaders_directory_ / shaderFile.filename());
    for (size_t i = 0; i < shader_list_.size(); ++i) {
        if (shader_list_[i] == target_shader) {
            initial_index = i;
            found_shader = true;
            break;
        }
    }

    // Si no se encuentra, intentar con shader por defecto (test.frag.glsl)
    if (!found_shader) {
        std::filesystem::path default_path = std::filesystem::path(resources_dir) / "shaders" / "test.frag.glsl";
        for (size_t i = 0; i < shader_list_.size(); ++i) {
            if (shader_list_[i] == default_path) {
                initial_index = i;
                found_shader = true;
                break;
            }
        }
    }

    // Si aún no se encuentra, usar el primer shader de la lista
    if (!found_shader) {
        Logger::info("Specified shader not found, using first shader in directory");
        initial_index = 0;
    }

    // Cargar shader inicial
    current_shader_index_ = initial_index;
    current_program_ = loadAndCompileShader(current_shader_index_);
    if (current_program_ == 0) {
        Logger::error("Failed to load initial shader");
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window_);
        SDL_Quit();
        return -1;
    }

    shader_start_ticks_ = SDL_GetTicks();
    fps_last_update_ticks_ = SDL_GetTicks();
    updateWindowTitle();

    // Quad setup
    float quadVertices[] = {
        -1.0f, -1.0f,
         1.0f, -1.0f,
        -1.0f,  1.0f,
         1.0f,  1.0f,
    };

    GLuint vao = 0, vbo = 0;
    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &vbo);
    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    bool running = true;

    while (running) {
        // Update FPS counter
        fps_frame_count_++;
        Uint32 current_ticks = SDL_GetTicks();

        // Update FPS display every 500ms
        if (current_ticks - fps_last_update_ticks_ >= 500) {
            float elapsed_seconds = (current_ticks - fps_last_update_ticks_) / 1000.0f;
            current_fps_ = fps_frame_count_ / elapsed_seconds;
            fps_frame_count_ = 0;
            fps_last_update_ticks_ = current_ticks;
            updateWindowTitle();
        }

        // Actualizar audio (necesario para streaming y loops)
        JA_Update();

        // Verificar si la música actual terminó y reproducir siguiente aleatoria
        if (!music_list_.empty() && JA_GetMusicState() == JA_MUSIC_STOPPED) {
            playRandomMusic();
        }

        SDL_Event e;
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_EVENT_QUIT) running = false;
            else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) running = false;
            else if (e.type == SDL_EVENT_KEY_DOWN) {
                // Escape cierra la app
                if (e.key.key == SDLK_ESCAPE) running = false;
                // handle your debug keys
                handleDebugEvents(e);
            } else if (e.type == SDL_EVENT_WINDOW_RESIZED) {
                // opcional: podrías actualizar algo con new size
            }
        }

        int w, h;
        SDL_GetWindowSize(window_, &w, &h);

        // Create/resize feedback FBO if needed
        if (current_shader_uses_feedback_) {
            if (feedback_fbo_ == 0 || current_window_width_ != w || current_window_height_ != h) {
                createFeedbackFBO(w, h);
                current_window_width_ = w;
                current_window_height_ = h;
            }
        }

        glUseProgram(current_program_);

        // Obtener uniform locations
        GLint locRes = glGetUniformLocation(current_program_, "iResolution");
        GLint locTime = glGetUniformLocation(current_program_, "iTime");

        float t = (SDL_GetTicks() - shader_start_ticks_) / 1000.0f;

        // === FEEDBACK RENDERING ===
        if (current_shader_uses_feedback_) {
            // Step 1: Bind feedback texture to iChannel
            std::string channelName = "iChannel" + std::to_string(feedback_channel_);
            GLint locChannel = glGetUniformLocation(current_program_, channelName.c_str());

            if (locChannel >= 0) {
                glActiveTexture(GL_TEXTURE0 + feedback_channel_);
                glBindTexture(GL_TEXTURE_2D, feedback_texture_);
                glUniform1i(locChannel, feedback_channel_);
            }

            // Step 2: Render to feedback FBO
            glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_);
            glViewport(0, 0, w, h);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);

            if (locRes >= 0) glUniform2f(locRes, float(w), float(h));
            if (locTime >= 0) glUniform1f(locTime, t);

            glBindVertexArray(vao);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            glBindVertexArray(0);

            // Step 3: Render to screen (using the same FBO texture)
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glViewport(0, 0, w, h);

            if (locRes >= 0) glUniform2f(locRes, float(w), float(h));
            if (locTime >= 0) glUniform1f(locTime, t);

            glBindVertexArray(vao);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            glBindVertexArray(0);

            // Unbind texture
            glActiveTexture(GL_TEXTURE0 + feedback_channel_);
            glBindTexture(GL_TEXTURE_2D, 0);
        } else {
            // === NORMAL RENDERING (no feedback) ===
            glViewport(0, 0, w, h);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);

            if (locRes >= 0) glUniform2f(locRes, float(w), float(h));
            if (locTime >= 0) glUniform1f(locTime, t);

            glBindVertexArray(vao);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            glBindVertexArray(0);
        }

        SDL_GL_SwapWindow(window_);
        if (!Options_video.vsync) {
            SDL_Delay(1);  // Prevent CPU spinning when vsync is off
        }
    }

    // Cleanup
    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
    if (current_program_ != 0) {
        glDeleteProgram(current_program_);
    }

    // Cleanup feedback FBO
    destroyFeedbackFBO();

    // Cleanup audio
    if (current_music_) {
        JA_DeleteMusic(current_music_);
        current_music_ = nullptr;
    }
    JA_Quit();

    SDL_GL_DestroyContext(glContext);
    SDL_DestroyWindow(window_);
    SDL_Quit();
    return 0;
}
