#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <EGL/egl.h>
#include <GLES2/gl2.h>

#include "../include/chroma.h"
#include "../include/vendor/stb_image.h"

// Convert filter quality enum to OpenGL parameters
static void get_gl_filter_params(chroma_filter_quality_t quality,
                                 GLint *min_filter, GLint *mag_filter,
                                 bool *needs_mipmaps) {
  switch (quality) {
  case CHROMA_FILTER_NEAREST:
    *min_filter = GL_NEAREST;
    *mag_filter = GL_NEAREST;
    *needs_mipmaps = false;
    break;
  case CHROMA_FILTER_LINEAR:
    *min_filter = GL_LINEAR;
    *mag_filter = GL_LINEAR;
    *needs_mipmaps = false;
    break;
  case CHROMA_FILTER_BILINEAR:
    *min_filter = GL_LINEAR;
    *mag_filter = GL_LINEAR;
    *needs_mipmaps = false;
    break;
  case CHROMA_FILTER_TRILINEAR:
    *min_filter = GL_LINEAR_MIPMAP_LINEAR;
    *mag_filter = GL_LINEAR;
    *needs_mipmaps = true;
    break;
  default:
    *min_filter = GL_LINEAR;
    *mag_filter = GL_LINEAR;
    *needs_mipmaps = false;
    break;
  }
}

// Calculate texture coordinates based on scaling mode and anchor position
static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
                                     float anchor_x, float anchor_y,
                                     int image_width, int image_height,
                                     int output_width, int output_height,
                                     float tex_coords[8]) {
  // Default texture coordinates (full texture)
  float u1 = 0.0f, v1 = 0.0f; // top-left
  float u2 = 1.0f, v2 = 1.0f; // bottom-right

  switch (scale_mode) {
  case CHROMA_SCALE_STRETCH:
    // Use full texture, stretch to fit
    u1 = 0.0f;
    v1 = 0.0f;
    u2 = 1.0f;
    v2 = 1.0f;
    break;

  case CHROMA_SCALE_CENTER:
    // Center image at original size
    // Calculate how much of the texture to show
    {
      float image_aspect = (float)image_width / (float)image_height;
      float output_aspect = (float)output_width / (float)output_height;

      if (image_aspect > output_aspect) {
        // Image is wider - fit width, show center portion vertically
        float visible_height = (float)image_width / output_aspect;
        float v_offset = ((float)image_height - visible_height) /
                         (2.0f * (float)image_height);
        u1 = 0.0f;
        v1 = v_offset;
        u2 = 1.0f;
        v2 = 1.0f - v_offset;
      } else {
        // Image is taller - fit height, show center portion horizontally
        float visible_width = (float)image_height * output_aspect;
        float u_offset =
            ((float)image_width - visible_width) / (2.0f * (float)image_width);
        u1 = u_offset;
        v1 = 0.0f;
        u2 = 1.0f - u_offset;
        v2 = 1.0f;
      }
    }
    break;

  case CHROMA_SCALE_FIT:
    // Fit image within output, maintaining aspect ratio
    {
      float image_aspect = (float)image_width / (float)image_height;
      float output_aspect = (float)output_width / (float)output_height;

      if (image_aspect > output_aspect) {
        // Image is wider - fit width, add borders top/bottom
        float scaled_height = (float)output_width / image_aspect;
        float v_border = ((float)output_height - scaled_height) /
                         (2.0f * (float)output_height);
        u1 = 0.0f;
        v1 = v_border;
        u2 = 1.0f;
        v2 = 1.0f - v_border;
      } else {
        // Image is taller - fit height, add borders left/right
        float scaled_width = (float)output_height * image_aspect;
        float u_border =
            ((float)output_width - scaled_width) / (2.0f * (float)output_width);
        u1 = u_border;
        v1 = 0.0f;
        u2 = 1.0f - u_border;
        v2 = 1.0f;
      }
    }
    break;

  case CHROMA_SCALE_FILL:
  default:
    // Fill entire output, crop if necessary
    {
      float image_aspect = (float)image_width / (float)image_height;
      float output_aspect = (float)output_width / (float)output_height;

      if (image_aspect > output_aspect) {
        // Image is wider - crop left/right
        float crop_width = (float)image_height * output_aspect;
        float u_crop =
            ((float)image_width - crop_width) / (2.0f * (float)image_width);
        u1 = u_crop;
        v1 = 0.0f;
        u2 = 1.0f - u_crop;
        v2 = 1.0f;
      } else {
        // Image is taller - crop top/bottom
        float crop_height = (float)image_width / output_aspect;
        float v_crop =
            ((float)image_height - crop_height) / (2.0f * (float)image_height);
        u1 = 0.0f;
        v1 = v_crop;
        u2 = 1.0f;
        v2 = 1.0f - v_crop;
      }
    }
    break;
  }

  // Apply anchor-based offset using anchor_x and anchor_y (0-100, 50=center)
  // anchor_x: 0=left edge, 50=center, 100=right edge
  // anchor_y: 0=top edge, 50=center, 100=bottom edge
  // u_shift: negative moves view left (shows more right side of image)
  // v_shift: negative moves view up (shows more top of image)
  float u_shift = (anchor_x - 50.0f) / 50.0f; // -1 to 1
  float v_shift = (50.0f - anchor_y) / 50.0f; // -1 to 1 (inverted for top-down)

  // Calculate the shift amount based on crop/border space available
  float u_crop = 1.0f - (u2 - u1);
  float v_crop = 1.0f - (v2 - v1);
  u1 += u_shift * u_crop;
  u2 += u_shift * u_crop;
  v1 += v_shift * v_crop;
  v2 += v_shift * v_crop;

  // Clamp to valid range [0, 1]
  if (u1 < 0.0f)
    u1 = 0.0f;
  if (u2 > 1.0f)
    u2 = 1.0f;
  if (v1 < 0.0f)
    v1 = 0.0f;
  if (v2 > 1.0f)
    v2 = 1.0f;

  // Set texture coordinates for quad (bottom-left, bottom-right, top-right,
  // top-left)
  tex_coords[0] = u1;
  tex_coords[1] = v2; // bottom-left
  tex_coords[2] = u2;
  tex_coords[3] = v2; // bottom-right
  tex_coords[4] = u2;
  tex_coords[5] = v1; // top-right
  tex_coords[6] = u1;
  tex_coords[7] = v1; // top-left
}

