#define _POSIX_C_SOURCE 200809L

#include "app.h"

#include <SDL2/SDL_ttf.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define GUIDE_BROWSE_STEP_MINUTES 30
#define GUIDE_BROWSE_MAX_AHEAD_MINUTES (12 * 60)
#define GUIDE_BROWSE_MAX_OFFSET_MINUTES (GUIDE_BROWSE_MAX_AHEAD_MINUTES - ((int) (TIMELINE_VISIBLE_SECONDS / 60.0) - 30))
#define CHANNEL_BANNER_DURATION_MS 5000
#define CHANNEL_SWITCH_LOCK_DURATION_MS 7000
#define NUMERIC_INPUT_TIMEOUT_MS 1000

static void configure_runtime_environment(void) {
    char runtime_dir[64];
    char pulse_socket[96];
    const char *display;

    if (!getenv("XDG_RUNTIME_DIR") || getenv("XDG_RUNTIME_DIR")[0] == '\0') {
        snprintf(runtime_dir, sizeof(runtime_dir), "/run/user/%u", (unsigned int) getuid());
        if (access(runtime_dir, R_OK | W_OK | X_OK) == 0) {
            setenv("XDG_RUNTIME_DIR", runtime_dir, 1);
        }
    }

    display = getenv("DISPLAY");
    if ((!display || display[0] == '\0') && access("/tmp/.X11-unix/X0", R_OK | W_OK) == 0) {
        setenv("DISPLAY", ":0", 1);
    }

    if (getenv("XDG_RUNTIME_DIR") && getenv("XDG_RUNTIME_DIR")[0] != '\0' &&
        (!getenv("PULSE_SERVER") || getenv("PULSE_SERVER")[0] == '\0')) {
        snprintf(pulse_socket, sizeof(pulse_socket), "%s/pulse/native", getenv("XDG_RUNTIME_DIR"));
        if (access(pulse_socket, R_OK | W_OK) == 0) {
            setenv("PULSE_SERVER", pulse_socket, 0);
        }
    }
}

static void log_runtime_environment(const char *phase) {
    const char *display = getenv("DISPLAY");
    const char *wayland_display = getenv("WAYLAND_DISPLAY");
    const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
    const char *pulse_server = getenv("PULSE_SERVER");
    const char *video_driver = SDL_GetCurrentVideoDriver();

    fprintf(stderr,
            "%s: DISPLAY=%s WAYLAND_DISPLAY=%s XDG_RUNTIME_DIR=%s PULSE_SERVER=%s SDL_VIDEODRIVER=%s\n",
            phase ? phase : "startup",
            display && display[0] != '\0' ? display : "<unset>",
            wayland_display && wayland_display[0] != '\0' ? wayland_display : "<unset>",
            runtime_dir && runtime_dir[0] != '\0' ? runtime_dir : "<unset>",
            pulse_server && pulse_server[0] != '\0' ? pulse_server : "<unset>",
            video_driver && video_driver[0] != '\0' ? video_driver : "<pending>");
}

static void destroy_video_texture(App *app) {
    if (app->video_texture) {
        SDL_DestroyTexture(app->video_texture);
        app->video_texture = NULL;
        app->texture_width = 0;
        app->texture_height = 0;
    }
}

static void begin_startup_handoff(App *app) {
    if (!app) {
        return;
    }

    app->startup_handoff_active = 1;
    app->startup_handoff_until = 0;
}

