#include "jail_shader.h"
#include <SDL3/SDL_log.h>  // Para SDL_LogCategory, SDL_LogError, SDL_LogWarn
#include <SDL3/SDL_rect.h> // Para SDL_FPoint, SDL_Point
#include <cstring>         // Para strncmp
#include <stdexcept>       // Para runtime_error
#include <vector>          // Para vector

#ifdef __APPLE__
#include "CoreFoundation/CoreFoundation.h" // Para Core Foundation en macOS
#include <OpenGL/OpenGL.h>                 // Para OpenGL en macOS
#if ESSENTIAL_GL_PRACTICES_SUPPORT_GL3
#include <OpenGL/gl3.h>      // Para OpenGL 3 en macOS
#else                        // NO ESSENTIAL_GL_PRACTICES_SUPPORT_GL3
#include <OpenGL/gl.h>       // Para OpenGL (compatibilidad) en macOS
#endif                       // ESSENTIAL_GL_PRACTICES_SUPPORT_GL3
#else                        // SI NO ES __APPLE__
#include <SDL3/SDL_opengl.h> // Para GLuint, glTexCoord2f, glVertex2f, GLfloat
#endif                       // __APPLE__

namespace shader
{
    // Constantes
    const GLuint INVALID_SHADER_ID = 0;
    const GLuint INVALID_PROGRAM_ID = 0;
    const GLuint DEFAULT_TEXTURE_ID = 1;

    // Variables globales
    SDL_Window *win = nullptr;
    SDL_Renderer *renderer = nullptr;
    GLuint programId = 0;
    SDL_Texture *backBuffer = nullptr;
    SDL_Point win_size = {320 * 4, 256 * 4};
    SDL_FPoint tex_size = {320, 256};
    bool usingOpenGL = false;

#ifndef __APPLE__
    // Declaración de funciones de extensión de OpenGL (evitando GLEW)
    PFNGLCREATESHADERPROC glCreateShader;
    PFNGLSHADERSOURCEPROC glShaderSource;
    PFNGLCOMPILESHADERPROC glCompileShader;
    PFNGLGETSHADERIVPROC glGetShaderiv;
    PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
    PFNGLDELETESHADERPROC glDeleteShader;
    PFNGLATTACHSHADERPROC glAttachShader;
    PFNGLCREATEPROGRAMPROC glCreateProgram;
    PFNGLLINKPROGRAMPROC glLinkProgram;
    PFNGLVALIDATEPROGRAMPROC glValidateProgram;
    PFNGLGETPROGRAMIVPROC glGetProgramiv;
    PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
    PFNGLUSEPROGRAMPROC glUseProgram;
    PFNGLDELETEPROGRAMPROC glDeleteProgram;

    bool initGLExtensions()
    {
        glCreateShader = (PFNGLCREATESHADERPROC)SDL_GL_GetProcAddress("glCreateShader");
        glShaderSource = (PFNGLSHADERSOURCEPROC)SDL_GL_GetProcAddress("glShaderSource");
        glCompileShader = (PFNGLCOMPILESHADERPROC)SDL_GL_GetProcAddress("glCompileShader");
        glGetShaderiv = (PFNGLGETSHADERIVPROC)SDL_GL_GetProcAddress("glGetShaderiv");
        glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)SDL_GL_GetProcAddress("glGetShaderInfoLog");
        glDeleteShader = (PFNGLDELETESHADERPROC)SDL_GL_GetProcAddress("glDeleteShader");
        glAttachShader = (PFNGLATTACHSHADERPROC)SDL_GL_GetProcAddress("glAttachShader");
        glCreateProgram = (PFNGLCREATEPROGRAMPROC)SDL_GL_GetProcAddress("glCreateProgram");
        glLinkProgram = (PFNGLLINKPROGRAMPROC)SDL_GL_GetProcAddress("glLinkProgram");
        glValidateProgram = (PFNGLVALIDATEPROGRAMPROC)SDL_GL_GetProcAddress("glValidateProgram");
        glGetProgramiv = (PFNGLGETPROGRAMIVPROC)SDL_GL_GetProcAddress("glGetProgramiv");
        glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)SDL_GL_GetProcAddress("glGetProgramInfoLog");
        glUseProgram = (PFNGLUSEPROGRAMPROC)SDL_GL_GetProcAddress("glUseProgram");
        glDeleteProgram = (PFNGLDELETEPROGRAMPROC)SDL_GL_GetProcAddress("glDeleteProgram");

        return glCreateShader && glShaderSource && glCompileShader && glGetShaderiv &&
               glGetShaderInfoLog && glDeleteShader && glAttachShader && glCreateProgram &&
               glLinkProgram && glValidateProgram && glGetProgramiv && glGetProgramInfoLog &&
               glUseProgram && glDeleteProgram;
    }
