#include "ui.h"

#include <SDL2/SDL_image.h>
#include <SDL2/SDL_filesystem.h>
#include <stdio.h>
#include <string.h>

static const char *FONT_CANDIDATES[] = {
    "BigBlueTermPlus Nerd Font Mono.ttf",
    "BigBlueTermPlusNerdFontMono-Regular.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
    "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
    "/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf"
};

static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
static void stroke_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height);

static void text_texture_destroy(UiTextTexture *text_texture) {
    if (!text_texture) {
        return;
    }

    if (text_texture->texture) {
        SDL_DestroyTexture(text_texture->texture);
    }
    memset(text_texture, 0, sizeof(*text_texture));
}

static int text_texture_init(UiTextTexture *text_texture,
                             SDL_Renderer *renderer,
                             TTF_Font *font,
                             const char *text,
                             SDL_Color color) {
    if (!text_texture) {
        return -1;
    }

    memset(text_texture, 0, sizeof(*text_texture));
    text_texture->texture = text_to_texture(renderer, font, text, color, &text_texture->width, &text_texture->height);
    return text_texture->texture ? 0 : -1;
}

static void set_draw_color(SDL_Renderer *renderer, SDL_Color color) {
    SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
}

static SDL_Color color_with_alpha(SDL_Color color, Uint8 alpha) {
    color.a = alpha;
    return color;
}

static SDL_Color blend_color(SDL_Color from, SDL_Color to, int weight_to_255) {
    SDL_Color blended;
    int weight_from = 255 - weight_to_255;

    blended.r = (Uint8) ((from.r * weight_from + to.r * weight_to_255) / 255);
    blended.g = (Uint8) ((from.g * weight_from + to.g * weight_to_255) / 255);
    blended.b = (Uint8) ((from.b * weight_from + to.b * weight_to_255) / 255);
    blended.a = 255;
    return blended;
}

static int color_luma(SDL_Color color) {
    return (299 * color.r + 587 * color.g + 114 * color.b) / 1000;
}

static SDL_Color readable_text_color(SDL_Color background) {
    return color_luma(background) < 120 ? COLOR_TEXT_LIGHT : COLOR_TEXT_DARK;
}

static SDL_Color ensure_contrast(SDL_Color preferred, SDL_Color background) {
    int diff = color_luma(preferred) - color_luma(background);
    if (diff < 0) {
        diff = -diff;
    }
    if (diff < 75) {
        return readable_text_color(background);
    }
    return preferred;
}