// Vertex shader for simple texture rendering
static const char *vertex_shader_source =
    "attribute vec2 position;\n"
    "attribute vec2 texcoord;\n"
    "varying vec2 v_texcoord;\n"
    "void main() {\n"
    "    gl_Position = vec4(position, 0.0, 1.0);\n"
    "    v_texcoord = texcoord;\n"
    "}\n";

// Fragment shader for simple texture rendering
static const char *fragment_shader_source =
    "precision mediump float;\n"
    "varying vec2 v_texcoord;\n"
    "uniform sampler2D texture;\n"
    "void main() {\n"
    "    gl_FragColor = texture2D(texture, v_texcoord);\n"
    "}\n";

// Vertices for a fullscreen quad
static const float vertices[] = {
    // Position  Texcoord
    -1.0f, -1.0f, 0.0f, 1.0f, // bottom left
    1.0f,  -1.0f, 1.0f, 1.0f, // bottom right
    1.0f,  1.0f,  1.0f, 0.0f, // top right
    -1.0f, 1.0f,  0.0f, 0.0f, // top left
};

static const unsigned short indices[] = {
    0, 1, 2, // first triangle
    2, 3, 0  // second triangle
};

static GLuint compile_shader(GLenum type, const char *source) {
  GLuint shader = glCreateShader(type);
  if (!shader) {
    chroma_log("ERROR", "Failed to create shader");
    return 0;
  }

  glShaderSource(shader, 1, &source, NULL);
  glCompileShader(shader);

  GLint status;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
  if (status != GL_TRUE) {
    char log[512];
    glGetShaderInfoLog(shader, sizeof(log), NULL, log);
    chroma_log("ERROR", "Shader compilation failed: %s", log);
    glDeleteShader(shader);
    return 0;
  }

  return shader;
}