static int update_video_texture(App *app) {
    FrameData frame = {0};
    double sync_clock;
    double lead_tolerance;
    Uint64 now_ticks;

    now_ticks = SDL_GetTicks64();
    sync_clock = player_get_sync_clock(&app->player, now_ticks);
    lead_tolerance = app->startup_handoff_active ? 0.240 : 0.060;

    if (!player_consume_synced_frame(&app->player, &frame, sync_clock, lead_tolerance)) {
        if (!app->startup_handoff_active || !player_consume_latest_frame(&app->player, &frame)) {
            return 0;
        }
    }

    if (!app->video_texture || app->texture_width != frame.width || app->texture_height != frame.height) {
        destroy_video_texture(app);
        app->video_texture = SDL_CreateTexture(app->renderer,
                                               SDL_PIXELFORMAT_IYUV,
                                               SDL_TEXTUREACCESS_STREAMING,
                                               frame.width,
                                               frame.height);
        if (!app->video_texture) {
            frame_data_free(&frame);
            return -1;
        }
        app->texture_width = frame.width;
        app->texture_height = frame.height;
        SDL_SetTextureBlendMode(app->video_texture, SDL_BLENDMODE_NONE);
    }

    SDL_UpdateYUVTexture(app->video_texture,
                         NULL,
                         frame.plane_y,
                         frame.pitch_y,
                         frame.plane_u,
                         frame.pitch_u,
                         frame.plane_v,
                         frame.pitch_v);
    frame_data_free(&frame);

    if (app->startup_handoff_active && app->startup_handoff_until != 0 && SDL_GetTicks() >= app->startup_handoff_until) {
        app->startup_handoff_active = 0;
        app->startup_handoff_until = 0;
    }

    return 0;
}

static void tune_relative(App *app, int delta) {
    int next_index;
    Uint32 now;

    if (app->channels.count == 0) {
        return;
    }

    now = SDL_GetTicks();
    if (now < app->channel_switch_lock_until) {
        return;
    }
    app->channel_switch_lock_until = now + CHANNEL_SWITCH_LOCK_DURATION_MS;

    next_index = app->player.current_index;
    fprintf(stderr, "[DEBUG tune_relative] current_index=%d, delta=%d\n", next_index, delta);
    if (next_index < 0) {
        next_index = 0;
    }
    next_index = (next_index + delta + app->channels.count) % app->channels.count;
    fprintf(stderr, "[DEBUG tune_relative] calculated next_index=%d\n", next_index);
    destroy_video_texture(app);
    begin_startup_handoff(app);
    player_tune(&app->player, next_index);
    fprintf(stderr, "[DEBUG tune_relative] after player_tune, current_index=%d\n", app->player.current_index);
    app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
    app->numeric_input_invalid = 0;
}

static int find_channel_by_number(const ChannelList *channels, int number) {
    for (int i = 0; i < channels->count; i++) {
        if (channels->items[i].number == number) {
            return i;
        }
    }
    return -1;
}

static void tune_to_channel(App *app, int channel_index) {
    Uint32 now;

    if (app->channels.count == 0 || channel_index < 0 || channel_index >= app->channels.count) {
        return;
    }

    fprintf(stderr, "[DEBUG tune_to_channel] requested channel_index=%d, current_index=%d\n", channel_index, app->player.current_index);
    now = SDL_GetTicks();
    if (now < app->channel_switch_lock_until) {
        fprintf(stderr, "[DEBUG tune_to_channel] LOCKED, aborting\n");
        return;
    }
    app->channel_switch_lock_until = now + CHANNEL_SWITCH_LOCK_DURATION_MS;

    destroy_video_texture(app);
    begin_startup_handoff(app);
    player_tune(&app->player, channel_index);
    fprintf(stderr, "[DEBUG tune_to_channel] after player_tune, current_index=%d\n", app->player.current_index);
    app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
    app->numeric_input_invalid = 0;
}

static void process_numeric_input(App *app) {
    int target_number;
    int channel_index;

    if (app->numeric_input_length == 0) {
        return;
    }

    target_number = atoi(app->numeric_input_buffer);
    fprintf(stderr, "[DEBUG process_numeric_input] input='%s', target_number=%d, current_index=%d\n",
            app->numeric_input_buffer, target_number, app->player.current_index);
    channel_index = find_channel_by_number(&app->channels, target_number);
    fprintf(stderr, "[DEBUG process_numeric_input] found channel_index=%d for number %d\n", channel_index, target_number);

    if (channel_index >= 0) {
        tune_to_channel(app, channel_index);
        app->numeric_input_invalid = 0;
    } else {
        app->numeric_input_invalid = 1;
        app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
    }

    app->numeric_input_length = 0;
    app->numeric_input_buffer[0] = '\0';
}

