#include "opengl_shader.hpp"

#include <SDL3/SDL.h>

#include <cstring>
#include <stdexcept>
#include <vector>

#include "ui/logger.hpp"  // Para Loger

namespace Rendering {

OpenGLShader::~OpenGLShader() {
    cleanup();
}

#ifndef __APPLE__
bool OpenGLShader::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");
    glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)SDL_GL_GetProcAddress("glGetUniformLocation");
    glUniform2f = (PFNGLUNIFORM2FPROC)SDL_GL_GetProcAddress("glUniform2f");
    glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glGenVertexArrays");
    glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)SDL_GL_GetProcAddress("glBindVertexArray");
    glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glDeleteVertexArrays");
    glGenBuffers = (PFNGLGENBUFFERSPROC)SDL_GL_GetProcAddress("glGenBuffers");
    glBindBuffer = (PFNGLBINDBUFFERPROC)SDL_GL_GetProcAddress("glBindBuffer");
    glBufferData = (PFNGLBUFFERDATAPROC)SDL_GL_GetProcAddress("glBufferData");
    glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)SDL_GL_GetProcAddress("glDeleteBuffers");
    glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)SDL_GL_GetProcAddress("glVertexAttribPointer");
    glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)SDL_GL_GetProcAddress("glEnableVertexAttribArray");

    return glCreateShader && glShaderSource && glCompileShader && glGetShaderiv &&
        glGetShaderInfoLog && glDeleteShader && glAttachShader && glCreateProgram &&
        glLinkProgram && glValidateProgram && glGetProgramiv && glGetProgramInfoLog &&
        glUseProgram && glDeleteProgram && glGetUniformLocation && glUniform2f &&
        glGenVertexArrays && glBindVertexArray && glDeleteVertexArrays &&
        glGenBuffers && glBindBuffer && glBufferData && glDeleteBuffers &&
        glVertexAttribPointer && glEnableVertexAttribArray;
}
#endif

void OpenGLShader::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);
    }
}

GLuint OpenGLShader::compileShader(const std::string& source, GLenum shader_type) {
    if (source.empty()) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "ERROR: El código fuente del shader está vacío");
        return 0;
    }

    GLuint shader_id = glCreateShader(shader_type);
    if (shader_id == 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al crear shader");
        checkGLError("glCreateShader");
        return 0;
    }

    const char* sources[1] = {source.c_str()};
    glShaderSource(shader_id, 1, sources, nullptr);
    checkGLError("glShaderSource");

    glCompileShader(shader_id);
    checkGLError("glCompileShader");

    // Verificar compilación
    GLint compiled = GL_FALSE;
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compiled);
    if (compiled != GL_TRUE) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error en compilación del shader");
        GLint log_length;
        glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &log_length);
        if (log_length > 0) {
            std::vector<char> log(log_length);
            glGetShaderInfoLog(shader_id, log_length, &log_length, log.data());
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                "Log de compilación: %s",
                log.data());
        }
        glDeleteShader(shader_id);
        return 0;
    }

    return shader_id;
}

GLuint OpenGLShader::linkProgram(GLuint vertex_shader, GLuint fragment_shader) {
    GLuint program = glCreateProgram();
    if (program == 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error al crear programa de shaders");
        return 0;
    }

    glAttachShader(program, vertex_shader);
    checkGLError("glAttachShader(vertex)");
    glAttachShader(program, fragment_shader);
    checkGLError("glAttachShader(fragment)");

    glLinkProgram(program);
    checkGLError("glLinkProgram");

    // Verificar enlace
    GLint linked = GL_FALSE;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (linked != GL_TRUE) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error al enlazar programa");
        GLint log_length;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length);
        if (log_length > 0) {
            std::vector<char> log(log_length);
            glGetProgramInfoLog(program, log_length, &log_length, log.data());
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                "Log de enlace: %s",
                log.data());
        }
        glDeleteProgram(program);
        return 0;
    }

    glValidateProgram(program);
    checkGLError("glValidateProgram");

    return program;
}