static GLuint create_shader_program(void) {
  GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
  if (!vertex_shader) {
    return 0;
  }

  GLuint fragment_shader =
      compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source);
  if (!fragment_shader) {
    glDeleteShader(vertex_shader);
    return 0;
  }

  GLuint program = glCreateProgram();
  if (!program) {
    chroma_log("ERROR", "Failed to create shader program");
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);
    return 0;
  }

  glAttachShader(program, vertex_shader);
  glAttachShader(program, fragment_shader);
  glLinkProgram(program);

  GLint status;
  glGetProgramiv(program, GL_LINK_STATUS, &status);
  if (status != GL_TRUE) {
    char log[512];
    glGetProgramInfoLog(program, sizeof(log), NULL, log);
    chroma_log("ERROR", "Shader program linking failed: %s", log);
    glDeleteProgram(program);
    program = 0;
  }

  glDeleteShader(vertex_shader);
  glDeleteShader(fragment_shader);

  return program;
}

// Initialize OpenGL resources for output
static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) {
  if (!output || output->gl_resources_initialized) {
    return CHROMA_OK;
  }

  // Use shared shader program from state if available
  if (state && state->shader_program != 0) {
    output->shader_program = state->shader_program;
    chroma_log("DEBUG", "Using shared shader program for output %u",
               output->id);
  } else {
    // Create shader program
    output->shader_program = create_shader_program();
    if (!output->shader_program) {
      chroma_log("ERROR", "Failed to create shader program for output %u",
                 output->id);
      return CHROMA_ERROR_EGL;
    }
    // Store in state for sharing
    if (state) {
      state->shader_program = output->shader_program;
      chroma_log("DEBUG", "Created shared shader program");
    }
  }

  // Create and setup VBO/EBO
  glGenBuffers(1, &output->vbo);
  glGenBuffers(1, &output->ebo);

  glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
               GL_STATIC_DRAW);

  // Unbind buffers
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

  output->texture_id = 0;   // will be created when image is assigned
  output->vbo_dirty = true; // VBO needs initial update
  output->gl_resources_initialized = true;

  chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
  return CHROMA_OK;
}

// Create or update texture from image data
static int update_texture_from_image(chroma_output_t *output,
                                     chroma_image_t *image,
                                     chroma_filter_quality_t filter_quality) {
  if (!output || !image || !image->loaded) {
    return CHROMA_ERROR_INIT;
  }

  // If image data was already freed after previous GPU upload, we can't upload
  // again
  if (!image->data) {
    chroma_log("ERROR",
               "Cannot create texture: image data already freed for %s",
               image->path);
    return CHROMA_ERROR_IMAGE;
  }

  // Delete existing texture if it exists
  if (output->texture_id != 0) {
    // Estimate texture size for logging
    // FIXME: Unfortunately this only works if we have previous image info.
    // Could this b made more accurate?
    if (output->image && output->image->loaded) {
      size_t texture_size = (size_t)output->image->width *
                            (size_t)output->image->height *
                            (size_t)output->image->channels;
      chroma_log_resource_deallocation("gpu_texture", texture_size,
                                       "texture replacement");
    }
    glDeleteTextures(1, &output->texture_id);
    output->texture_id = 0;
  }

  // Create new texture
  glGenTextures(1, &output->texture_id);
  glBindTexture(GL_TEXTURE_2D, output->texture_id);

  // Log GPU texture allocation
  size_t texture_size =
      (size_t)image->width * (size_t)image->height * (size_t)image->channels;
  chroma_log_resource_allocation("gpu_texture", texture_size, image->path);

  // Set texture parameters
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

  // Use configured filter quality
  GLint min_filter, mag_filter;
  bool needs_mipmaps;
  get_gl_filter_params(filter_quality, &min_filter, &mag_filter,
                       &needs_mipmaps);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);

  // Upload texture data with appropriate format based on channels
  GLint internal_format;
  GLenum format;
  if (image->channels == 4) {
    internal_format = GL_RGBA;
    format = GL_RGBA;
  } else if (image->channels == 3) {
    internal_format = GL_RGB;
    format = GL_RGB;
  } else if (image->channels == 2) {
    internal_format = GL_LUMINANCE_ALPHA;
    format = GL_LUMINANCE_ALPHA;
  } else {
    internal_format = GL_LUMINANCE;
    format = GL_LUMINANCE;
  }
  glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height,
               0, format, GL_UNSIGNED_BYTE, image->data);

  // Generate mipmaps for trilinear filtering if they're needed
  if (needs_mipmaps) {
    glGenerateMipmap(GL_TEXTURE_2D);
    chroma_log("DEBUG", "Generated mipmaps for trilinear filtering");
  }

  glBindTexture(GL_TEXTURE_2D, 0);

  // Mark this output as having uploaded its texture
  output->texture_uploaded = true;

  // Decrement reference count and free system RAM when all outputs
  // have uploaded their textures
  image->ref_count--;
  if (image->ref_count <= 0 && image->data) {
    size_t freed_bytes =
        (size_t)image->width * (size_t)image->height * (size_t)image->channels;
    stbi_image_free(image->data);
    image->data = NULL;
    chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s",
               (double)freed_bytes / (1024.0 * 1024.0), image->path);
    chroma_log_resource_deallocation("image_data", freed_bytes,
                                     "post-gpu-upload");
    chroma_log_memory_stats("post-gpu-upload");
  } else {
    chroma_log("DEBUG",
               "Keeping image data for %s (ref_count: %d, waiting for %d more "
               "outputs)",
               image->path, image->ref_count, image->ref_count);
  }

  chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
             image->width, image->height);
  return CHROMA_OK;
}