#endif

    // Función para verificar errores de OpenGL
    void checkGLError(const char* operation)
    {
        GLenum error = glGetError();
        if (error != GL_NO_ERROR)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, 
                        "Error OpenGL en %s: 0x%x", operation, error);
        }
    }

    // Función para compilar un shader a partir de un std::string
    GLuint compileShader(const std::string &source, GLuint shader_type)
    {
        if (source.empty())
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ERROR FATAL: El código fuente del shader está vacío.");
            throw std::runtime_error("ERROR FATAL: El código fuente del shader está vacío.");
        }

        // Crear identificador del shader
        GLuint shader_id = glCreateShader(shader_type);
        if (shader_id == INVALID_SHADER_ID)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al crear el shader.");
            checkGLError("glCreateShader");
            return INVALID_SHADER_ID;
        }

        // Agregar una directiva según el tipo de shader
        std::string directive = (shader_type == GL_VERTEX_SHADER)
                                    ? "#define VERTEX\n"
                                    : "#define FRAGMENT\n";

        const char *sources[2] = {directive.c_str(), source.c_str()};

        // Especificar el código fuente del shader
        glShaderSource(shader_id, 2, sources, nullptr);
        checkGLError("glShaderSource");

        // Compilar el shader
        glCompileShader(shader_id);
        checkGLError("glCompileShader");

        // Verificar si la compilación fue exitosa
        GLint compiled_ok = GL_FALSE;
        glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compiled_ok);
        if (compiled_ok != GL_TRUE)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error en la compilación del shader (%d)!", shader_id);
            GLint log_length;
            glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &log_length);
            if (log_length > 0)
            {
                std::vector<GLchar> log(log_length);
                glGetShaderInfoLog(shader_id, log_length, &log_length, log.data());
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Registro de compilación del shader: %s", log.data());
            }
            glDeleteShader(shader_id);
            return INVALID_SHADER_ID;
        }
        return shader_id;
    }

    // Función para compilar un programa de shaders (vertex y fragment) a partir de std::string
    GLuint compileProgram(const std::string &vertex_shader_source, const std::string &fragment_shader_source)
    {
        GLuint program_id = glCreateProgram();
        if (program_id == INVALID_PROGRAM_ID)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al crear el programa de shaders.");
            checkGLError("glCreateProgram");
            return INVALID_PROGRAM_ID;
        }

        // Si el fragment shader está vacío, reutilizamos el código del vertex shader
        GLuint vertex_shader_id = compileShader(vertex_shader_source, GL_VERTEX_SHADER);
        GLuint fragment_shader_id = compileShader(fragment_shader_source.empty() ? vertex_shader_source : fragment_shader_source, GL_FRAGMENT_SHADER);

        if (vertex_shader_id != INVALID_SHADER_ID && fragment_shader_id != INVALID_SHADER_ID)
        {
            // Asociar los shaders al programa
            glAttachShader(program_id, vertex_shader_id);
            checkGLError("glAttachShader vertex");
            glAttachShader(program_id, fragment_shader_id);
            checkGLError("glAttachShader fragment");
            
            glLinkProgram(program_id);
            checkGLError("glLinkProgram");

            // Verificar el estado del enlace
            GLint isLinked = GL_FALSE;
            glGetProgramiv(program_id, GL_LINK_STATUS, &isLinked);
            if (isLinked == GL_FALSE)
            {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al enlazar el programa de shaders.");
                GLint log_length;
                glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &log_length);
                if (log_length > 0)
                {
                    std::vector<char> log(log_length);
                    glGetProgramInfoLog(program_id, log_length, &log_length, log.data());
                    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Registro de enlace del programa: %s", log.data());
                }
                glDeleteProgram(program_id);
                program_id = INVALID_PROGRAM_ID;
            }
            else
            {
                glValidateProgram(program_id);
                checkGLError("glValidateProgram");

                // Log de información del programa (solo si hay información)
                GLint log_length;
                glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &log_length);
                if (log_length > 1) // > 1 porque algunos drivers devuelven 1 para cadena vacía
                {
                    std::vector<char> log(log_length);
                    glGetProgramInfoLog(program_id, log_length, &log_length, log.data());
                    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registro de información del programa:\n%s", log.data());
                }
            }
        }
        else
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: No se pudieron compilar los shaders.");
            glDeleteProgram(program_id);
            program_id = INVALID_PROGRAM_ID;
        }

        // Limpiar los shaders (ya no son necesarios después del enlace)
        if (vertex_shader_id != INVALID_SHADER_ID)
        {
            glDeleteShader(vertex_shader_id);
        }
        if (fragment_shader_id != INVALID_SHADER_ID)
        {
            glDeleteShader(fragment_shader_id);
        }

        return program_id;
    }

    // Función para obtener el ID de textura OpenGL desde SDL3
    GLuint getTextureID(SDL_Texture* texture)
    {
        if (!texture) return DEFAULT_TEXTURE_ID;
        
        // Intentar obtener el ID de textura OpenGL desde las propiedades de SDL3
        SDL_PropertiesID props = SDL_GetTextureProperties(texture);
        GLuint textureId = 0;
        
        // Intentar diferentes nombres de propiedades según la versión de SDL3
        textureId = (GLuint)(uintptr_t)SDL_GetPointerProperty(props, "SDL.texture.opengl.texture", nullptr);
        
        // Si la primera no funciona, intentar con el nombre alternativo
        if (textureId == 0) {
            textureId = (GLuint)(uintptr_t)SDL_GetPointerProperty(props, "texture.opengl.texture", nullptr);
        }
        
        // Si aún no funciona, intentar obtener como número
        if (textureId == 0) {
            textureId = (GLuint)SDL_GetNumberProperty(props, "SDL.texture.opengl.texture", DEFAULT_TEXTURE_ID);
        }
        
        // Si ninguna funciona, usar el método manual de bindeo de textura
        if (textureId == 0) {
            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, 
                       "No se pudo obtener el ID de textura OpenGL, usando ID por defecto (%d)", DEFAULT_TEXTURE_ID);
            textureId = DEFAULT_TEXTURE_ID;
        }
        
        return textureId;
    }

    bool init(SDL_Window *window, SDL_Texture *back_buffer_texture, const std::string &vertex_shader, const std::string &fragment_shader)
    {
        shader::win = window;
        shader::renderer = SDL_GetRenderer(window);
        shader::backBuffer = back_buffer_texture;
        
        if (!shader::renderer)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: No se pudo obtener el renderer de la ventana.");
            return false;
        }

        SDL_GetWindowSize(window, &win_size.x, &win_size.y);
        SDL_GetTextureSize(back_buffer_texture, &tex_size.x, &tex_size.y);

        const auto render_name = SDL_GetRendererName(renderer);
        if (!render_name)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: No se pudo obtener el nombre del renderer.");
            return false;
        }

        // Verificar que el renderer sea OpenGL
        if (!strncmp(render_name, "opengl", 6))
        {
#ifndef __APPLE__
            if (!initGLExtensions())
            {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ERROR: No se han podido inicializar las extensiones de OpenGL.");
                usingOpenGL = false;
                return false;
            }
#endif
            // Compilar el programa de shaders utilizando std::string
            programId = compileProgram(vertex_shader, fragment_shader);
            if (programId == INVALID_PROGRAM_ID)
            {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ERROR: No se pudo compilar el programa de shaders.");
                usingOpenGL = false;
                return false;
            }
        }
        else
        {
            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "ADVERTENCIA: El driver del renderer no es OpenGL (%s).", render_name);
            usingOpenGL = false;
            return false;
        }
        
        usingOpenGL = true;
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Shader system initialized successfully.");
        return true;
    }

    void render()
    {
        // Establece el color de fondo
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_SetRenderTarget(renderer, nullptr);
        SDL_RenderClear(renderer);

        if (usingOpenGL && programId != INVALID_PROGRAM_ID)
        {
            // Guardar estados de OpenGL
            GLint oldProgramId;
            glGetIntegerv(GL_CURRENT_PROGRAM, &oldProgramId);
            
            GLint oldViewport[4];
            glGetIntegerv(GL_VIEWPORT, oldViewport);
            
            GLboolean wasTextureEnabled = glIsEnabled(GL_TEXTURE_2D);
            GLint oldTextureId;
            glGetIntegerv(GL_TEXTURE_BINDING_2D, &oldTextureId);

            // Obtener y bindear la textura
            GLuint textureId = getTextureID(backBuffer);
            glEnable(GL_TEXTURE_2D);
            glBindTexture(GL_TEXTURE_2D, textureId);
            checkGLError("glBindTexture");

            // Usar nuestro programa de shaders
            glUseProgram(programId);
            checkGLError("glUseProgram");

            // Recupera el tamaño lógico configurado con SDL_RenderSetLogicalSize
            int logicalW, logicalH;
            SDL_RendererLogicalPresentation mode;
            SDL_GetRenderLogicalPresentation(renderer, &logicalW, &logicalH, &mode);
            if (logicalW == 0 || logicalH == 0)
            {
                logicalW = win_size.x;
                logicalH = win_size.y;
            }

            // Cálculo del viewport
            int viewportX = 0, viewportY = 0, viewportW = win_size.x, viewportH = win_size.y;
            const bool USE_INTEGER_SCALE = mode == SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
            if (USE_INTEGER_SCALE)
            {
                // Calcula el factor de escalado entero máximo que se puede aplicar
                int scaleX = win_size.x / logicalW;
                int scaleY = win_size.y / logicalH;
                int scale = (scaleX < scaleY ? scaleX : scaleY);
                if (scale < 1)
                {
                    scale = 1;
                }
                viewportW = logicalW * scale;
                viewportH = logicalH * scale;
                viewportX = (win_size.x - viewportW) / 2;
                viewportY = (win_size.y - viewportH) / 2;
            }
            else
            {
                // Letterboxing: preserva la relación de aspecto usando una escala flotante
                float windowAspect = static_cast<float>(win_size.x) / win_size.y;
                float logicalAspect = static_cast<float>(logicalW) / logicalH;
                if (windowAspect > logicalAspect)
                {
                    viewportW = static_cast<int>(logicalAspect * win_size.y);
                    viewportX = (win_size.x - viewportW) / 2;
                }
                else
                {
                    viewportH = static_cast<int>(win_size.x / logicalAspect);
                    viewportY = (win_size.y - viewportH) / 2;
                }
            }
            glViewport(viewportX, viewportY, viewportW, viewportH);
            checkGLError("glViewport");

            // Configurar la proyección ortográfica usando el espacio lógico
            glMatrixMode(GL_PROJECTION);
            glLoadIdentity();

            // Queremos que el origen esté en la esquina superior izquierda del espacio lógico.
            glOrtho(0, static_cast<GLdouble>(logicalW), static_cast<GLdouble>(logicalH), 0, -1, 1);
            glMatrixMode(GL_MODELVIEW);
            glLoadIdentity();

            // Dibuja el quad con las coordenadas ajustadas.
            // Se asignan las coordenadas de textura "normales" para que no quede espejado horizontalmente,
            // y se mantiene el flip vertical para que la imagen no aparezca volteada.
            glBegin(GL_TRIANGLE_STRIP);
            // Vértice superior izquierdo
            glTexCoord2f(0.0f, 1.0f);
            glVertex2f(0.0f, 0.0f);
            // Vértice superior derecho
            glTexCoord2f(1.0f, 1.0f);
            glVertex2f(static_cast<GLfloat>(logicalW), 0.0f);
            // Vértice inferior izquierdo
            glTexCoord2f(0.0f, 0.0f);
            glVertex2f(0.0f, static_cast<GLfloat>(logicalH));
            // Vértice inferior derecho
            glTexCoord2f(1.0f, 0.0f);
            glVertex2f(static_cast<GLfloat>(logicalW), static_cast<GLfloat>(logicalH));
            glEnd();
            checkGLError("render quad");

            SDL_GL_SwapWindow(win);

            // Restaurar estados de OpenGL
            glUseProgram(oldProgramId);
            glBindTexture(GL_TEXTURE_2D, oldTextureId);
            if (!wasTextureEnabled)
            {
                glDisable(GL_TEXTURE_2D);
            }
            glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
        }
        else
        {
            // Fallback a renderizado normal de SDL
            SDL_RenderTexture(renderer, backBuffer, nullptr, nullptr);
            SDL_RenderPresent(renderer);
        }
    }

    void cleanup()
    {
        if (programId != INVALID_PROGRAM_ID)
        {
            glDeleteProgram(programId);
            programId = INVALID_PROGRAM_ID;
            SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Programa de shaders liberado.");
        }
        
        // Reinicializar variables
        win = nullptr;
        renderer = nullptr;
        backBuffer = nullptr;
        usingOpenGL = false;
    }

    bool isUsingOpenGL()
    {
        return usingOpenGL;
    }

    GLuint getProgramId()
    {
        return programId;
    }
}