void OpenGLShader::createQuadGeometry() {
    // Datos del quad: posición (x, y) + coordenadas de textura (u, v)
    // Formato: x, y, u, v
    float vertices[] = {
        // Posición      // TexCoords
        -1.0f,
        -1.0f,
        0.0f,
        0.0f,  // Inferior izquierda
        1.0f,
        -1.0f,
        1.0f,
        0.0f,  // Inferior derecha
        1.0f,
        1.0f,
        1.0f,
        1.0f,  // Superior derecha
        -1.0f,
        1.0f,
        0.0f,
        1.0f  // Superior izquierda
    };

    // Índices para dibujar el quad con dos triángulos
    unsigned int indices[] = {
        0,
        1,
        2,  // Primer triángulo
        2,
        3,
        0  // Segundo triángulo
    };

    // Generar y configurar VAO
    glGenVertexArrays(1, &vao_);
    glBindVertexArray(vao_);
    checkGLError("glBindVertexArray");

    // Generar y configurar VBO
    glGenBuffers(1, &vbo_);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    checkGLError("glBufferData(VBO)");

    // Generar y configurar EBO
    glGenBuffers(1, &ebo_);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    checkGLError("glBufferData(EBO)");

    // Atributo 0: Posición (2 floats)
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    checkGLError("glVertexAttribPointer(position)");

    // Atributo 1: Coordenadas de textura (2 floats)
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
    glEnableVertexAttribArray(1);
    checkGLError("glVertexAttribPointer(texcoord)");

    // Desvincular
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

GLuint OpenGLShader::getTextureID(SDL_Texture* texture) {
    if (!texture) return 1;

    SDL_PropertiesID props = SDL_GetTextureProperties(texture);
    GLuint texture_id = 0;

    // Intentar obtener ID de textura OpenGL
    texture_id = (GLuint)(uintptr_t)SDL_GetPointerProperty(props, "SDL.texture.opengl.texture", nullptr);

    if (texture_id == 0) {
        texture_id = (GLuint)(uintptr_t)SDL_GetPointerProperty(props, "texture.opengl.texture", nullptr);
    }

    if (texture_id == 0) {
        texture_id = (GLuint)SDL_GetNumberProperty(props, "SDL.texture.opengl.texture", 1);
    }

    if (texture_id == 0) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
            "No se pudo obtener ID de textura OpenGL, usando 1 por defecto");
        texture_id = 1;
    }

    return texture_id;
}

bool OpenGLShader::init(SDL_Window* window,
    SDL_Texture* texture,
    const std::string& vertex_source,
    const std::string& fragment_source) {
    window_ = window;
    back_buffer_ = texture;
    renderer_ = SDL_GetRenderer(window);

    if (!renderer_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error: No se pudo obtener el renderer");
        return false;
    }

    // Obtener tamaños
    SDL_GetWindowSize(window_, &window_width_, &window_height_);
    SDL_GetTextureSize(back_buffer_, &texture_width_, &texture_height_);

    /*
    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
        "Inicializando shaders: ventana=%dx%d, textura=%.0fx%.0f",
        window_width_,
        window_height_,
        texture_width_,
        texture_height_);
    */

    Logger::info(
        "Inicializando shaders: ventana=" +
        std::to_string(window_width_) +
        "x" +
        std::to_string(window_height_) +
        ", textura=" +
        std::to_string(static_cast<int>(texture_width_)) +
        "x" +
        std::to_string(static_cast<int>(texture_height_)));

    // Verificar que es OpenGL
    const char* renderer_name = SDL_GetRendererName(renderer_);
    if (!renderer_name || strncmp(renderer_name, "opengl", 6) != 0) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
            "Renderer no es OpenGL: %s",
            renderer_name ? renderer_name : "unknown");
        return false;
    }

#ifndef __APPLE__
    // Inicializar extensiones OpenGL en Windows/Linux
    if (!initGLExtensions()) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error al inicializar extensiones OpenGL");
        return false;
    }
#endif

    // Limpiar shader anterior si existe
    if (program_id_ != 0) {
        glDeleteProgram(program_id_);
        program_id_ = 0;
    }

    // Compilar shaders
    GLuint vertex_shader = compileShader(vertex_source, GL_VERTEX_SHADER);
    GLuint fragment_shader = compileShader(fragment_source, GL_FRAGMENT_SHADER);

    if (vertex_shader == 0 || fragment_shader == 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error al compilar shaders");
        if (vertex_shader != 0) glDeleteShader(vertex_shader);
        if (fragment_shader != 0) glDeleteShader(fragment_shader);
        return false;
    }

    // Enlazar programa
    program_id_ = linkProgram(vertex_shader, fragment_shader);

    // Limpiar shaders (ya no necesarios tras el enlace)
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);

    if (program_id_ == 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "Error al crear programa de shaders");
        return false;
    }

    // Crear geometría del quad
    createQuadGeometry();

    // Obtener ubicación del uniform TextureSize
    glUseProgram(program_id_);
    texture_size_location_ = glGetUniformLocation(program_id_, "TextureSize");
    if (texture_size_location_ != -1) {
        /*
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
            "Configurando TextureSize uniform: %.0fx%.0f",
            texture_width_,
            texture_height_);
        */
        Logger::info(
            "Configurando TextureSize uniform: " +
            std::to_string(static_cast<int>(texture_width_)) +
            "x" +
            std::to_string(static_cast<int>(texture_height_)));
        glUniform2f(texture_size_location_, texture_width_, texture_height_);
        checkGLError("glUniform2f(TextureSize)");
    } else {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
            "Uniform 'TextureSize' no encontrado en shader");
    }
    glUseProgram(0);

    is_initialized_ = true;
    /*
    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
        "** OpenGL 3.3 Shader Backend inicializado correctamente");
    */
    Logger::info("OpenGL 3.3 Shader Backend inicializado correctamente");
    return true;
}