static void handle_numeric_key(App *app, int digit) {
    Uint32 now;

    if (app->channels.count == 0) {
        return;
    }

    now = SDL_GetTicks();

    if (now < app->channel_switch_lock_until) {
        return;
    }

    if (app->numeric_input_length == 0) {
        app->numeric_input_timeout = now + NUMERIC_INPUT_TIMEOUT_MS;
        app->numeric_input_invalid = 0;
    }

    if (app->numeric_input_length < 3) {
        app->numeric_input_buffer[app->numeric_input_length] = '0' + digit;
        app->numeric_input_length++;
        app->numeric_input_buffer[app->numeric_input_length] = '\0';
        app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
    }

    if (app->numeric_input_length >= 3) {
        process_numeric_input(app);
    }
}

static void browse_guide_time(App *app, int delta_minutes) {
    int next_offset;

    if (!app || app->mode != MODE_GUIDE) {
        return;
    }

    next_offset = app->guide_time_offset_minutes + delta_minutes;
    if (next_offset < 0) {
        next_offset = 0;
    }
    if (next_offset > GUIDE_BROWSE_MAX_OFFSET_MINUTES) {
        next_offset = GUIDE_BROWSE_MAX_OFFSET_MINUTES;
    }
    app->guide_time_offset_minutes = next_offset;
}

static void toggle_fullscreen(App *app) {
    Uint32 flags;

    if (!app || !app->window) {
        return;
    }

    flags = app->is_fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
    if (SDL_SetWindowFullscreen(app->window, flags) == 0) {
        app->is_fullscreen = !app->is_fullscreen;
    } else {
        fprintf(stderr, "Fullscreen toggle failed: %s\n", SDL_GetError());
    }
}

static void handle_event(App *app, const SDL_Event *event) {
    if (event->type == SDL_QUIT) {
        app->running = 0;
        return;
    }

    if (event->type != SDL_KEYDOWN || event->key.repeat) {
        return;
    }

    if (app->about_modal_open) {
        switch (event->key.keysym.sym) {
            case SDLK_ESCAPE:
            case SDLK_a:
                app->about_modal_open = 0;
                return;
            default:
                return;
        }
    }

    if (app->theme_picker_open) {
        switch (event->key.keysym.sym) {
            case SDLK_ESCAPE:
            case SDLK_b:
                app->theme_picker_open = 0;
                return;
            case SDLK_UP:
                app->theme_picker_selection = (app->theme_picker_selection + GUIDE_THEME_COUNT - 1) % GUIDE_THEME_COUNT;
                return;
            case SDLK_DOWN:
                app->theme_picker_selection = (app->theme_picker_selection + 1) % GUIDE_THEME_COUNT;
                return;
            case SDLK_RETURN:
            case SDLK_KP_ENTER:
                app->theme_index = app->theme_picker_selection;
                app->theme_picker_open = 0;
                return;
            default:
                return;
        }
    }

    switch (event->key.keysym.sym) {
        case SDLK_ESCAPE:
            if (app->mode == MODE_GUIDE) {
                app->mode = MODE_FULLSCREEN;
            } else {
                app->running = 0;
            }
            break;
        case SDLK_TAB:
            if (app->mode == MODE_FULLSCREEN) {
                app->mode = MODE_GUIDE;
                app->guide_selected_index = app->player.current_index;
            } else {
                app->mode = MODE_FULLSCREEN;
            }
            break;
        case SDLK_RETURN:
        case SDLK_KP_ENTER:
            if (app->mode == MODE_GUIDE) {
                tune_to_channel(app, app->guide_selected_index);
                app->mode = MODE_FULLSCREEN;
            }
            break;
        case SDLK_f:
            toggle_fullscreen(app);
            break;
        case SDLK_b:
            if (app->mode == MODE_FULLSCREEN) {
                app->mode = MODE_GUIDE;
            }
            app->about_modal_open = 0;
            app->theme_picker_open = !app->theme_picker_open;
            app->theme_picker_selection = app->theme_index;
            break;
        case SDLK_a:
            if (app->mode == MODE_FULLSCREEN) {
                app->mode = MODE_GUIDE;
            }
            app->theme_picker_open = 0;
            app->about_modal_open = !app->about_modal_open;
            break;
        case SDLK_UP:
            if (app->mode == MODE_GUIDE && app->channels.count > 0) {
                app->guide_selected_index = (app->guide_selected_index - 1 + app->channels.count) % app->channels.count;
            } else if (app->mode != MODE_GUIDE) {
                tune_relative(app, 1);
            }
            break;
        case SDLK_DOWN:
            if (app->mode == MODE_GUIDE && app->channels.count > 0) {
                app->guide_selected_index = (app->guide_selected_index + 1) % app->channels.count;
            } else if (app->mode != MODE_GUIDE) {
                tune_relative(app, -1);
            }
            break;
        case SDLK_LEFT:
            browse_guide_time(app, -GUIDE_BROWSE_STEP_MINUTES);
            break;
        case SDLK_RIGHT:
            browse_guide_time(app, GUIDE_BROWSE_STEP_MINUTES);
            break;
        case SDLK_0: case SDLK_1: case SDLK_2: case SDLK_3: case SDLK_4:
        case SDLK_5: case SDLK_6: case SDLK_7: case SDLK_8: case SDLK_9:
            handle_numeric_key(app, event->key.keysym.sym - SDLK_0);
            break;
        case SDLK_KP_0: case SDLK_KP_1: case SDLK_KP_2: case SDLK_KP_3: case SDLK_KP_4:
        case SDLK_KP_5: case SDLK_KP_6: case SDLK_KP_7: case SDLK_KP_8: case SDLK_KP_9:
            handle_numeric_key(app, event->key.keysym.sym - SDLK_KP_0);
            break;
        default:
            break;
    }
}