static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height) {
    SDL_Surface *surface;
    SDL_Texture *texture;

    if (!font || !text || text[0] == '\0') {
        return NULL;
    }

    surface = TTF_RenderUTF8_Solid(font, text, color);
    if (!surface) {
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    if (width) {
        *width = surface->w;
    }
    if (height) {
        *height = surface->h;
    }
    SDL_FreeSurface(surface);
    return texture;
}

static SDL_Texture *load_png_texture(SDL_Renderer *renderer, const char *path, int *width, int *height) {
    SDL_Surface *surface;
    SDL_Texture *texture;

    surface = IMG_Load(path);
    if (!surface) {
        fprintf(stderr, "Failed to load image %s: %s\n", path, IMG_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    if (!texture) {
        SDL_FreeSurface(surface);
        fprintf(stderr, "Failed to create texture from %s: %s\n", path, SDL_GetError());
        return NULL;
    }

    if (width) {
        *width = surface->w;
    }
    if (height) {
        *height = surface->h;
    }

    SDL_FreeSurface(surface);
    return texture;
}

static void draw_cached_text(SDL_Renderer *renderer, const UiTextTexture *text_texture, int x, int y) {
    SDL_Rect dst;

    if (!text_texture || !text_texture->texture) {
        return;
    }

    dst.x = x;
    dst.y = y;
    dst.w = text_texture->width;
    dst.h = text_texture->height;
    SDL_RenderCopy(renderer, text_texture->texture, NULL, &dst);
}

static void fill_three_stop_gradient(SDL_Renderer *renderer,
                                     const SDL_Rect *rect,
                                     SDL_Color top,
                                     SDL_Color middle,
                                     SDL_Color bottom) {
    int split;

    if (!rect || rect->h <= 0) {
        return;
    }

    top = blend_color(top, middle, 140);
    bottom = blend_color(bottom, middle, 140);

    split = rect->h / 2;
    for (int i = 0; i < rect->h; ++i) {
        SDL_Color from = i < split ? top : middle;
        SDL_Color to = i < split ? middle : bottom;
        int segment_height = i < split ? SDL_max(split, 1) : SDL_max(rect->h - split, 1);
        int segment_pos = i < split ? i : i - split;
        Uint8 r = (Uint8) (from.r + (to.r - from.r) * segment_pos / SDL_max(segment_height - 1, 1));
        Uint8 g = (Uint8) (from.g + (to.g - from.g) * segment_pos / SDL_max(segment_height - 1, 1));
        Uint8 b = (Uint8) (from.b + (to.b - from.b) * segment_pos / SDL_max(segment_height - 1, 1));
        SDL_SetRenderDrawColor(renderer, r, g, b, 255);
        SDL_RenderDrawLine(renderer, rect->x, rect->y + i, rect->x + rect->w, rect->y + i);
    }
}

static void fill_rect_alpha(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
    SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
    SDL_RenderFillRect(renderer, rect);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
}

static void draw_panel_shadow(SDL_Renderer *renderer, const SDL_Rect *rect) {
    SDL_Rect shadow = {rect->x + 6, rect->y + 6, rect->w, rect->h};
    fill_rect_alpha(renderer, &shadow, color_with_alpha(COLOR_PANEL_SHADOW, 48));
}

static void draw_gloss_line(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color gloss_color) {
    SDL_Rect gloss = {rect->x + 4, rect->y + 1, SDL_max(rect->w - 8, 0), 2};
    if (gloss.w > 0) {
        fill_rect_alpha(renderer, &gloss, gloss_color);
    }
}

static void draw_panel_bevel(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color gloss_color) {
    SDL_Rect top_highlight = {rect->x + 4, rect->y + 1, SDL_max(rect->w - 8, 0), 2};
    SDL_Rect left_highlight = {rect->x + 1, rect->y + 4, 2, SDL_max(rect->h - 8, 0)};
    SDL_Rect inner_shadow_top = {rect->x + 4, rect->y + 3, SDL_max(rect->w - 8, 0), 1};
    SDL_Rect right_shadow = {rect->x + rect->w - 3, rect->y + 3, 2, SDL_max(rect->h - 6, 0)};
    SDL_Rect bottom_shadow = {rect->x + 3, rect->y + rect->h - 3, SDL_max(rect->w - 6, 0), 2};

    if (!rect || rect->w <= 6 || rect->h <= 6) {
        return;
    }

    fill_rect_alpha(renderer, &top_highlight, color_with_alpha(gloss_color, 96));
    fill_rect_alpha(renderer, &left_highlight, color_with_alpha(gloss_color, 64));
    fill_rect_alpha(renderer, &inner_shadow_top, (SDL_Color){0, 0, 0, 34});
    fill_rect_alpha(renderer, &right_shadow, (SDL_Color){0, 0, 0, 56});
    fill_rect_alpha(renderer, &bottom_shadow, (SDL_Color){0, 0, 0, 64});
}

static void stroke_rect_alpha(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
    SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
    SDL_RenderDrawRect(renderer, rect);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
}

static void draw_selection_glow(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color edge) {
    SDL_Rect outer = *rect;
    SDL_Rect inner = {rect->x + 1, rect->y + 1, rect->w - 2, rect->h - 2};
    stroke_rect_alpha(renderer, &outer, color_with_alpha(edge, 62));
    if (inner.w > 0 && inner.h > 0) {
        stroke_rect_alpha(renderer, &inner, color_with_alpha(edge, 34));
    }
}

static void draw_rounded_top_panel(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color fill_color, SDL_Color border_color, int radius) {
    SDL_Rect body;
    SDL_Rect top;

    if (!rect || rect->w < radius || rect->h < radius) {
        return;
    }

    body = (SDL_Rect){rect->x, rect->y + radius, rect->w, rect->h - radius};
    top = (SDL_Rect){rect->x + radius, rect->y, rect->w - radius * 2, radius};
    fill_rect(renderer, &body, fill_color);
    fill_rect(renderer, &top, fill_color);
    fill_rect(renderer, &(SDL_Rect){rect->x + radius / 2, rect->y + 2, rect->w - radius, radius - 2}, fill_color);
    fill_rect(renderer, &(SDL_Rect){rect->x + 2, rect->y + radius / 2, rect->w - 4, radius / 2}, fill_color);

    set_draw_color(renderer, border_color);
    SDL_RenderDrawLine(renderer, rect->x + radius, rect->y, rect->x + rect->w - radius - 1, rect->y);
    SDL_RenderDrawLine(renderer, rect->x + radius / 2, rect->y + 2, rect->x + radius / 2 + 1, rect->y + 1);
    SDL_RenderDrawLine(renderer, rect->x + rect->w - radius / 2 - 2, rect->y + 1, rect->x + rect->w - radius / 2 - 1, rect->y + 2);
    SDL_RenderDrawLine(renderer, rect->x + 1, rect->y + radius - 2, rect->x + 1, rect->y + rect->h - 2);
    SDL_RenderDrawLine(renderer, rect->x + rect->w - 2, rect->y + radius - 2, rect->x + rect->w - 2, rect->y + rect->h - 2);
    SDL_RenderDrawLine(renderer, rect->x + 1, rect->y + rect->h - 1, rect->x + rect->w - 2, rect->y + rect->h - 1);
}

static void draw_beveled_bar(SDL_Renderer *renderer,
                             const SDL_Rect *rect,
                             SDL_Color top,
                             SDL_Color mid,
                             SDL_Color bottom,
                             SDL_Color top_edge,
                             SDL_Color bottom_edge) {
    if (!rect) {
        return;
    }

    fill_three_stop_gradient(renderer, rect, top, mid, bottom);
    top_edge = blend_color(top_edge, mid, 170);
    bottom_edge = blend_color(bottom_edge, mid, 170);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
    SDL_SetRenderDrawColor(renderer, top_edge.r, top_edge.g, top_edge.b, 110);
    SDL_RenderDrawLine(renderer, rect->x, rect->y, rect->x + rect->w - 1, rect->y);
    SDL_SetRenderDrawColor(renderer, bottom_edge.r, bottom_edge.g, bottom_edge.b, 110);
    SDL_RenderDrawLine(renderer, rect->x, rect->y + rect->h - 1, rect->x + rect->w - 1, rect->y + rect->h - 1);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
}

static void draw_program_block(SDL_Renderer *renderer,
                               const SDL_Rect *rect,
                               SDL_Color top,
                               SDL_Color mid,
                               SDL_Color bottom,
                               SDL_Color gloss,
                               SDL_Color border,
                               int is_selected,
                               SDL_Color selection_edge,
                               SDL_Color block_border) {
    SDL_Rect inner;
    SDL_Rect outer_border;

    if (!rect || rect->w <= 4 || rect->h <= 4) {
        return;
    }

    draw_beveled_bar(renderer, rect, top, mid, bottom, gloss, border);

    outer_border = (SDL_Rect){rect->x + 1, rect->y + 1, rect->w - 2, rect->h - 2};
    if (outer_border.w > 0 && outer_border.h > 0) {
        stroke_rect(renderer, &outer_border, block_border);
    }

    inner = (SDL_Rect){rect->x + 2, rect->y + 2, rect->w - 4, rect->h - 4};
    if (inner.w > 1 && inner.h > 1) {
        stroke_rect_alpha(renderer, &inner, color_with_alpha(COLOR_TEXT_LIGHT, is_selected ? 80 : 50));
        fill_rect_alpha(renderer,
                        &(SDL_Rect){inner.x + 1, inner.y + 1, SDL_max(inner.w - 2, 0), 2},
                        color_with_alpha(COLOR_TEXT_LIGHT, is_selected ? 45 : 25));
    }

    if (is_selected) {
        draw_selection_glow(renderer, rect, selection_edge);
    }
}

static void draw_pill_button(SDL_Renderer *renderer, const GuideTheme *theme, const SDL_Rect *rect, SDL_Color fill, SDL_Color border) {
    SDL_Rect shadow;

    if (!rect || rect->w < 10 || rect->h < 10) {
        return;
    }

    shadow = (SDL_Rect){rect->x + 2, rect->y + 2, rect->w, rect->h};
    fill_rect_alpha(renderer, &shadow, theme->pill_shadow);
    draw_beveled_bar(renderer, rect, theme->pill_light, fill, fill, theme->gloss, border);
    set_draw_color(renderer, border);
    SDL_RenderDrawLine(renderer, rect->x + 4, rect->y, rect->x + rect->w - 5, rect->y);
    SDL_RenderDrawLine(renderer, rect->x + 4, rect->y + rect->h - 1, rect->x + rect->w - 5, rect->y + rect->h - 1);
    SDL_RenderDrawLine(renderer, rect->x, rect->y + 4, rect->x, rect->y + rect->h - 5);
    SDL_RenderDrawLine(renderer, rect->x + rect->w - 1, rect->y + 4, rect->x + rect->w - 1, rect->y + rect->h - 5);
}

static void draw_text_clipped(SDL_Renderer *renderer,
                              TTF_Font *font,
                              const char *text,
                              const SDL_Rect *clip_rect,
                              int x,
                              int y,
                              SDL_Color color) {
    SDL_Rect previous_clip;
    SDL_bool had_clip = SDL_RenderIsClipEnabled(renderer);
    if (had_clip) {
        SDL_RenderGetClipRect(renderer, &previous_clip);
    }
    SDL_RenderSetClipRect(renderer, clip_rect);
    {
        SDL_Texture *texture;
        int width = 0;
        int height = 0;
        SDL_Rect dst;

        texture = text_to_texture(renderer, font, text, color, &width, &height);
        if (texture) {
            dst.x = x;
            dst.y = y;
            dst.w = width;
            dst.h = height;
            SDL_RenderCopy(renderer, texture, NULL, &dst);
            SDL_DestroyTexture(texture);
        }
    }
    SDL_RenderSetClipRect(renderer, had_clip ? &previous_clip : NULL);
}

static void draw_text_shadowed(SDL_Renderer *renderer,
                               TTF_Font *font,
                               const char *text,
                               const SDL_Rect *clip_rect,
                               int x,
                               int y,
                               SDL_Color color,
                               SDL_Color shadow) {
    draw_text_clipped(renderer, font, text, clip_rect, x + 1, y + 1, shadow);
    draw_text_clipped(renderer, font, text, clip_rect, x, y, color);
}

static void fit_text_with_ellipsis(TTF_Font *font, const char *text, int max_width, char *out, size_t out_size) {
    int width = 0;
    size_t len;

    if (!out || out_size == 0) {
        return;
    }

    out[0] = '\0';
    if (!font || !text || max_width <= 0) {
        return;
    }

    if (TTF_SizeUTF8(font, text, &width, NULL) == 0 && width <= max_width) {
        snprintf(out, out_size, "%s", text);
        return;
    }

    len = strlen(text);
    while (len > 0) {
        snprintf(out, out_size, "%.*s...", (int) len, text);
        if (TTF_SizeUTF8(font, out, &width, NULL) == 0 && width <= max_width) {
            return;
        }
        len -= 1;
    }

    snprintf(out, out_size, "...");
}

static void format_time_compact(char *buffer, size_t buffer_size, time_t value) {
    struct tm local_time;
    localtime_r(&value, &local_time);
    strftime(buffer, buffer_size, "%I:%M %p", &local_time);
}

static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
    set_draw_color(renderer, color);
    SDL_RenderFillRect(renderer, rect);
}

static void stroke_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
    set_draw_color(renderer, color);
    SDL_RenderDrawRect(renderer, rect);
}

static SDL_Rect fit_rect(int outer_x, int outer_y, int outer_w, int outer_h, int inner_w, int inner_h) {
    SDL_Rect dst = {outer_x, outer_y, outer_w, outer_h};
    double scale;

    if (inner_w <= 0 || inner_h <= 0) {
        return dst;
    }

    scale = SDL_min((double) outer_w / (double) inner_w, (double) outer_h / (double) inner_h);
    dst.w = (int) (inner_w * scale);
    dst.h = (int) (inner_h * scale);
    dst.x = outer_x + (outer_w - dst.w) / 2;
    dst.y = outer_y + (outer_h - dst.h) / 2;
    return dst;
}

static void draw_video(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height, SDL_Rect destination) {
    if (!video_texture) {
        fill_rect(renderer, &destination, COLOR_BLACK);
        return;
    }

    {
        SDL_Rect fitted = fit_rect(destination.x, destination.y, destination.w, destination.h, texture_width, texture_height);
        fill_rect(renderer, &destination, COLOR_BLACK);
        SDL_RenderCopy(renderer, video_texture, NULL, &fitted);
    }
}

static void format_clock_label(char *buffer, size_t buffer_size, time_t now, int minute_offset) {
    struct tm local_time;
    time_t slot = now + minute_offset * 60;
    localtime_r(&slot, &local_time);
    strftime(buffer, buffer_size, "%I:%M %p", &local_time);
}

static int timeline_marker_x(int guide_x_start, double pixels_per_minute, int minute_offset) {
    return guide_x_start + (int) ((minute_offset * pixels_per_minute) + 0.5);
}

static void draw_timeline_header_cached(SDL_Renderer *renderer,
                                        const UiCache *cache,
                                        const GuideTheme *theme,
                                        SDL_Rect rect,
                                        int guide_x_start,
                                        double pixels_per_minute,
                                        int timeline_width,
                                        int visible_minutes) {
    SDL_Color ribbon_text = ensure_contrast(theme->ribbon_text, theme->ribbon_mid);
    SDL_Color ribbon_shadow = color_luma(theme->ribbon_mid) < 120 ? color_with_alpha(COLOR_BLACK, 255) : color_with_alpha(COLOR_TEXT_LIGHT, 255);

    draw_beveled_bar(renderer,
                     &rect,
                     theme->ribbon_top,
                     theme->ribbon_mid,
                     theme->ribbon_bottom,
                     theme->gloss,
                     theme->panel_border);

    (void) timeline_width;
    for (int i = 0; i < 4; ++i) {
        int minute_offset = (visible_minutes / 3) * i;
        int x = timeline_marker_x(guide_x_start, pixels_per_minute, minute_offset);
        int centered_x = x - cache->timeline_labels[i].width / 2;
        int min_center_x = guide_x_start;
        int max_center_x = guide_x_start + timeline_width - cache->timeline_labels[i].width;
        SDL_Rect shadow_dst = {centered_x + 1, rect.y + 11, cache->timeline_labels[i].width, cache->timeline_labels[i].height};
        SDL_Rect text_dst = {centered_x, rect.y + 10, cache->timeline_labels[i].width, cache->timeline_labels[i].height};

        if (centered_x < min_center_x) {
            centered_x = min_center_x;
        }
        if (centered_x > max_center_x) {
            centered_x = max_center_x;
        }
        shadow_dst.x = centered_x + 1;
        text_dst.x = centered_x;

        SDL_SetTextureColorMod(cache->timeline_labels[i].texture, ribbon_shadow.r, ribbon_shadow.g, ribbon_shadow.b);
        SDL_RenderCopy(renderer, cache->timeline_labels[i].texture, NULL, &shadow_dst);
        SDL_SetTextureColorMod(cache->timeline_labels[i].texture, ribbon_text.r, ribbon_text.g, ribbon_text.b);
        SDL_RenderCopy(renderer, cache->timeline_labels[i].texture, NULL, &text_dst);
        set_draw_color(renderer, theme->grid_line);
        SDL_RenderDrawLine(renderer, x, rect.y + rect.h - 2, x, rect.y + rect.h + 5 * 76);
    }
}

static void draw_mini_status_bar(SDL_Renderer *renderer,
                                 TTF_Font *font,
                                 const GuideTheme *theme,
                                 const Channel *selected_channel,
                                 const SDL_Rect *rect,
                                 time_t now_wall) {
    char clock_text[32];
    char channel_text[192];
    char fitted_channel_text[192];
    SDL_Color text_color;
    SDL_Rect time_rect;
    SDL_Rect channel_rect;
    int time_width = 0;
    int gap = 8;

    if (!rect) {
        return;
    }

    text_color = readable_text_color(theme->status_mid);

    fill_three_stop_gradient(renderer,
                             rect,
                             blend_color(theme->status_top, theme->status_mid, 220),
                             theme->status_mid,
                             blend_color(theme->status_bottom, theme->status_mid, 220));
    stroke_rect(renderer, rect, theme->panel_border);
    draw_panel_bevel(renderer, rect, theme->gloss);

    format_time_compact(clock_text, sizeof(clock_text), now_wall);
    if (TTF_SizeUTF8(font, clock_text, &time_width, NULL) != 0) {
        time_width = rect->w / 3;
    }

    time_rect = (SDL_Rect){rect->x + 8, rect->y, SDL_min(time_width + 4, rect->w - 16), rect->h};
    draw_text_clipped(renderer, font, clock_text, &time_rect, time_rect.x, rect->y + 4, text_color);

    if (selected_channel) {
        channel_rect = (SDL_Rect){time_rect.x + time_rect.w + gap,
                                  rect->y,
                                  rect->x + rect->w - (time_rect.x + time_rect.w + gap) - 8,
                                  rect->h};
        if (channel_rect.w > 0) {
            snprintf(channel_text, sizeof(channel_text), "%d %s", selected_channel->number, selected_channel->name);
            fit_text_with_ellipsis(font, channel_text, channel_rect.w, fitted_channel_text, sizeof(fitted_channel_text));
            draw_text_clipped(renderer,
                              font,
                              fitted_channel_text,
                              &channel_rect,
                              channel_rect.x,
                              rect->y + 4,
                              text_color);
        }
    }
}

static void draw_info_panel(SDL_Renderer *renderer,
                             const UiFonts *fonts,
                             const GuideTheme *theme,
                             const Channel *selected_channel,
                             const SDL_Rect *rect,
                             time_t app_start_time,
                             time_t guide_focus_time) {
    SDL_Rect accent;
    SDL_Rect clip_rect;
    char time_range[64];
    char end_text[32];
    char description[384];
    time_t start_time;
    time_t end_time;
    double program_seek;
    const ProgramEntry *program;
    SDL_Color panel_text;

    if (!rect || !selected_channel) {
        return;
    }

    panel_text = ensure_contrast(theme->panel_text, theme->panel_fill);

    fill_rect(renderer, rect, theme->panel_fill);
    stroke_rect(renderer, rect, theme->panel_border);
    draw_panel_bevel(renderer, rect, theme->gloss);
    accent = (SDL_Rect){rect->x + 1, rect->y + 1, rect->w - 2, 36};
    fill_rect(renderer, &accent, blend_color(theme->panel_fill, theme->ribbon_mid, 40));

    program = channel_resolve_program_at_elapsed(selected_channel,
                                                 channel_schedule_elapsed_seconds(app_start_time, guide_focus_time),
                                                 &program_seek,
                                                 NULL);
    if (!program) {
        return;
    }
    start_time = guide_focus_time - (time_t) program_seek;
    end_time = start_time + (time_t) program->duration_seconds;
    format_time_compact(time_range, sizeof(time_range), start_time);
    format_time_compact(end_text, sizeof(end_text), end_time);
    strncat(time_range, " - ", sizeof(time_range) - strlen(time_range) - 1);
    strncat(time_range, end_text, sizeof(time_range) - strlen(time_range) - 1);
    snprintf(description,
             sizeof(description),
             "%s",
             selected_channel->description[0] != '\0' ? selected_channel->description : "Local programming lineup.");

    clip_rect = (SDL_Rect){rect->x + 16, rect->y + 12, rect->w - 32, rect->h - 24};
    draw_text_clipped(renderer, fonts->large, selected_channel->name, &clip_rect, rect->x + 18, rect->y + 44, panel_text);
    draw_text_clipped(renderer, fonts->medium, program->program_title, &clip_rect, rect->x + 18, rect->y + 88, panel_text);
    draw_text_clipped(renderer, fonts->small, time_range, &clip_rect, rect->x + 18, rect->y + 124, panel_text);
    draw_text_clipped(renderer, fonts->small, description, &clip_rect, rect->x + 18, rect->y + 148, panel_text);
}

static void draw_grid_background(SDL_Renderer *renderer,
                                 const GuideTheme *theme,
                                 const SDL_Rect *grid_rect,
                                 int guide_x_start,
                                 int row_height,
                                 double pixels_per_minute) {
    fill_three_stop_gradient(renderer,
                             grid_rect,
                             theme->background_top,
                             theme->background_mid,
                             theme->background_bottom);

    for (int minute = 0; minute <= 90; minute += 30) {
        int x = timeline_marker_x(guide_x_start, pixels_per_minute, minute);
        SDL_Color strong_line = ensure_contrast(theme->grid_line, theme->background_mid);
        set_draw_color(renderer, strong_line);
        SDL_RenderDrawLine(renderer, x, grid_rect->y, x, grid_rect->y + grid_rect->h);
        SDL_RenderDrawLine(renderer, x + 1, grid_rect->y, x + 1, grid_rect->y + grid_rect->h);
    }

    for (int row = 0; row <= GUIDE_VISIBLE_ROWS; ++row) {
        int y = grid_rect->y + row * row_height;
        set_draw_color(renderer, theme->row_line_hi);
        SDL_RenderDrawLine(renderer, grid_rect->x, y, grid_rect->x + grid_rect->w, y);
        if (y + row_height - 1 <= grid_rect->y + grid_rect->h) {
            set_draw_color(renderer, theme->row_line_lo);
            SDL_RenderDrawLine(renderer, grid_rect->x, y + row_height - 1, grid_rect->x + grid_rect->w, y + row_height - 1);
        }
    }
}

static void draw_footer_legend(SDL_Renderer *renderer,
                               const UiFonts *fonts,
                               const GuideTheme *theme,
                               int window_width,
                               int window_height,
                               SDL_Texture *logo_texture) {
    SDL_Rect footer = {0, window_height - 54, window_width, 54};
    SDL_Rect chip = {window_width / 2 - 220, window_height - 38, 34, 20};
    SDL_Color footer_text = readable_text_color(theme->footer_mid);

    fill_three_stop_gradient(renderer,
                             &footer,
                             blend_color(theme->footer_top, theme->footer_mid, 240),
                             theme->footer_mid,
                             blend_color(theme->footer_bottom, theme->footer_mid, 240));
    draw_gloss_line(renderer, &footer, color_with_alpha(theme->gloss, 20));
    stroke_rect(renderer, &footer, theme->panel_border);
    draw_panel_bevel(renderer, &footer, theme->gloss);

    draw_pill_button(renderer, theme, &chip, theme->panel_fill, theme->panel_border);
    draw_text_clipped(renderer, fonts->small, "A", &footer, chip.x + 11, chip.y + 2, footer_text);
    draw_text_clipped(renderer, fonts->small, "ABOUT", &footer, chip.x + 42, chip.y - 1, footer_text);

    chip.x += 144;
    draw_pill_button(renderer, theme, &chip, theme->block_mid, theme->panel_border);
    draw_text_clipped(renderer, fonts->small, "B", &footer, chip.x + 11, chip.y + 2, footer_text);
    draw_text_clipped(renderer, fonts->small, "THEME", &footer, chip.x + 42, chip.y - 1, footer_text);

    chip.x += 160;
    draw_pill_button(renderer, theme, &chip, theme->row_mid, theme->panel_border);
    draw_text_clipped(renderer, fonts->small, "<>", &footer, chip.x + 5, chip.y + 2, footer_text);
    draw_text_clipped(renderer, fonts->small, "TIME", &footer, chip.x + 42, chip.y - 1, footer_text);

    if (logo_texture) {
        int logo_size = 32;
        SDL_Rect logo_dst = {
            10,
            footer.y + (footer.h - logo_size) / 2,
            logo_size,
            logo_size
        };
        SDL_RenderCopy(renderer, logo_texture, NULL, &logo_dst);
    }
}

static void draw_channel_status_banner(SDL_Renderer *renderer,
                                       const UiFonts *fonts,
                                       const GuideTheme *theme,
                                       const ChannelList *channels,
                                       int active_channel,
                                       time_t app_start_time,
                                       time_t now_wall,
                                       int window_width,
                                       int window_height,
                                       const char *numeric_input_buffer,
                                       int numeric_input_length,
                                       int numeric_input_invalid) {
    SDL_Rect banner;
    SDL_Rect channel_pill;
    SDL_Rect info_clip;
    int banner_padding_x;
    int banner_padding_y;
    int divider_x;
    SDL_Color banner_text;
    SDL_Color sub_text;
    char channel_text[96];
    char time_range[64];
    char end_text[32];
    char numeric_display[8];
    time_t start_time;
    time_t end_time;
    double program_seek = 0.0;
    const Channel *channel;
    const ProgramEntry *program;

    if (!renderer || !fonts || !theme || !channels || active_channel < 0 || active_channel >= channels->count) {
        return;
    }

    channel = &channels->items[active_channel];
    program = channel_resolve_program_at_elapsed(channel,
                                                 channel_schedule_elapsed_seconds(app_start_time, now_wall),
                                                 &program_seek,
                                                 NULL);
    if (!program) {
        return;
    }

    banner_padding_x = CHANNEL_BANNER_PADDING_X;
    banner_padding_y = CHANNEL_BANNER_PADDING_Y;
    banner = (SDL_Rect){0, window_height - CHANNEL_BANNER_HEIGHT, window_width, CHANNEL_BANNER_HEIGHT};
    channel_pill = (SDL_Rect){banner.x + banner_padding_x,
                              banner.y + banner_padding_y,
                              CHANNEL_BANNER_PILL_WIDTH,
                              banner.h - (banner_padding_y * 2)};
    divider_x = channel_pill.x + channel_pill.w + CHANNEL_BANNER_DIVIDER_GAP;
    info_clip = (SDL_Rect){divider_x + CHANNEL_BANNER_DIVIDER_GAP,
                           banner.y + banner_padding_y,
                           banner.w - (divider_x + CHANNEL_BANNER_DIVIDER_GAP) - banner_padding_x,
                           banner.h - (banner_padding_y * 2)};
    banner_text = ensure_contrast(theme->ribbon_text, theme->status_mid);
    sub_text = ensure_contrast(theme->row_subtext, theme->status_mid);

    start_time = now_wall - (time_t) program_seek;
    end_time = start_time + (time_t) program->duration_seconds;
    format_time_compact(time_range, sizeof(time_range), start_time);
    format_time_compact(end_text, sizeof(end_text), end_time);
    strncat(time_range, " - ", sizeof(time_range) - strlen(time_range) - 1);
    strncat(time_range, end_text, sizeof(time_range) - strlen(time_range) - 1);
    snprintf(channel_text, sizeof(channel_text), "%s  %d", channel->name, channel->number);

    draw_panel_shadow(renderer, &banner);
    draw_beveled_bar(renderer,
                     &banner,
                     blend_color(theme->status_top, theme->status_mid, 220),
                     theme->status_mid,
                     blend_color(theme->status_bottom, theme->status_mid, 220),
                     color_with_alpha(theme->gloss, 42),
                     theme->panel_border);
    stroke_rect(renderer, &banner, theme->panel_border);
    draw_panel_bevel(renderer, &banner, theme->gloss);

    if (numeric_input_length > 0 || numeric_input_invalid) {
        SDL_Color accent_fill = theme->ribbon_mid;
        SDL_Color accent_border = theme->ribbon_bottom;
        SDL_Color text_color = COLOR_TEXT_LIGHT;

        if (numeric_input_invalid) {
            snprintf(numeric_display, sizeof(numeric_display), "???");
        } else {
            snprintf(numeric_display, sizeof(numeric_display), "%s_", numeric_input_buffer);
        }

        draw_pill_button(renderer, theme, &channel_pill, accent_fill, accent_border);
        draw_text_shadowed(renderer,
                           fonts->medium,
                           numeric_display,
                           &channel_pill,
                           channel_pill.x + 14,
                           channel_pill.y + 8,
                           text_color,
                           color_with_alpha(COLOR_BLACK, 255));
    } else {
        draw_pill_button(renderer, theme, &channel_pill, theme->panel_fill, theme->panel_border);
        draw_text_shadowed(renderer,
                           fonts->medium,
                           channel_text,
                           &channel_pill,
                           channel_pill.x + 14,
                           channel_pill.y + 8,
                           ensure_contrast(theme->panel_text, theme->panel_fill),
                           color_with_alpha(COLOR_BLACK, 255));
    }

    set_draw_color(renderer, theme->status_divider);
    SDL_RenderDrawLine(renderer,
                       divider_x,
                       banner.y + banner_padding_y,
                       divider_x,
                       banner.y + banner.h - banner_padding_y);
    draw_text_clipped(renderer,
                      fonts->medium,
                      program->program_title,
                      &info_clip,
                      info_clip.x,
                       banner.y + 18,
                      banner_text);
    draw_text_clipped(renderer,
                      fonts->small,
                      time_range,
                      &info_clip,
                      info_clip.x,
                       banner.y + 52,
                      sub_text);
}

void ui_render_about_modal(SDL_Renderer *renderer,
                           const UiFonts *fonts,
                           int window_width,
                           int window_height,
                           const GuideTheme *active_theme) {
    SDL_Rect overlay = {0, 0, window_width, window_height};
    SDL_Rect modal = {window_width / 2 - 280, window_height / 2 - 160, 560, 320};
    SDL_Rect title_bar = {modal.x, modal.y, modal.w, 40};
    SDL_Rect body = {modal.x + 18, modal.y + 56, modal.w - 36, modal.h - 88};
    SDL_Color body_bg = blend_color(active_theme->footer_mid, active_theme->panel_fill, 70);
    SDL_Color title_text = readable_text_color(active_theme->ribbon_mid);
    SDL_Color body_text = readable_text_color(body_bg);
    SDL_Color sub_text = ensure_contrast(active_theme->row_subtext, body_bg);

    fill_rect_alpha(renderer, &overlay, (SDL_Color){0, 0, 0, 120});
    draw_panel_shadow(renderer, &modal);
    draw_rounded_top_panel(renderer,
                           &modal,
                           body_bg,
                           active_theme->panel_border,
                           active_theme->rounded_radius > 0 ? active_theme->rounded_radius : 8);
    fill_rect(renderer, &(SDL_Rect){modal.x, modal.y + 40, modal.w, modal.h - 40}, body_bg);
    draw_beveled_bar(renderer,
                     &title_bar,
                     active_theme->ribbon_top,
                     active_theme->ribbon_mid,
                     active_theme->ribbon_bottom,
                     color_with_alpha(active_theme->gloss, 32),
                     active_theme->panel_border);
    draw_text_shadowed(renderer, fonts->medium, "ABOUT", &modal, modal.x + 16, modal.y + 10, title_text, color_with_alpha(COLOR_BLACK, 255));
    draw_text_shadowed(renderer, fonts->large, "FreePassport-C 0.1", &body, body.x, body.y, body_text, color_with_alpha(COLOR_BLACK, 255));
    draw_text_shadowed(renderer,
                       fonts->small,
                       "by markmental (Mark Robillard Jr)",
                       &body,
                       body.x,
                       body.y + 42,
                       sub_text,
                       color_with_alpha(COLOR_BLACK, 255));
    draw_text_shadowed(renderer,
                       fonts->small,
                       "Your media library turned into",
                       &body,
                       body.x,
                       body.y + 86,
                       body_text,
                       color_with_alpha(COLOR_BLACK, 255));
    draw_text_shadowed(renderer,
                       fonts->small,
                       "an early 2000s Passport cable box",
                       &body,
                       body.x,
                       body.y + 112,
                       body_text,
                       color_with_alpha(COLOR_BLACK, 255));
    draw_text_shadowed(renderer,
                       fonts->small,
                       "inspired virtual TV guide",
                       &body,
                       body.x,
                       body.y + 138,
                       body_text,
                       color_with_alpha(COLOR_BLACK, 255));
    draw_text_shadowed(renderer,
                       fonts->small,
                       "A CLOSE   ESC CLOSE",
                       &body,
                       body.x,
                       modal.y + modal.h - 28,
                       sub_text,
                       color_with_alpha(COLOR_BLACK, 255));
}

void ui_render_no_media(SDL_Renderer *renderer, const UiCache *cache, int window_width, int window_height) {
    SDL_Rect full = {0, 0, window_width, window_height};
    fill_rect(renderer, &full, COLOR_NAVY_DARK);
    draw_cached_text(renderer, &cache->no_media_title, 58, 80);
    draw_cached_text(renderer, &cache->no_media_body, 58, 136);
    draw_cached_text(renderer, &cache->no_media_hint, 58, 176);
}

void ui_render_fullscreen(SDL_Renderer *renderer,
                          SDL_Texture *video_texture,
                          int texture_width,
                          int texture_height,
                          int window_width,
                          int window_height,
                          const GuideTheme *theme,
                          const UiFonts *fonts,
                          const ChannelList *channels,
                          int active_channel,
                          time_t app_start_time,
                          time_t now_wall,
                          int show_channel_banner,
                          const char *numeric_input_buffer,
                          int numeric_input_length,
                          int numeric_input_invalid) {
    SDL_Rect window = {0, 0, window_width, window_height};
    fill_rect(renderer, &window, COLOR_BLACK);
    draw_video(renderer, video_texture, texture_width, texture_height, window);
    if (show_channel_banner) {
        draw_channel_status_banner(renderer,
                                   fonts,
                                   theme,
                                   channels,
                                   active_channel,
                                   app_start_time,
                                   now_wall,
                                   window_width,
                                   window_height,
                                   numeric_input_buffer,
                                   numeric_input_length,
                                   numeric_input_invalid);
    }
}

void ui_render_guide(SDL_Renderer *renderer,
                     SDL_Texture *video_texture,
                     int texture_width,
                     int texture_height,
                     int window_width,
                     int window_height,
                     const GuideTheme *theme,
                     const UiFonts *fonts,
                     UiCache *cache,
                     const ChannelList *channels,
                     int active_channel,
                     int selected_channel,
                     time_t app_start_time,
                     time_t now_wall,
                     int guide_time_offset_minutes) {
    double scale_x = (double) window_width / WINDOW_WIDTH;
    double scale_y = (double) window_height / WINDOW_HEIGHT;
    int guide_x_start = (int) (GUIDE_X_START * scale_x);
    int sidebar_width = (int) (GUIDE_SIDEBAR_WIDTH * scale_x);
    int mini_status_height = (int) (26 * scale_y);
    int top_section_gap = (int) (GUIDE_TOP_SECTION_GAP * scale_y);
    SDL_Rect full = {0, 0, window_width, window_height};
    SDL_Rect info_panel = {0, 0, (int) (GUIDE_INFO_WIDTH * scale_x), (int) (GUIDE_INFO_HEIGHT * scale_y)};
    SDL_Rect preview = {window_width - (int) (GUIDE_PREVIEW_WIDTH * scale_x), 0, (int) (GUIDE_PREVIEW_WIDTH * scale_x), (int) (GUIDE_PREVIEW_HEIGHT * scale_y)};
    int mini_status_y = preview.y + preview.h;
    int top_section_bottom = SDL_max(info_panel.y + info_panel.h, mini_status_y + mini_status_height);
    int timeline_header_y = top_section_bottom + top_section_gap;
    int grid_y = timeline_header_y + (int) (GUIDE_STATUS_HEIGHT * scale_y);
    SDL_Rect mini_status_bar = {preview.x, mini_status_y, preview.w, mini_status_height};
    SDL_Rect header_row = {0, timeline_header_y, window_width, (int) (GUIDE_STATUS_HEIGHT * scale_y)};
    SDL_Rect grid = {0, grid_y, window_width, window_height - grid_y - (int) (GUIDE_FOOTER_HEIGHT * scale_y)};
    int row_height = grid.h / GUIDE_VISIBLE_ROWS;
    int timeline_w = window_width - guide_x_start - (int) (20 * scale_x);
    int start_index = selected_channel - 2;
    const Channel *selected_channel_ptr = NULL;
    double pixels_per_minute = timeline_w / 90.0;
    time_t guide_view_start_time = now_wall - (30 * 60) + (guide_time_offset_minutes * 60);
    time_t guide_focus_time = guide_view_start_time + (30 * 60);
    double guide_view_start_seconds = channel_schedule_elapsed_seconds(app_start_time, guide_view_start_time);
    double guide_view_end_seconds = guide_view_start_seconds + TIMELINE_VISIBLE_SECONDS;
    double guide_focus_seconds = channel_schedule_elapsed_seconds(app_start_time, guide_focus_time);

    (void) active_channel;

    if (channels && channels->count > 0 && selected_channel >= 0 && selected_channel < channels->count) {
        selected_channel_ptr = &channels->items[selected_channel];
    }

    fill_three_stop_gradient(renderer,
                             &full,
                             theme->background_top,
                             theme->background_mid,
                             theme->background_bottom);
    draw_info_panel(renderer, fonts, theme, selected_channel_ptr, &info_panel, app_start_time, guide_focus_time);
    draw_panel_shadow(renderer, &preview);
    fill_rect(renderer, &preview, COLOR_BLACK);
    stroke_rect(renderer, &preview, theme->panel_border);
    draw_panel_bevel(renderer, &preview, theme->gloss);
    draw_video(renderer, video_texture, texture_width, texture_height, preview);
    draw_mini_status_bar(renderer, fonts->small, theme, selected_channel_ptr, &mini_status_bar, now_wall);

    if (cache->timeline_label_slot != guide_view_start_time / 60 || cache->timeline_theme != theme) {
        char label[32];
        for (int i = 0; i < 4; ++i) {
            format_clock_label(label, sizeof(label), guide_view_start_time, 30 * i);
            text_texture_destroy(&cache->timeline_labels[i]);
            text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_LIGHT);
        }
        cache->timeline_label_slot = guide_view_start_time / 60;
        cache->timeline_theme = theme;
    }
    draw_timeline_header_cached(renderer,
                                cache,
                                theme,
                                header_row,
                                guide_x_start,
                                pixels_per_minute,
                                timeline_w,
                                (int) (TIMELINE_VISIBLE_SECONDS / 60.0));
    draw_grid_background(renderer, theme, &grid, guide_x_start, row_height, pixels_per_minute);

    if (start_index < 0) {
        start_index = 0;
    }
    if (channels && start_index + GUIDE_VISIBLE_ROWS > channels->count) {
        start_index = SDL_max(0, channels->count - GUIDE_VISIBLE_ROWS);
    }

    for (int row = 0; row < GUIDE_VISIBLE_ROWS; ++row) {
        int channel_index = start_index + row;
        SDL_Rect row_rect = {0, grid.y + row * row_height, window_width, row_height};
        SDL_Rect sidebar = {0, row_rect.y, sidebar_width, row_height};
        SDL_Rect timeline_rect = {guide_x_start, row_rect.y + (int) (6 * scale_y), timeline_w, row_height - (int) (12 * scale_y)};
        SDL_Rect clip = timeline_rect;
        int is_selected = channel_index == selected_channel;
        SDL_Rect inset = {row_rect.x + 4, row_rect.y + 3, row_rect.w - 8, row_rect.h - 6};

        draw_beveled_bar(renderer,
                         &row_rect,
                         is_selected ? theme->row_active_top : theme->row_top,
                         is_selected ? theme->row_active_mid : theme->row_mid,
                         is_selected ? theme->row_active_bottom : theme->row_bottom,
                         theme->gloss,
                         theme->panel_border);
        fill_rect_alpha(renderer, &inset, (SDL_Color){255, 255, 255, is_selected ? 12 : 6});
        draw_beveled_bar(renderer,
                         &sidebar,
                         is_selected ? theme->row_active_top : theme->row_top,
                         is_selected ? theme->row_active_mid : theme->row_mid,
                         is_selected ? theme->row_active_bottom : theme->row_bottom,
                         theme->gloss,
                         theme->panel_border);

        if (!channels || channel_index >= channels->count) {
            continue;
        }

        {
            const Channel *channel = &channels->items[channel_index];
            double focus_program_seek = 0.0;
            const ProgramEntry *focus_program = channel_resolve_program_at_elapsed(channel, guide_focus_seconds, &focus_program_seek, NULL);
            char title[128];
            SDL_Color row_primary = ensure_contrast(is_selected ? theme->row_active_text : theme->row_text,
                                                    is_selected ? theme->row_active_mid : theme->row_mid);
            SDL_Color row_secondary = ensure_contrast(is_selected ? theme->row_active_subtext : theme->row_subtext,
                                                      is_selected ? theme->row_active_mid : theme->row_mid);
            SDL_Color block_text = ensure_contrast(is_selected ? theme->block_active_text : theme->block_text,
                                                   is_selected ? theme->block_active_mid : theme->block_mid);
            SDL_Color block_shadow = color_luma(is_selected ? theme->block_active_mid : theme->block_mid) < 120
                ? color_with_alpha(COLOR_BLACK, 255)
                : color_with_alpha(COLOR_TEXT_LIGHT, 255);

            if (!focus_program) {
                continue;
            }

            if (is_selected) {
                draw_text_shadowed(renderer, fonts->medium, channel->name, &sidebar, 20, row_rect.y + 12, row_primary, color_with_alpha(COLOR_BLACK, 255));
                {
                    char number[16];
                    snprintf(number, sizeof(number), "%d", channel->number);
                    draw_text_shadowed(renderer, fonts->medium, number, &sidebar, 176, row_rect.y + 12, row_primary, color_with_alpha(COLOR_BLACK, 255));
                }
            } else {
                draw_text_shadowed(renderer, fonts->medium, channel->name, &sidebar, 20, row_rect.y + 12, row_primary, color_with_alpha(COLOR_BLACK, 255));
                {
                    char number[16];
                    snprintf(number, sizeof(number), "%d", channel->number);
                    draw_text_shadowed(renderer, fonts->medium, number, &sidebar, 176, row_rect.y + 12, row_primary, color_with_alpha(COLOR_BLACK, 255));
                }
            }
            draw_text_shadowed(renderer,
                               fonts->small,
                               focus_program->file_name,
                               &sidebar,
                               20,
                               row_rect.y + 38,
                               row_secondary,
                               color_with_alpha(COLOR_BLACK, 255));

            SDL_RenderSetClipRect(renderer, &clip);

            {
                double block_cursor_seconds = guide_view_start_seconds;
                int block_guard = channel->program_count;

                if (channel->total_duration_seconds > 0.0) {
                    block_guard *= 1 + (int) (TIMELINE_VISIBLE_SECONDS / channel->total_duration_seconds);
                }
                block_guard += 8;
                if (block_guard < 16) {
                    block_guard = 16;
                }

                while (block_cursor_seconds < guide_view_end_seconds && block_guard-- > 0) {
                    double program_seek = 0.0;
                    const ProgramEntry *program = channel_resolve_program_at_elapsed(channel, block_cursor_seconds, &program_seek, NULL);
                    double program_start_seconds;
                    double program_end_seconds;
                    double visible_start_seconds;
                    double visible_end_seconds;
                    int block_x;
                    int block_w;
                    SDL_Rect block;
                    SDL_Rect title_rect;

                    if (!program || program->duration_seconds <= 0.0) {
                        break;
                    }

                    program_start_seconds = block_cursor_seconds - program_seek;
                    program_end_seconds = program_start_seconds + program->duration_seconds;
                    visible_start_seconds = SDL_max(program_start_seconds, guide_view_start_seconds);
                    visible_end_seconds = SDL_min(program_end_seconds, guide_view_end_seconds);

                    block_cursor_seconds = program_end_seconds;
                    if (visible_end_seconds <= visible_start_seconds) {
                        continue;
                    }

                    block_x = guide_x_start + (int) ((((visible_start_seconds - guide_view_start_seconds) / 60.0) * pixels_per_minute) + 0.5);
                    block_w = (int) ((((visible_end_seconds - visible_start_seconds) / 60.0) * pixels_per_minute) + 0.5);
                    block = (SDL_Rect){block_x + 1, timeline_rect.y + 4, SDL_max(block_w - 2, 22), timeline_rect.h - 8};
                    title_rect = (SDL_Rect){block.x + 8, block.y + 8, block.w - 16, block.h - 16};

                    draw_program_block(renderer,
                                       &block,
                                       is_selected ? theme->block_active_top : theme->block_top,
                                       is_selected ? theme->block_active_mid : theme->block_mid,
                                       is_selected ? theme->block_active_bottom : theme->block_bottom,
                                       theme->gloss,
                                       theme->panel_border,
                                       is_selected,
                                       theme->selection_edge,
                                       is_selected ? theme->block_active_border : theme->block_border);

                    if (title_rect.w > 24) {
                        fit_text_with_ellipsis(is_selected ? fonts->medium : fonts->small,
                                               program->program_title,
                                               title_rect.w,
                                               title,
                                               sizeof(title));
                        draw_text_shadowed(renderer,
                                           is_selected ? fonts->medium : fonts->small,
                                           title,
                                           &title_rect,
                                           title_rect.x,
                                           title_rect.y,
                                           block_text,
                                           block_shadow);
                    }
                }
            }

            SDL_RenderSetClipRect(renderer, NULL);
        }
    }

    if (selected_channel_ptr && selected_channel >= 0 && start_index <= selected_channel && selected_channel < start_index + GUIDE_VISIBLE_ROWS) {
        SDL_Rect highlight = {0, grid.y + (selected_channel - start_index) * row_height, window_width, row_height};
        draw_selection_glow(renderer, &highlight, theme->selection_edge);
        draw_selection_glow(renderer, &(SDL_Rect){guide_x_start, highlight.y + (int) (6 * scale_y), timeline_w, row_height - (int) (12 * scale_y)}, theme->selection_edge);
    }

    draw_footer_legend(renderer, fonts, theme, window_width, window_height, cache->logo_texture);
}

