#include "player.h"

#include <limits.h>
#include <math.h>
#include <stdint.h>
#include <libavutil/channel_layout.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libavutil/samplefmt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct DecoderThreadArgs {
    Player *player;
    const Channel *channel;
} DecoderThreadArgs;

static void player_set_error(Player *player, const char *message) {
    if (!player || !player->error_mutex) {
        return;
    }

    SDL_LockMutex(player->error_mutex);
    snprintf(player->last_error, sizeof(player->last_error), "%s", message ? message : "Unknown playback error");
    SDL_UnlockMutex(player->error_mutex);
}

static int should_stop(Player *player) {
    return player ? SDL_AtomicGet(&player->stop_requested) : 1;
}

static double player_audio_bytes_per_second(const Player *player) {
    if (!player || player->audio_spec.freq <= 0 || player->audio_spec.channels <= 0) {
        return 0.0;
    }

    return (double) player->audio_spec.freq * (double) player->audio_spec.channels * (double) sizeof(int16_t);
}

static void player_reset_clock_state(Player *player) {
    if (!player || !player->clock_mutex) {
        return;
    }

    SDL_LockMutex(player->clock_mutex);
    player->latest_audio_end_pts = 0.0;
    player->audio_started = 0;
    player->preroll_ready = 0;
    SDL_UnlockMutex(player->clock_mutex);
}

static void player_mark_preroll_ready(Player *player) {
    Uint32 minimum_release;
    int already_ready;

    if (!player || !player->clock_mutex) {
        return;
    }

    minimum_release = SDL_GetTicks() + 120;
    SDL_LockMutex(player->clock_mutex);
    already_ready = player->preroll_ready;
    player->preroll_ready = 1;
    SDL_UnlockMutex(player->clock_mutex);
    if (!already_ready && player->tuning_blackout_until < minimum_release) {
        player->tuning_blackout_until = minimum_release;
    }
}

static void player_clear_audio(Player *player) {
    if (!player || !player->audio_mutex) {
        return;
    }

    SDL_LockMutex(player->audio_mutex);
    if (player->audio_device != 0) {
        SDL_ClearQueuedAudio(player->audio_device);
        SDL_PauseAudioDevice(player->audio_device, 1);
    }
    SDL_UnlockMutex(player->audio_mutex);
    player_reset_clock_state(player);
}

static void player_close_audio(Player *player) {
    if (!player || !player->audio_mutex) {
        return;
    }

    SDL_LockMutex(player->audio_mutex);
    if (player->audio_device != 0) {
        SDL_ClearQueuedAudio(player->audio_device);
        SDL_CloseAudioDevice(player->audio_device);
        player->audio_device = 0;
        memset(&player->audio_spec, 0, sizeof(player->audio_spec));
    }
    SDL_UnlockMutex(player->audio_mutex);
}

static int player_ensure_audio_backend(Player *player) {
    static const char *driver_candidates[] = {
        "pulseaudio",
        "pipewire",
        "alsa"
    };
    const char *current_driver;
    char error_buffer[256] = {0};

    if (!player) {
        return -1;
    }

    current_driver = SDL_GetCurrentAudioDriver();
    if (current_driver && current_driver[0] != '\0') {
        snprintf(player->audio_driver, sizeof(player->audio_driver), "%s", current_driver);
        return 0;
    }

    for (size_t i = 0; i < sizeof(driver_candidates) / sizeof(driver_candidates[0]); ++i) {
        if (SDL_AudioInit(driver_candidates[i]) == 0) {
            snprintf(player->audio_driver, sizeof(player->audio_driver), "%s", driver_candidates[i]);
            fprintf(stderr, "audio: using SDL audio driver %s\n", player->audio_driver);
            return 0;
        }

        if (error_buffer[0] == '\0') {
            snprintf(error_buffer,
                     sizeof(error_buffer),
                     "%s audio init failed: %s",
                     driver_candidates[i],
                     SDL_GetError());
        }
        SDL_AudioQuit();
    }

    player_set_error(player, error_buffer[0] != '\0' ? error_buffer : "Unable to initialize any SDL audio driver");
    return -1;
}