// Cleanup OpenGL resources for output
static void cleanup_gl_resources(chroma_state_t *state,
                                 chroma_output_t *output) {
  if (!output || !output->gl_resources_initialized) {
    return;
  }

  if (output->texture_id != 0) {
    chroma_log_resource_deallocation("gpu_texture", 0, "cleanup");
    glDeleteTextures(1, &output->texture_id);
    output->texture_id = 0;
  }

  // Don't delete shared shader program here, it's managed by state
  if (output->shader_program != 0) {
    // Only delete if this output owns the program (not shared)
    if (state && state->shader_program != output->shader_program) {
      glDeleteProgram(output->shader_program);
    }
    output->shader_program = 0;
  }

  if (output->vbo != 0) {
    glDeleteBuffers(1, &output->vbo);
    output->vbo = 0;
  }

  if (output->ebo != 0) {
    glDeleteBuffers(1, &output->ebo);
    output->ebo = 0;
  }

  output->gl_resources_initialized = false;
  chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id);
}

// EGL configuration selection
static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
  EGLint attributes[] = {EGL_SURFACE_TYPE,
                         EGL_WINDOW_BIT,
                         EGL_RED_SIZE,
                         8,
                         EGL_GREEN_SIZE,
                         8,
                         EGL_BLUE_SIZE,
                         8,
                         EGL_ALPHA_SIZE,
                         0,
                         EGL_RENDERABLE_TYPE,
                         EGL_OPENGL_ES2_BIT,
                         EGL_NONE};

  EGLint num_configs;
  if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) {
    chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError());
    return -1;
  }

  if (num_configs == 0) {
    chroma_log("ERROR", "No suitable EGL configs found");
    return -1;
  }

  return 0;
}

// EGL initialization
int chroma_egl_init(chroma_state_t *state) {
  if (!state || !state->display) {
    return CHROMA_ERROR_INIT;
  }

  // Get EGL display
  state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display);
  if (state->egl_display == EGL_NO_DISPLAY) {
    chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError());
    return CHROMA_ERROR_EGL;
  }

  // Initialize EGL
  EGLint major, minor;
  if (!eglInitialize(state->egl_display, &major, &minor)) {
    chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError());
    return CHROMA_ERROR_EGL;
  }

  chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
  chroma_log_memory_stats("post-egl-init");

  // Bind OpenGL ES API for better efficiency on embedded/GPU drivers
  if (!eglBindAPI(EGL_OPENGL_ES_API)) {
    chroma_log("ERROR", "Failed to bind OpenGL ES API: 0x%04x", eglGetError());
    chroma_egl_cleanup(state);
    return CHROMA_ERROR_EGL;
  }

  // Choose EGL config
  if (choose_egl_config(state->egl_display, &state->egl_config) != 0) {
    chroma_egl_cleanup(state);
    return CHROMA_ERROR_EGL;
  }

  // Create EGL context for GLES 2.0
  EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};

  state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
                                        EGL_NO_CONTEXT, context_attributes);
  if (state->egl_context == EGL_NO_CONTEXT) {
    chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError());
    chroma_egl_cleanup(state);
    return CHROMA_ERROR_EGL;
  }

  chroma_log("INFO", "EGL context created successfully");
  return CHROMA_OK;
}