void ui_render_theme_picker(SDL_Renderer *renderer,
                            const UiFonts *fonts,
                            int window_width,
                            int window_height,
                            const GuideTheme *active_theme,
                            int current_theme_index,
                            int selected_theme_index) {
    SDL_Rect overlay = {0, 0, window_width, window_height};
    SDL_Rect modal = {window_width / 2 - 250, window_height / 2 - 180, 500, 360};
    SDL_Rect title_bar = {modal.x, modal.y, modal.w, 38};

    (void) current_theme_index;
    fill_rect_alpha(renderer, &overlay, (SDL_Color){0, 0, 0, 120});
    draw_panel_shadow(renderer, &modal);
    draw_rounded_top_panel(renderer, &modal, active_theme->panel_fill, active_theme->panel_border, active_theme->rounded_radius > 0 ? active_theme->rounded_radius : 8);
    fill_rect(renderer, &(SDL_Rect){modal.x, modal.y + 38, modal.w, modal.h - 38}, active_theme->footer_mid);
    draw_beveled_bar(renderer, &title_bar, active_theme->ribbon_top, active_theme->ribbon_mid, active_theme->ribbon_bottom, active_theme->gloss, active_theme->panel_border);
    draw_text_clipped(renderer, fonts->medium, "THEME SELECT", &modal, modal.x + 16, modal.y + 10, active_theme->ribbon_text);

    for (int i = 0; i < GUIDE_THEME_COUNT; ++i) {
        SDL_Rect item = {modal.x + 18, modal.y + 52 + i * 28, modal.w - 36, 24};
        SDL_Rect swatch = {modal.x + modal.w - 118, item.y + 4, 92, 14};
        char label[96];
        const GuideTheme *theme = &GUIDE_THEMES[i];

        if (i == selected_theme_index) {
            draw_beveled_bar(renderer, &item, theme->row_active_top, theme->row_active_mid, theme->row_active_bottom, theme->gloss, theme->panel_border);
            draw_selection_glow(renderer, &item, theme->selection_edge);
        }

        snprintf(label, sizeof(label), "%c %s", i == selected_theme_index ? '>' : ' ', theme->name);
        draw_text_clipped(renderer,
                          fonts->small,
                          label,
                          &item,
                          item.x + 8,
                          item.y + 4,
                          i == selected_theme_index ? theme->row_active_text : active_theme->panel_text);
        fill_three_stop_gradient(renderer, &swatch, theme->background_top, theme->background_mid, theme->background_bottom);
        stroke_rect(renderer, &swatch, theme->panel_border);
    }

    draw_text_clipped(renderer,
                      fonts->small,
                      "ENTER APPLY   ESC CANCEL   B CLOSE",
                      &modal,
                      modal.x + 18,
                      modal.y + modal.h - 28,
                      active_theme->panel_text);
}