static int player_ensure_audio_device(Player *player) {
    SDL_AudioSpec desired;
    SDL_AudioSpec obtained;
    int result = 0;

    if (!player || !player->audio_mutex) {
        return -1;
    }

    if (player_ensure_audio_backend(player) != 0) {
        return -1;
    }

    SDL_zero(desired);
    desired.freq = 48000;
    desired.format = AUDIO_S16SYS;
    desired.channels = 2;
    desired.samples = 1024;
    desired.callback = NULL;

    SDL_LockMutex(player->audio_mutex);
    if (player->audio_device == 0) {
        player->audio_device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
        if (player->audio_device == 0) {
            result = -1;
        } else {
            player->audio_spec = obtained;
        }
    }
    SDL_UnlockMutex(player->audio_mutex);

    if (result != 0) {
        player_set_error(player, SDL_GetError());
    }

    return result;
}

static int queue_audio_frame(Player *player,
                            SwrContext **swr_context,
                            AVCodecContext *audio_codec_context,
                            AVRational audio_time_base,
                            AVFrame *audio_frame,
                            double pts_base_offset,
                            int *queued_any_audio) {
    uint8_t **converted_data = NULL;
    int out_rate;
    int out_channels;
    enum AVSampleFormat out_format;
    AVChannelLayout out_layout;
    int swr_result;
    int max_samples;
    int sample_count;
    int buffer_size;
    int queued_limit;
    int line_size = 0;
    int result;
    double audio_pts;
    double frame_duration;

    if (!player || !audio_codec_context || !audio_frame) {
        return -1;
    }

    if (player_ensure_audio_device(player) != 0) {
        return -1;
    }

    out_rate = player->audio_spec.freq;
    out_channels = player->audio_spec.channels;
    out_format = AV_SAMPLE_FMT_S16;
    av_channel_layout_default(&out_layout, out_channels);

    if (!*swr_context) {
        swr_result = swr_alloc_set_opts2(swr_context,
                                         &out_layout,
                                         out_format,
                                         out_rate,
                                         &audio_codec_context->ch_layout,
                                         audio_codec_context->sample_fmt,
                                         audio_codec_context->sample_rate,
                                         0,
                                         NULL);
        if (swr_result < 0 || !*swr_context || swr_init(*swr_context) < 0) {
            av_channel_layout_uninit(&out_layout);
            player_set_error(player, "Unable to initialize audio resampler");
            return -1;
        }
    }

    max_samples = (int) av_rescale_rnd(swr_get_delay(*swr_context, audio_codec_context->sample_rate) + audio_frame->nb_samples,
                                       out_rate,
                                       audio_codec_context->sample_rate,
                                       AV_ROUND_UP);

    if (av_samples_alloc_array_and_samples(&converted_data,
                                           &line_size,
                                           out_channels,
                                           max_samples,
                                           out_format,
                                           0) < 0) {
        av_channel_layout_uninit(&out_layout);
        player_set_error(player, "Unable to allocate audio buffer");
        return -1;
    }

    sample_count = swr_convert(*swr_context,
                               converted_data,
                               max_samples,
                               (const uint8_t * const *) audio_frame->extended_data,
                               audio_frame->nb_samples);
    if (sample_count < 0) {
        av_freep(&converted_data[0]);
        av_freep(&converted_data);
        av_channel_layout_uninit(&out_layout);
        player_set_error(player, "Audio resample failed");
        return -1;
    }

    buffer_size = av_samples_get_buffer_size(&line_size, out_channels, sample_count, out_format, 1);
    if (buffer_size <= 0) {
        av_freep(&converted_data[0]);
        av_freep(&converted_data);
        av_channel_layout_uninit(&out_layout);
        return 0;
    }

    audio_pts = audio_frame->best_effort_timestamp == AV_NOPTS_VALUE
        ? NAN
        : pts_base_offset + (audio_frame->best_effort_timestamp * av_q2d(audio_time_base));
    frame_duration = (double) sample_count / (double) out_rate;

    queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18);
    while (!should_stop(player) && (int) SDL_GetQueuedAudioSize(player->audio_device) > queued_limit) {
        SDL_Delay(2);
    }

    result = SDL_QueueAudio(player->audio_device, converted_data[0], (Uint32) buffer_size);
    av_freep(&converted_data[0]);
    av_freep(&converted_data);

    if (result != 0) {
        av_channel_layout_uninit(&out_layout);
        player_set_error(player, SDL_GetError());
        return -1;
    }

    SDL_LockMutex(player->clock_mutex);
    if (!isnan(audio_pts)) {
        player->latest_audio_end_pts = audio_pts + frame_duration;
    } else {
        player->latest_audio_end_pts += frame_duration;
    }
    SDL_UnlockMutex(player->clock_mutex);

    if (queued_any_audio) {
        *queued_any_audio = 1;
    }

    av_channel_layout_uninit(&out_layout);
    return 0;
}

