#include "yuyv.h"

YUYV::YUYV()
{
    error err;

    // Vertex shader
    auto vertexSource = "attribute vec4 a_position;"
                        "attribute vec2 a_texCoord;"
                        "varying vec2 v_texCoord;"
                        "void main() {"
                        " gl_Position = a_position;"
                        " v_texCoord = a_texCoord;"
                        "}\n";

    // Fragment shader
    auto fragmentSource = "varying vec2 v_texCoord;"
                          " uniform sampler2D y_texture;"
                          " uniform sampler2D uv_texture;"
                          "\n"
                          "void main (void){"
                          " float r, g, b, y, u, v;"
                          " y = texture2D(y_texture, v_texCoord).r;"
                          " u = texture2D(uv_texture, v_texCoord).g - 0.5;"
                          " v = texture2D(uv_texture, v_texCoord).a - 0.5;"
                          "\n"
                          " r = y + 1.13983*v;"
                          " g = y - 0.39465*u - 0.58060*v;"
                          " b = y + 2.03211*u;"
                          "\n"
                          " gl_FragColor = vec4(r, g, b, 1.0);"
                          "}\n";

    // Create and compile shaders
    err = loadProgram(vertexSource, fragmentSource, &this->shaderProgram);
    if (err != "") {
        this->err = err;
        return;
    }

    // Specify the layout of the vertex data
    this->posAttrib = glGetAttribLocation(this->shaderProgram, "a_position");
    this->texAttrib = glGetAttribLocation(this->shaderProgram, "a_texCoord");

    // Get uniform locations
    this->yTexture = glGetUniformLocation(this->shaderProgram, "y_texture");
    this->uvTexture = glGetUniformLocation(this->shaderProgram, "uv_texture");

    // Vertex Buffer object - allocate once with static data
    glGenBuffers(1, &this->vbo);
    glBindBuffer(GL_ARRAY_BUFFER, this->vbo);
    float vertices[] = {
        // Position Texcoords
        -1.0, +1.0, 0.0, 0.0, // Top-left
        -1.0, -1.0, 0.0, 1.0, // Bottom-left
        +1.0, -1.0, 1.0, 1.0, // Bottom-right
        +1.0, +1.0, 1.0, 0.0, // Top-right
    };
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // Element array object - allocate once with static data
    glGenBuffers(1, &this->ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->ebo);
    uint32_t indices[] = {
        0, 1, 2,
        2, 3, 0
    };
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // **CRITICAL OPTIMIZATION: Set up vertex attributes once during initialization**
    glVertexAttribPointer(this->posAttrib, 2, GL_FLOAT, GL_FALSE, 4 * 4, (void*)(0 * 4));
    glEnableVertexAttribArray(this->posAttrib);

    glVertexAttribPointer(this->texAttrib, 2, GL_FLOAT, GL_FALSE, 4 * 4, (void*)(2 * 4));
    glEnableVertexAttribArray(this->texAttrib);

    // Generate textures
    glGenTextures(1, &this->yTextureName);
    glGenTextures(1, &this->uvTextureName);

    // Initialize texture parameters once
    glBindTexture(GL_TEXTURE_2D, this->yTextureName);
    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, this->uvTextureName);
    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);

    // **OPTIMIZATION: Set uniform values once during initialization**
    glUseProgram(this->shaderProgram);
    glUniform1i(this->yTexture, 1);
    glUniform1i(this->uvTexture, 2);

    // Track current texture sizes
    this->currentYWidth = 0;
    this->currentYHeight = 0;
    this->currentUVWidth = 0;
    this->currentUVHeight = 0;

    // Black background
    glClearColor(0.0, 0.0, 0.0, 0.0);
}

YUYV::~YUYV()
{
    if (this->yTextureName)
        glDeleteTextures(1, &this->yTextureName);
    if (this->uvTextureName)
        glDeleteTextures(1, &this->uvTextureName);
    if (this->vbo)
        glDeleteBuffers(1, &this->vbo);
    if (this->ebo)
        glDeleteBuffers(1, &this->ebo);
    if (this->shaderProgram)
        glDeleteProgram(this->shaderProgram);
}

void YUYV::Draw(int width, int height, char* data)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(this->shaderProgram);

    // **OPTIMIZED: Just bind buffers - vertex attributes already set up in constructor**
    glBindBuffer(GL_ARRAY_BUFFER, this->vbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->ebo);

    // Y texture
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, this->yTextureName);
    
    // Only reallocate if size changed
    if (this->currentYWidth != width || this->currentYHeight != height) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, width, height, 0, GL_RG, GL_UNSIGNED_BYTE, data);
        this->currentYWidth = width;
        this->currentYHeight = height;
    } else {
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RG, GL_UNSIGNED_BYTE, data);
    }

    // UV texture
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, this->uvTextureName);
    
    int uvWidth = width / 2;
    int uvHeight = height;
    
    // Only reallocate if size changed
    if (this->currentUVWidth != uvWidth || this->currentUVHeight != uvHeight) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, uvWidth, uvHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        this->currentUVWidth = uvWidth;
        this->currentUVHeight = uvHeight;
    } else {
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, uvWidth, uvHeight, GL_RGBA, GL_UNSIGNED_BYTE, data);
    }

    // Draw
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);
}