static char *get_exe_dir(void);

int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels) {
    char buffer[256];

    if (!cache || !renderer || !fonts) {
        return -1;
    }

    memset(cache, 0, sizeof(*cache));
    cache->renderer = renderer;
    cache->timeline_label_slot = -1;
    cache->timeline_labels = calloc(4, sizeof(UiTextTexture));
    if (!cache->timeline_labels) {
        ui_cache_destroy(cache);
        return -1;
    }

    if (text_texture_init(&cache->no_media_title, renderer, fonts->large, "FreePassport-C Media Player", COLOR_TEXT_LIGHT) != 0 ||
        text_texture_init(&cache->no_media_body, renderer, fonts->medium, "No channels found in ./media", COLOR_HIGHLIGHT_YELLOW) != 0 ||
        text_texture_init(&cache->no_media_hint, renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", COLOR_PALE_BLUE) != 0 ||
        text_texture_init(&cache->footer_a, renderer, fonts->medium, "A", COLOR_TEXT_DARK) != 0 ||
        text_texture_init(&cache->footer_time, renderer, fonts->small, "TIME", COLOR_TEXT_DARK) != 0 ||
        text_texture_init(&cache->footer_b, renderer, fonts->medium, "B", COLOR_TEXT_LIGHT) != 0 ||
        text_texture_init(&cache->footer_theme, renderer, fonts->small, "THEME", COLOR_TEXT_DARK) != 0 ||
        text_texture_init(&cache->footer_c, renderer, fonts->medium, "C", COLOR_TEXT_LIGHT) != 0 ||
        text_texture_init(&cache->footer_title, renderer, fonts->small, "TITLE", COLOR_TEXT_DARK) != 0) {
        ui_cache_destroy(cache);
        return -1;
    }

    {
        char *exe_dir = get_exe_dir();
        if (exe_dir) {
            char logo_path[512];
            snprintf(logo_path, sizeof(logo_path), "%slogo.png", exe_dir);
            cache->logo_texture = load_png_texture(renderer, logo_path, NULL, NULL);
            SDL_free(exe_dir);
        }

        if (!cache->logo_texture) {
            cache->logo_texture = load_png_texture(renderer, "logo.png", NULL, NULL);
        }
    }

    if (channels && channels->count > 0) {
        cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache));
        if (!cache->channels) {
            ui_cache_destroy(cache);
            return -1;
        }
        cache->channel_count = channels->count;

        for (int i = 0; i < channels->count; ++i) {
            const Channel *channel = &channels->items[i];
            const ProgramEntry *program = channel->program_count > 0 ? &channel->programs[0] : NULL;
            snprintf(buffer, sizeof(buffer), "%d", channel->number);
            if (text_texture_init(&cache->channels[i].name_medium, renderer, fonts->medium, channel->name, COLOR_TEXT_LIGHT) != 0 ||
                text_texture_init(&cache->channels[i].number_medium, renderer, fonts->medium, buffer, COLOR_TEXT_LIGHT) != 0 ||
                text_texture_init(&cache->channels[i].file_small, renderer, fonts->small, program ? program->file_name : "", COLOR_PALE_BLUE) != 0 ||
                text_texture_init(&cache->channels[i].program_small_light, renderer, fonts->small, program ? program->program_title : "", COLOR_TEXT_LIGHT) != 0 ||
                text_texture_init(&cache->channels[i].program_medium_dark, renderer, fonts->medium, program ? program->program_title : "", COLOR_TEXT_DARK) != 0) {
                ui_cache_destroy(cache);
                return -1;
            }

            snprintf(buffer, sizeof(buffer), "%.0f min  -  %s", program ? program->duration_seconds / 60.0 : 0.0, program ? program->file_name : "");
            if (text_texture_init(&cache->channels[i].detail_small_dark, renderer, fonts->small, buffer, COLOR_TEXT_DARK) != 0) {
                ui_cache_destroy(cache);
                return -1;
            }
        }
    }

    return 0;
}