typedef struct DecodeContext {
    AVFormatContext *format_context;
    AVCodecContext *video_codec_context;
    AVCodecContext *audio_codec_context;
    struct SwsContext *sws_context;
    SwrContext *swr_context;
    AVPacket *packet;
    AVFrame *decoded_frame;
    AVFrame *audio_frame;
    AVRational video_time_base;
    AVRational audio_time_base;
    int video_stream_index;
    int audio_stream_index;
} DecodeContext;

static void decode_context_reset(DecodeContext *ctx) {
    if (!ctx) {
        return;
    }
    if (ctx->packet) av_packet_free(&ctx->packet);
    if (ctx->decoded_frame) av_frame_free(&ctx->decoded_frame);
    if (ctx->audio_frame) av_frame_free(&ctx->audio_frame);
    if (ctx->video_codec_context) avcodec_free_context(&ctx->video_codec_context);
    if (ctx->audio_codec_context) avcodec_free_context(&ctx->audio_codec_context);
    if (ctx->format_context) avformat_close_input(&ctx->format_context);
    if (ctx->sws_context) sws_freeContext(ctx->sws_context);
    if (ctx->swr_context) swr_free(&ctx->swr_context);
    memset(ctx, 0, sizeof(*ctx));
    ctx->video_stream_index = -1;
    ctx->audio_stream_index = -1;
}

static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program, Player *player) {
    const AVCodec *video_codec = NULL;
    const AVCodec *audio_codec = NULL;

    decode_context_reset(ctx);
    ctx->video_stream_index = -1;
    ctx->audio_stream_index = -1;

    if (avformat_open_input(&ctx->format_context, program->file_path, NULL, NULL) < 0) {
        player_set_error(player, "Unable to open media file");
        return -1;
    }
    if (avformat_find_stream_info(ctx->format_context, NULL) < 0) {
        player_set_error(player, "Unable to read stream metadata");
        return -1;
    }
    for (unsigned int i = 0; i < ctx->format_context->nb_streams; ++i) {
        if (ctx->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            ctx->video_stream_index = (int) i;
        } else if (ctx->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && ctx->audio_stream_index < 0) {
            ctx->audio_stream_index = (int) i;
        }
    }
    if (ctx->video_stream_index < 0) {
        player_set_error(player, "No video stream found");
        return -1;
    }

    video_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->video_stream_index]->codecpar->codec_id);
    if (!video_codec) {
        player_set_error(player, "Unsupported video codec");
        return -1;
    }
    ctx->video_codec_context = avcodec_alloc_context3(video_codec);
    if (!ctx->video_codec_context ||
        avcodec_parameters_to_context(ctx->video_codec_context, ctx->format_context->streams[ctx->video_stream_index]->codecpar) < 0 ||
        avcodec_open2(ctx->video_codec_context, video_codec, NULL) < 0) {
        player_set_error(player, "Unable to initialize decoder");
        return -1;
    }

    if (ctx->audio_stream_index >= 0) {
        audio_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->audio_stream_index]->codecpar->codec_id);
        if (audio_codec) {
            ctx->audio_codec_context = avcodec_alloc_context3(audio_codec);
            if (!ctx->audio_codec_context ||
                avcodec_parameters_to_context(ctx->audio_codec_context, ctx->format_context->streams[ctx->audio_stream_index]->codecpar) < 0 ||
                avcodec_open2(ctx->audio_codec_context, audio_codec, NULL) < 0) {
                player_set_error(player, "Unable to initialize audio decoder");
                return -1;
            }
            ctx->audio_time_base = ctx->format_context->streams[ctx->audio_stream_index]->time_base;
        }
    }

    ctx->packet = av_packet_alloc();
    ctx->decoded_frame = av_frame_alloc();
    ctx->audio_frame = av_frame_alloc();
    if (!ctx->packet || !ctx->decoded_frame || !ctx->audio_frame) {
        player_set_error(player, "Unable to allocate FFmpeg frame buffers");
        return -1;
    }

    ctx->video_time_base = ctx->format_context->streams[ctx->video_stream_index]->time_base;
    ctx->sws_context = sws_getContext(ctx->video_codec_context->width,
                                      ctx->video_codec_context->height,
                                      ctx->video_codec_context->pix_fmt,
                                      ctx->video_codec_context->width,
                                      ctx->video_codec_context->height,
                                      AV_PIX_FMT_YUV420P,
                                      SWS_BILINEAR,
                                      NULL,
                                      NULL,
                                      NULL);
    if (!ctx->sws_context) {
        player_set_error(player, "Unable to initialize scaler");
        return -1;
    }

    player->has_audio_stream = ctx->audio_codec_context != NULL;
    return 0;
}