// EGL cleanup
void chroma_egl_cleanup(chroma_state_t *state) {
  if (!state) {
    return;
  }

  // Clean up shared shader program while context is still current
  if (state->shader_program != 0) {
    glDeleteProgram(state->shader_program);
    state->shader_program = 0;
  }

  if (state->egl_display != EGL_NO_DISPLAY) {
    eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
                   EGL_NO_CONTEXT);

    if (state->egl_context != EGL_NO_CONTEXT) {
      eglDestroyContext(state->egl_display, state->egl_context);
      state->egl_context = EGL_NO_CONTEXT;
    }

    eglTerminate(state->egl_display);
    state->egl_display = EGL_NO_DISPLAY;
  }

  chroma_log("INFO", "EGL cleaned up");
}

// Create surface for output
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
  if (!state || !output || !state->compositor || !state->layer_shell) {
    return CHROMA_ERROR_INIT;
  }

  // Create Wayland surface
  output->surface = wl_compositor_create_surface(state->compositor);
  if (!output->surface) {
    chroma_log("ERROR", "Failed to create Wayland surface for output %u",
               output->id);
    return CHROMA_ERROR_WAYLAND;
  }

  // Create layer surface for wallpaper
  output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
      state->layer_shell, output->surface, output->wl_output,
      ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper");

  if (!output->layer_surface) {
    chroma_log("ERROR", "Failed to create layer surface for output %u",
               output->id);
    chroma_surface_destroy(output);
    return CHROMA_ERROR_WAYLAND;
  }

  // Configure layer surface
  zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width,
                                 (uint32_t)output->height);
  zwlr_layer_surface_v1_set_anchor(output->layer_surface,
                                   ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
  zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
  zwlr_layer_surface_v1_set_keyboard_interactivity(
      output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);

  // Add layer surface listener
  zwlr_layer_surface_v1_add_listener(
      output->layer_surface, &chroma_layer_surface_listener_impl, output);

  // Commit surface to trigger configure event
  wl_surface_commit(output->surface);

  // Wait for configure event
  wl_display_roundtrip(state->display);

  // Create EGL window
  output->egl_window =
      wl_egl_window_create(output->surface, output->width, output->height);
  if (!output->egl_window) {
    chroma_log("ERROR", "Failed to create EGL window for output %u",
               output->id);
    chroma_surface_destroy(output);
    return CHROMA_ERROR_EGL;
  }

  // Create EGL surface
  output->egl_surface =
      eglCreateWindowSurface(state->egl_display, state->egl_config,
                             (EGLNativeWindowType)output->egl_window, NULL);
  if (output->egl_surface == EGL_NO_SURFACE) {
    chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x",
               output->id, eglGetError());
    chroma_surface_destroy(output);
    return CHROMA_ERROR_EGL;
  }

  chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
             output->width, output->height);

  // Log surface creation resource allocation
  size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
  chroma_log_resource_allocation("egl_surface", surface_size, "output surface");

  return CHROMA_OK;
}