void ui_cache_destroy(UiCache *cache) {
    if (!cache) {
        return;
    }

    if (cache->logo_texture) {
        SDL_DestroyTexture(cache->logo_texture);
    }

    text_texture_destroy(&cache->no_media_title);
    text_texture_destroy(&cache->no_media_body);
    text_texture_destroy(&cache->no_media_hint);
    text_texture_destroy(&cache->footer_a);
    text_texture_destroy(&cache->footer_time);
    text_texture_destroy(&cache->footer_b);
    text_texture_destroy(&cache->footer_theme);
    text_texture_destroy(&cache->footer_c);
    text_texture_destroy(&cache->footer_title);

    if (cache->timeline_labels) {
        for (int i = 0; i < 4; ++i) {
            text_texture_destroy(&cache->timeline_labels[i]);
        }
        free(cache->timeline_labels);
    }

    if (cache->channels) {
        for (int i = 0; i < cache->channel_count; ++i) {
            text_texture_destroy(&cache->channels[i].name_medium);
            text_texture_destroy(&cache->channels[i].number_medium);
            text_texture_destroy(&cache->channels[i].file_small);
            text_texture_destroy(&cache->channels[i].program_small_light);
            text_texture_destroy(&cache->channels[i].program_medium_dark);
            text_texture_destroy(&cache->channels[i].detail_small_dark);
        }
        free(cache->channels);
    }

    memset(cache, 0, sizeof(*cache));
}