static int decode_context_seek_to_seconds(DecodeContext *ctx, double seek_seconds) {
    int64_t seek_target;

    if (!ctx || seek_seconds <= 0.0 || !ctx->format_context || ctx->video_stream_index < 0) {
        return 0;
    }

    seek_target = (int64_t) (seek_seconds / av_q2d(ctx->video_time_base));
    if (avformat_seek_file(ctx->format_context, ctx->video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0) < 0) {
        return -1;
    }

    avcodec_flush_buffers(ctx->video_codec_context);
    if (ctx->audio_codec_context) {
        avcodec_flush_buffers(ctx->audio_codec_context);
    }

    return 0;
}

static int decode_context_sync_to_schedule(Player *player,
                                           const Channel *channel,
                                           DecodeContext *ctx,
                                           const ProgramEntry **program,
                                           int *current_program_index,
                                           double *seek_seconds,
                                           int force_reopen,
                                           int *queued_any_audio,
                                           int *queued_first_video) {
    const ProgramEntry *scheduled_program;
    int scheduled_program_index = -1;
    double scheduled_seek_seconds = 0.0;

    if (!player || !channel || !ctx || !program || !current_program_index || !seek_seconds) {
        return -1;
    }

    scheduled_program = channel_resolve_program(channel,
                                                player->app_start_ticks,
                                                SDL_GetTicks64(),
                                                &scheduled_seek_seconds,
                                                &scheduled_program_index);
    if (!scheduled_program) {
        return -1;
    }

    if (!force_reopen && *program == scheduled_program && *current_program_index == scheduled_program_index) {
        *seek_seconds = scheduled_seek_seconds;
        return 0;
    }

    if (decode_context_open(ctx, scheduled_program, player) != 0) {
        return -1;
    }
    if (decode_context_seek_to_seconds(ctx, scheduled_seek_seconds) != 0) {
        player_set_error(player, "Unable to seek to scheduled program position");
        return -1;
    }

    frame_queue_clear(&player->frame_queue);
    player_clear_audio(player);
    *program = scheduled_program;
    *current_program_index = scheduled_program_index;
    *seek_seconds = scheduled_seek_seconds;
    if (queued_any_audio) {
        *queued_any_audio = 0;
    }
    if (queued_first_video) {
        *queued_first_video = 0;
    }

    return 0;
}