int app_init(App *app) {
    memset(app, 0, sizeof(*app));
    app->running = 1;
    app->mode = MODE_FULLSCREEN;
    app->last_blackout_state = 1;
    app->theme_index = 0;

    configure_runtime_environment();
    log_runtime_environment("startup-before-sdl");

    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_EVENTS) != 0) {
        fprintf(stderr, "SDL init failed: %s\n", SDL_GetError());
        return -1;
    }

    log_runtime_environment("startup-after-sdl");
    if (SDL_GetCurrentVideoDriver() && strcmp(SDL_GetCurrentVideoDriver(), "offscreen") == 0) {
        fprintf(stderr, "SDL selected offscreen video driver unexpectedly; refusing to continue without a visible display.\n");
        return -1;
    }

    app->app_start_time = time(NULL);
    app->app_start_ticks = SDL_GetTicks64();
    srand((unsigned int)app->app_start_time);

    if (TTF_Init() != 0) {
        fprintf(stderr, "SDL_ttf init failed: %s\n", TTF_GetError());
        return -1;
    }

    app->window = SDL_CreateWindow("Passport-C Media Player",
                                   SDL_WINDOWPOS_CENTERED,
                                   SDL_WINDOWPOS_CENTERED,
                                   WINDOW_WIDTH,
                                   WINDOW_HEIGHT,
                                   SDL_WINDOW_SHOWN);
    app->renderer = SDL_CreateRenderer(app->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

    if (!app->window || !app->renderer) {
        fprintf(stderr, "SDL window or renderer creation failed: %s\n", SDL_GetError());
        return -1;
    }

    if (ui_load_fonts(&app->fonts) != 0) {
        fprintf(stderr, "Unable to load a fallback font.\n");
        return -1;
    }

    if (channel_list_load(&app->channels, "./media") < 0) {
        fprintf(stderr, "Unable to scan ./media.\n");
        return -1;
    }

    if (player_init(&app->player, &app->channels, app->app_start_ticks) != 0) {
        fprintf(stderr, "Unable to initialize player.\n");
        return -1;
    }

    if (ui_cache_init(&app->ui_cache, app->renderer, &app->fonts, &app->channels) != 0) {
        fprintf(stderr, "Unable to initialize UI cache.\n");
        return -1;
    }

    if (app->channels.count > 0) {
        begin_startup_handoff(app);
        player_tune(&app->player, 0);
        app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
    }

    return 0;
}