static char *get_exe_dir(void) {
    return SDL_GetBasePath();
}

int ui_load_fonts(UiFonts *fonts) {
    char *exe_dir;

    if (!fonts) {
        return -1;
    }

    memset(fonts, 0, sizeof(*fonts));

    exe_dir = get_exe_dir();
    if (exe_dir) {
        char font_path[512];
        snprintf(font_path, sizeof(font_path), "%sBigBlueTermPlusNerdFontMono-Regular.ttf", exe_dir);

        fonts->small = TTF_OpenFont(font_path, 13);
        fonts->medium = TTF_OpenFont(font_path, 17);
        fonts->large = TTF_OpenFont(font_path, 22);

        SDL_free(exe_dir);

        if (fonts->small && fonts->medium && fonts->large) {
            return 0;
        }
        ui_destroy_fonts(fonts);
    }

    for (size_t i = 0; i < sizeof(FONT_CANDIDATES) / sizeof(FONT_CANDIDATES[0]); ++i) {
        fonts->small = TTF_OpenFont(FONT_CANDIDATES[i], 13);
        fonts->medium = TTF_OpenFont(FONT_CANDIDATES[i], 17);
        fonts->large = TTF_OpenFont(FONT_CANDIDATES[i], 22);
        if (fonts->small && fonts->medium && fonts->large) {
            return 0;
        }
        ui_destroy_fonts(fonts);
    }

    return -1;
}

void ui_destroy_fonts(UiFonts *fonts) {
    if (!fonts) {
        return;
    }

    if (fonts->small) {
        TTF_CloseFont(fonts->small);
    }
    if (fonts->medium) {
        TTF_CloseFont(fonts->medium);
    }
    if (fonts->large) {
        TTF_CloseFont(fonts->large);
    }
    memset(fonts, 0, sizeof(*fonts));
}