static int decode_thread_main(void *userdata) {
    DecoderThreadArgs *args = (DecoderThreadArgs *) userdata;
    Player *player = args->player;
    const Channel *channel = args->channel;
    DecodeContext ctx;
    const ProgramEntry *program;
    int current_program_index = 0;
    double seek_seconds = 0.0;
    int queued_any_audio = 0;
    int queued_first_video = 0;
    int rc = -1;

    #define MAYBE_MARK_PREROLL_READY() do { if (queued_first_video && (queued_any_audio || !player->has_audio_stream)) { player_mark_preroll_ready(player); } } while (0)

    memset(&ctx, 0, sizeof(ctx));
    ctx.video_stream_index = -1;
    ctx.audio_stream_index = -1;
    free(args);

    if (decode_context_sync_to_schedule(player,
                                        channel,
                                        &ctx,
                                        &program,
                                        &current_program_index,
                                        &seek_seconds,
                                        1,
                                        &queued_any_audio,
                                        &queued_first_video) != 0) {
        decode_context_reset(&ctx);
        return -1;
    }

    while (!should_stop(player)) {
        if (decode_context_sync_to_schedule(player,
                                            channel,
                                            &ctx,
                                            &program,
                                            &current_program_index,
                                            &seek_seconds,
                                            0,
                                            &queued_any_audio,
                                            &queued_first_video) != 0) {
            goto cleanup;
        }

        if (av_read_frame(ctx.format_context, ctx.packet) < 0) {
            if (decode_context_sync_to_schedule(player,
                                                channel,
                                                &ctx,
                                                &program,
                                                &current_program_index,
                                                &seek_seconds,
                                                1,
                                                &queued_any_audio,
                                                &queued_first_video) != 0) {
                goto cleanup;
            }
            continue;
        }

        if (ctx.packet->stream_index == ctx.audio_stream_index && ctx.audio_codec_context) {
            if (avcodec_send_packet(ctx.audio_codec_context, ctx.packet) >= 0) {
                while (!should_stop(player)) {
                    int receive_audio = avcodec_receive_frame(ctx.audio_codec_context, ctx.audio_frame);
                    if (receive_audio == AVERROR(EAGAIN) || receive_audio == AVERROR_EOF) {
                        break;
                    }
                    if (receive_audio < 0) {
                        player_set_error(player, "Audio decode failed");
                        av_packet_unref(ctx.packet);
                        goto cleanup;
                    }
                    if (queue_audio_frame(player,
                                          &ctx.swr_context,
                                          ctx.audio_codec_context,
                                          ctx.audio_time_base,
                                          ctx.audio_frame,
                                          program->start_offset_seconds,
                                          &queued_any_audio) != 0) {
                        av_packet_unref(ctx.packet);
                        goto cleanup;
                    }
                    MAYBE_MARK_PREROLL_READY();
                    av_frame_unref(ctx.audio_frame);
                }
            }
            av_packet_unref(ctx.packet);
            continue;
        }

        if (ctx.packet->stream_index != ctx.video_stream_index) {
            av_packet_unref(ctx.packet);
            continue;
        }
        if (avcodec_send_packet(ctx.video_codec_context, ctx.packet) < 0) {
            av_packet_unref(ctx.packet);
            continue;
        }
        av_packet_unref(ctx.packet);

        while (!should_stop(player)) {
            double frame_seconds;
            int receive = avcodec_receive_frame(ctx.video_codec_context, ctx.decoded_frame);
            if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) {
                break;
            }
            if (receive < 0) {
                player_set_error(player, "Video decode failed");
                goto cleanup;
            }

            frame_seconds = ctx.decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE
                ? program->start_offset_seconds + seek_seconds
                : program->start_offset_seconds + (ctx.decoded_frame->best_effort_timestamp * av_q2d(ctx.video_time_base));

            {
                FrameData frame = {0};
                uint8_t *dest_data[4] = {0};
                int dest_linesize[4] = {0};
                int image_size;

                frame.width = ctx.video_codec_context->width;
                frame.height = ctx.video_codec_context->height;
                frame.pts_seconds = frame_seconds;
                image_size = av_image_alloc(dest_data, dest_linesize, frame.width, frame.height, AV_PIX_FMT_YUV420P, 1);
                if (image_size < 0) {
                    player_set_error(player, "Unable to allocate frame buffer");
                    goto cleanup;
                }
                frame.buffer = dest_data[0];
                frame.plane_y = dest_data[0];
                frame.plane_u = dest_data[1];
                frame.plane_v = dest_data[2];
                frame.pitch_y = dest_linesize[0];
                frame.pitch_u = dest_linesize[1];
                frame.pitch_v = dest_linesize[2];
                sws_scale(ctx.sws_context,
                          (const uint8_t *const *) ctx.decoded_frame->data,
                          ctx.decoded_frame->linesize,
                          0,
                          ctx.video_codec_context->height,
                          dest_data,
                          dest_linesize);
                frame_queue_push(&player->frame_queue, &frame);
                queued_first_video = 1;
                MAYBE_MARK_PREROLL_READY();
            }
        }
    }

    rc = 0;