void app_run(App *app) {
    SDL_Event event;

    while (app->running) {
        int in_blackout;
        int output_width = WINDOW_WIDTH;
        int output_height = WINDOW_HEIGHT;
        Uint64 now_ticks;
        time_t now_wall;

        while (SDL_PollEvent(&event)) {
            handle_event(app, &event);
        }

        if (app->numeric_input_length > 0 && SDL_GetTicks() >= app->numeric_input_timeout) {
            process_numeric_input(app);
        }

        if (update_video_texture(app) != 0) {
            app->running = 0;
            break;
        }

        if (SDL_GetRendererOutputSize(app->renderer, &output_width, &output_height) != 0) {
            output_width = WINDOW_WIDTH;
            output_height = WINDOW_HEIGHT;
        }

        in_blackout = player_is_in_blackout(&app->player);
        now_ticks = SDL_GetTicks64();
        now_wall = channel_wall_time_from_ticks(app->app_start_time, app->app_start_ticks, now_ticks);
        
        static Uint32 last_debug_print = 0;
        if (now_ticks - last_debug_print > 1000) {
            fprintf(stderr, "[DEBUG main_loop] current_index=%d, channel_number=%d, lock_remaining=%d\n",
                    app->player.current_index,
                    app->player.current_index >= 0 ? app->channels.items[app->player.current_index].number : -1,
                    (int)(app->channel_switch_lock_until - SDL_GetTicks()));
            last_debug_print = now_ticks;
        }
        if (app->last_blackout_state && !in_blackout) {
            app->startup_handoff_active = 1;
            app->startup_handoff_until = SDL_GetTicks() + 400;
            player_set_catchup_until(&app->player, app->startup_handoff_until);
        }
        app->last_blackout_state = in_blackout;

        if (app->channels.count == 0) {
            ui_render_no_media(app->renderer, &app->ui_cache, output_width, output_height);
        } else if (app->mode == MODE_GUIDE) {
            if (!in_blackout) {
                player_resume_audio(&app->player);
            }
            ui_render_guide(app->renderer,
                            in_blackout ? NULL : app->video_texture,
                            app->texture_width,
                            app->texture_height,
                            output_width,
                            output_height,
                            &GUIDE_THEMES[app->theme_index],
                            &app->fonts,
                            &app->ui_cache,
                            &app->channels,
                            app->player.current_index,
                            app->guide_selected_index,
                            app->app_start_time,
                            now_wall,
                            app->guide_time_offset_minutes);
            if (app->theme_picker_open) {
                ui_render_theme_picker(app->renderer,
                                       &app->fonts,
                                       output_width,
                                       output_height,
                                       &GUIDE_THEMES[app->theme_index],
                                       app->theme_index,
                                       app->theme_picker_selection);
            } else if (app->about_modal_open) {
                ui_render_about_modal(app->renderer,
                                      &app->fonts,
                                      output_width,
                                      output_height,
                                      &GUIDE_THEMES[app->theme_index]);
            }
        } else {
            if (!in_blackout) {
                player_resume_audio(&app->player);
            }
            ui_render_fullscreen(app->renderer,
                                 in_blackout ? NULL : app->video_texture,
                                 app->texture_width,
                                 app->texture_height,
                                 output_width,
                                 output_height,
                                 &GUIDE_THEMES[app->theme_index],
                                 &app->fonts,
                                 &app->channels,
                                 app->player.current_index,
                                 app->app_start_time,
                                 now_wall,
                                 SDL_GetTicks() < app->channel_banner_until,
                                 app->numeric_input_buffer,
                                 app->numeric_input_length,
                                 app->numeric_input_invalid);
        }

        SDL_RenderPresent(app->renderer);
    }
}

void app_destroy(App *app) {
    if (!app) {
        return;
    }

    player_destroy(&app->player);
    channel_list_destroy(&app->channels);
    destroy_video_texture(app);
    ui_cache_destroy(&app->ui_cache);
    ui_destroy_fonts(&app->fonts);
    if (app->renderer) {
        SDL_DestroyRenderer(app->renderer);
    }
    if (app->window) {
        SDL_DestroyWindow(app->window);
    }
    TTF_Quit();
    SDL_Quit();
}