// Destroy surface
void chroma_surface_destroy(chroma_output_t *output) {
  if (!output) {
    return;
  }

  // Clean up OpenGL resources first. We use output's back-reference to state
  cleanup_gl_resources(output->state, output);

  if (output->egl_surface != EGL_NO_SURFACE) {
    eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
    output->egl_surface = EGL_NO_SURFACE;
  }

  if (output->egl_window) {
    wl_egl_window_destroy(output->egl_window);
    output->egl_window = NULL;
  }

  if (output->layer_surface) {
    zwlr_layer_surface_v1_destroy(output->layer_surface);
    output->layer_surface = NULL;
  }

  if (output->surface) {
    wl_surface_destroy(output->surface);
    output->surface = NULL;
  }

  output->configured = false;

  // Log surface destruction
  size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
  chroma_log_resource_deallocation("egl_surface", surface_size,
                                   "output surface cleanup");

  chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
}

// Render wallpaper to output using cached resources
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
  if (!state || !output || !output->image || !output->image->loaded) {
    return CHROMA_ERROR_INIT;
  }

  // Make context current
  if (!eglMakeCurrent(state->egl_display, output->egl_surface,
                      output->egl_surface, state->egl_context)) {
    chroma_log("ERROR", "Failed to make EGL context current: 0x%04x",
               eglGetError());
    return CHROMA_ERROR_EGL;
  }

  if (init_gl_resources(state, output) != CHROMA_OK) {
    return CHROMA_ERROR_EGL;
  }

  if (output->texture_id == 0) {
    if (update_texture_from_image(output, output->image,
                                  output->filter_quality) != CHROMA_OK) {
      chroma_log("ERROR", "Failed to update texture for output %u", output->id);
      return CHROMA_ERROR_EGL;
    }
  }

  // Set viewport
  glViewport(0, 0, output->width, output->height);

  // Clear screen
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  // Use cached shader program
  glUseProgram(output->shader_program);

  // Bind cached texture
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
  glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);

  // Update VBO only if needed. E.g, image changed, scale mode changed, or first
  // render
  if (output->vbo_dirty) {
    // Calculate texture coordinates based on scaling mode, anchor, and anchor
    // coords
    float tex_coords[8];
    calculate_texture_coords(output->scale_mode, output->anchor_x,
                             output->anchor_y, output->image->width,
                             output->image->height, output->width,
                             output->height, tex_coords);

    // Create dynamic vertex data with calculated texture coordinates
    float dynamic_vertices[] = {
        // Position  Texcoord
        -1.0f, -1.0f, tex_coords[0], tex_coords[1], // bottom-left
        1.0f,  -1.0f, tex_coords[2], tex_coords[3], // bottom-right
        1.0f,  1.0f,  tex_coords[4], tex_coords[5], // top-right
        -1.0f, 1.0f,  tex_coords[6], tex_coords[7]  // top-left
    };

    // Update VBO with dynamic texture coordinates
    glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices),
                    dynamic_vertices);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);

    output->vbo_dirty = false; // mark VBO as up to date
  } else {
    // Just bind the existing buffers
    glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
  }

  // Set vertex attributes
  GLint position_attr = glGetAttribLocation(output->shader_program, "position");
  GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");

  glEnableVertexAttribArray((GLuint)position_attr);
  glVertexAttribPointer((GLuint)position_attr, 2, GL_FLOAT, GL_FALSE,
                        4 * sizeof(float), (void *)0);

  glEnableVertexAttribArray((GLuint)texcoord_attr);
  glVertexAttribPointer((GLuint)texcoord_attr, 2, GL_FLOAT, GL_FALSE,
                        4 * sizeof(float), (void *)(2 * sizeof(float)));

  // Draw
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

  // Unbind resources
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glBindTexture(GL_TEXTURE_2D, 0);
  glUseProgram(0);

  // Swap buffers; this implicitly commits the surface for EGL on Wayland
  if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
    chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
               output->id, eglGetError());
    return CHROMA_ERROR_EGL;
  }

  chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
             output->id);
  return CHROMA_OK;
}

// Invalidate texture cache for output
void chroma_output_invalidate_texture(chroma_output_t *output) {
  if (!output || !output->gl_resources_initialized) {
    return;
  }

  if (output->texture_id != 0) {
    glDeleteTextures(1, &output->texture_id);
    output->texture_id = 0;
    output->texture_uploaded = false; // reset upload flag
    chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
  }

  // Mark VBO as dirty since texture coordinates may need recalculation
  output->vbo_dirty = true;
}