void OpenGLShader::render() {
    if (!is_initialized_ || program_id_ == 0) {
        // Fallback: renderizado SDL normal
        SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
        SDL_SetRenderTarget(renderer_, nullptr);
        SDL_RenderClear(renderer_);
        SDL_RenderTexture(renderer_, back_buffer_, nullptr, nullptr);
        SDL_RenderPresent(renderer_);
        return;
    }

    // Obtener tamaño actual de ventana (puede haber cambiado)
    int current_width, current_height;
    SDL_GetWindowSize(window_, &current_width, &current_height);

    // Guardar estados OpenGL
    GLint old_program;
    glGetIntegerv(GL_CURRENT_PROGRAM, &old_program);

    GLint old_viewport[4];
    glGetIntegerv(GL_VIEWPORT, old_viewport);

    GLboolean was_texture_enabled = glIsEnabled(GL_TEXTURE_2D);
    GLint old_texture;
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture);

    GLint old_vao;
    glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &old_vao);

    // Preparar renderizado
    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
    SDL_SetRenderTarget(renderer_, nullptr);
    SDL_RenderClear(renderer_);

    // Obtener y bindear textura
    GLuint texture_id = getTextureID(back_buffer_);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, texture_id);
    checkGLError("glBindTexture");

    // Usar nuestro programa
    glUseProgram(program_id_);
    checkGLError("glUseProgram");

    // Configurar viewport (obtener tamaño lógico de SDL)
    int logical_w, logical_h;
    SDL_RendererLogicalPresentation mode;
    SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &mode);

    if (logical_w == 0 || logical_h == 0) {
        logical_w = current_width;
        logical_h = current_height;
    }

    // Calcular viewport considerando aspect ratio
    int viewport_x = 0, viewport_y = 0;
    int viewport_w = current_width, viewport_h = current_height;

    if (mode == SDL_LOGICAL_PRESENTATION_INTEGER_SCALE) {
        int scale_x = current_width / logical_w;
        int scale_y = current_height / logical_h;
        int scale = (scale_x < scale_y) ? scale_x : scale_y;
        if (scale < 1) scale = 1;

        viewport_w = logical_w * scale;
        viewport_h = logical_h * scale;
        viewport_x = (current_width - viewport_w) / 2;
        viewport_y = (current_height - viewport_h) / 2;
    } else {
        float window_aspect = static_cast<float>(current_width) / current_height;
        float logical_aspect = static_cast<float>(logical_w) / logical_h;

        if (window_aspect > logical_aspect) {
            viewport_w = static_cast<int>(logical_aspect * current_height);
            viewport_x = (current_width - viewport_w) / 2;
        } else {
            viewport_h = static_cast<int>(current_width / logical_aspect);
            viewport_y = (current_height - viewport_h) / 2;
        }
    }

    glViewport(viewport_x, viewport_y, viewport_w, viewport_h);
    checkGLError("glViewport");

    // Dibujar quad usando VAO
    glBindVertexArray(vao_);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    checkGLError("glDrawElements");

    // Presentar
    SDL_GL_SwapWindow(window_);

    // Restaurar estados OpenGL
    glUseProgram(old_program);
    glBindTexture(GL_TEXTURE_2D, old_texture);
    if (!was_texture_enabled) {
        glDisable(GL_TEXTURE_2D);
    }
    glBindVertexArray(old_vao);
    glViewport(old_viewport[0], old_viewport[1], old_viewport[2], old_viewport[3]);
}

void OpenGLShader::setTextureSize(float width, float height) {
    if (!is_initialized_ || program_id_ == 0) {
        return;
    }

    texture_width_ = width;
    texture_height_ = height;

    GLint old_program;
    glGetIntegerv(GL_CURRENT_PROGRAM, &old_program);

    glUseProgram(program_id_);

    if (texture_size_location_ != -1) {
        glUniform2f(texture_size_location_, width, height);
        checkGLError("glUniform2f(TextureSize)");
    }

    glUseProgram(old_program);
}

void OpenGLShader::cleanup() {
    if (vao_ != 0) {
        glDeleteVertexArrays(1, &vao_);
        vao_ = 0;
    }

    if (vbo_ != 0) {
        glDeleteBuffers(1, &vbo_);
        vbo_ = 0;
    }

    if (ebo_ != 0) {
        glDeleteBuffers(1, &ebo_);
        ebo_ = 0;
    }

    if (program_id_ != 0) {
        glDeleteProgram(program_id_);
        program_id_ = 0;
    }

    is_initialized_ = false;
    window_ = nullptr;
    renderer_ = nullptr;
    back_buffer_ = nullptr;
}

}  // namespace Rendering