cleanup:
    decode_context_reset(&ctx);
    player->has_audio_stream = 0;
    return rc;

    #undef MAYBE_MARK_PREROLL_READY
}

static void player_stop_thread(Player *player) {
    if (!player || !player->thread) {
        return;
    }

    SDL_AtomicSet(&player->stop_requested, 1);
    SDL_WaitThread(player->thread, NULL);
    player->thread = NULL;
}

int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks) {
    if (!player || !channels) {
        return -1;
    }

    memset(player, 0, sizeof(*player));
    player->channels = channels;
    player->current_index = -1;
    player->app_start_ticks = app_start_ticks;
    player->audio_mutex = SDL_CreateMutex();
    player->clock_mutex = SDL_CreateMutex();
    player->error_mutex = SDL_CreateMutex();
    if (!player->audio_mutex || !player->clock_mutex || !player->error_mutex) {
        if (player->audio_mutex) {
            SDL_DestroyMutex(player->audio_mutex);
        }
        if (player->clock_mutex) {
            SDL_DestroyMutex(player->clock_mutex);
        }
        if (player->error_mutex) {
            SDL_DestroyMutex(player->error_mutex);
        }
        return -1;
    }

    if (frame_queue_init(&player->frame_queue) != 0) {
        SDL_DestroyMutex(player->audio_mutex);
        SDL_DestroyMutex(player->clock_mutex);
        SDL_DestroyMutex(player->error_mutex);
        memset(player, 0, sizeof(*player));
        return -1;
    }

    return 0;
}

void player_destroy(Player *player) {
    if (!player) {
        return;
    }

    player_stop_thread(player);
    player_close_audio(player);
    frame_queue_destroy(&player->frame_queue);
    if (player->audio_mutex) {
        SDL_DestroyMutex(player->audio_mutex);
    }
    if (player->clock_mutex) {
        SDL_DestroyMutex(player->clock_mutex);
    }
    if (player->error_mutex) {
        SDL_DestroyMutex(player->error_mutex);
    }
    memset(player, 0, sizeof(*player));
}

int player_tune(Player *player, int channel_index) {
    DecoderThreadArgs *args;

    if (!player || !player->channels || channel_index < 0 || channel_index >= player->channels->count) {
        return -1;
    }

    player_stop_thread(player);
    frame_queue_clear(&player->frame_queue);
    player_clear_audio(player);
    player->has_audio_stream = 0;
    SDL_AtomicSet(&player->stop_requested, 0);
    player->current_index = channel_index;
    player->tuning_blackout_until = SDL_GetTicks() + 200;
    player->catchup_until = player->tuning_blackout_until + 350;
    player_set_error(player, "");

    args = malloc(sizeof(*args));
    if (!args) {
        player_set_error(player, "Unable to allocate thread args");
        return -1;
    }

    args->player = player;
    args->channel = &player->channels->items[channel_index];
    player->thread = SDL_CreateThread(decode_thread_main, "decoder", args);
    if (!player->thread) {
        free(args);
        player_set_error(player, "Unable to start decoder thread");
        return -1;
    }

    return 0;
}

int player_consume_latest_frame(Player *player, FrameData *out) {
    if (!player || !out) {
        return 0;
    }

    return frame_queue_pop_latest(&player->frame_queue, out);
}

int player_consume_synced_frame(Player *player, FrameData *out, double clock_seconds, double lead_tolerance) {
    FrameData candidate = {0};
    FrameData selected = {0};
    int found = 0;
    double late_tolerance;

    if (!player || !out) {
        return 0;
    }

    late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.220 : 0.130;

    while (frame_queue_peek_first(&player->frame_queue, &candidate)) {
        if (candidate.pts_seconds < clock_seconds - late_tolerance) {
            if (!frame_queue_pop_first(&player->frame_queue, &candidate)) {
                break;
            }
            frame_data_free(&candidate);
            memset(&candidate, 0, sizeof(candidate));
            continue;
        }

        if (candidate.pts_seconds > clock_seconds + lead_tolerance) {
            break;
        }

        if (!frame_queue_pop_first(&player->frame_queue, &candidate)) {
            break;
        }

        if (found) {
            frame_data_free(&selected);
        }
        selected = candidate;
        memset(&candidate, 0, sizeof(candidate));
        found = 1;
    }

    if (found) {
        *out = selected;
        return 1;
    }

    return 0;
}

double player_live_position(const Player *player, Uint64 now_ticks) {
    if (!player || !player->channels || player->current_index < 0 || player->current_index >= player->channels->count) {
        return 0.0;
    }

    return channel_live_position_precise(&player->channels->items[player->current_index], player->app_start_ticks, now_ticks);
}

double player_get_sync_clock(Player *player, Uint64 now_ticks) {
    double clock_seconds;
    double queued_seconds = 0.0;
    double bytes_per_second;
    int audio_started;

    if (!player) {
        return 0.0;
    }

    if (!player->has_audio_stream || player->audio_device == 0) {
        return player_live_position(player, now_ticks);
    }

    bytes_per_second = player_audio_bytes_per_second(player);
    if (bytes_per_second <= 0.0) {
        return player_live_position(player, now_ticks);
    }

    SDL_LockMutex(player->clock_mutex);
    clock_seconds = player->latest_audio_end_pts;
    audio_started = player->audio_started;
    SDL_UnlockMutex(player->clock_mutex);

    if (!audio_started) {
        return player_live_position(player, now_ticks);
    }

    queued_seconds = (double) SDL_GetQueuedAudioSize(player->audio_device) / bytes_per_second;
    clock_seconds -= queued_seconds;
    if (clock_seconds < 0.0) {
        clock_seconds = 0.0;
    }

    return clock_seconds;
}

void player_set_catchup_until(Player *player, Uint32 tick) {
    if (!player) {
        return;
    }

    player->catchup_until = tick;
}

int player_is_in_blackout(const Player *player) {
    int in_blackout;
    int ready;

    if (!player) {
        return 0;
    }

    SDL_LockMutex(player->clock_mutex);
    ready = player->preroll_ready;
    SDL_UnlockMutex(player->clock_mutex);

    in_blackout = SDL_GetTicks() < player->tuning_blackout_until || !ready;
    return in_blackout;
}

void player_resume_audio(Player *player) {
    if (!player || !player->audio_mutex || !player->has_audio_stream) {
        return;
    }

    SDL_LockMutex(player->audio_mutex);
    if (player->audio_device != 0 && SDL_GetQueuedAudioSize(player->audio_device) > 0) {
        SDL_PauseAudioDevice(player->audio_device, 0);
        SDL_LockMutex(player->clock_mutex);
        player->audio_started = 1;
        SDL_UnlockMutex(player->clock_mutex);
    }
    SDL_UnlockMutex(player->audio_mutex);
}

void player_get_error(Player *player, char *buffer, size_t buffer_size) {
    if (!player || !buffer || buffer_size == 0) {
        return;
    }

    SDL_LockMutex(player->error_mutex);
    snprintf(buffer, buffer_size, "%s", player->last_error);
    SDL_UnlockMutex(player->error_mutex);
}
