const std = @import("std");
const c = @import("c.zig");
const gl = @import("gl.zig");

fn check(err: c_int) !void {
    if (err >= 0) return;
    var buf: [c.AV_ERROR_MAX_STRING_SIZE]u8 = undefined;
    _ = c.av_make_error_string(&buf, buf.len, err);
    std.debug.print("libav error: {s}\n", .{buf});
    return error.AVError;
}

pub fn loadVideo(progress_root: std.Progress.Node, filename: [*:0]const u8, texture: *gl.Texture) !void {
    const outer_progress = progress_root.start("load video", 0);
    defer outer_progress.end();

    var format_ctx: ?*c.AVFormatContext = null;
    try check(c.avformat_open_input(&format_ctx, filename, null, null));
    defer c.avformat_close_input(&format_ctx);

    if (format_ctx) |format| {
        try check(c.avformat_find_stream_info(format, null));

        const video_stream = for (format.streams, 0..format.nb_streams) |c_stream, _| {
            const stream = @as(*c.AVStream, c_stream orelse unreachable);
            if (stream.codecpar.*.codec_type == c.AVMEDIA_TYPE_VIDEO) break stream;
        } else unreachable;

        const codec_par = video_stream.codecpar.*;
        const codec = c.avcodec_find_decoder(codec_par.codec_id) orelse unreachable;
        var codec_ctx = c.avcodec_alloc_context3(codec) orelse unreachable;
        defer c.avcodec_free_context(&codec_ctx);

        try check(c.avcodec_parameters_to_context(codec_ctx, &codec_par));
        try check(c.avcodec_open2(codec_ctx, codec, null));
        defer check(c.avcodec_close(codec_ctx)) catch unreachable;

        std.debug.print("opened codec, size {}x{}, ~{} frames\n", .{ codec_par.width, codec_par.height, video_stream.nb_frames });

        const sws_ctx = c.sws_getContext(
            codec_par.width,
            codec_par.height,
            codec_par.format,
            codec_par.width,
            codec_par.height,
            c.AV_PIX_FMT_RGBA,
            c.SWS_POINT,
            null,
            null,
            0,
        ) orelse unreachable;
        defer c.sws_freeContext(sws_ctx);

        var packet = c.av_packet_alloc();
        defer c.av_packet_free(&packet);

        var raw_frame = c.av_frame_alloc();
        defer c.av_frame_free(&raw_frame);
        var rgba_frame = c.av_frame_alloc();
        defer c.av_frame_free(&rgba_frame);

        const frame_count = blk: {
            const progress = outer_progress.start("scanning video frames", @intCast(video_stream.nb_frames));
            defer progress.end();

            var i: usize = 0;
            var res = c.av_read_frame(format, packet);
            while (res >= 0) : (res = c.av_read_frame(format, packet)) {
                if (packet.*.stream_index == video_stream.index) {
                    try check(c.avcodec_send_packet(codec_ctx, packet));

                    const ires = c.avcodec_receive_frame(codec_ctx, raw_frame);
                    if (ires == c.AVERROR_EOF) break;
                    if (ires == c.AVERROR(c.EAGAIN)) continue;
                    try check(ires);

                    i += 1;
                    progress.completeOne();
                }
                c.av_packet_unref(packet);
            }

            try check(c.avformat_seek_file(format, video_stream.index, 0, 0, 0, 0));
            c.avcodec_flush_buffers(codec_ctx);

            break :blk i;
        };

        std.debug.print("counted frames, found {}\n", .{frame_count});
        switch (texture.type) {
            .TEXTURE_2D => {},
            .TEXTURE_3D, .TEXTURE_2D_ARRAY => texture.allocate3D(codec_par.width, codec_par.height, @intCast(frame_count)),
            else => unreachable,
        }

        {
            const progress = outer_progress.start("loading video frames", frame_count);
            defer progress.end();

            defer c.glPixelStorei(c.GL_UNPACK_ROW_LENGTH, 0);

            var i: i32 = 0;
            var res = c.av_read_frame(format, packet);
            while (res >= 0) : (res = c.av_read_frame(format, packet)) {
                if (packet.*.stream_index == video_stream.index) {
                    try check(c.avcodec_send_packet(codec_ctx, packet));

                    const ires = c.avcodec_receive_frame(codec_ctx, raw_frame);
                    if (ires == c.AVERROR_EOF) break;
                    if (ires == c.AVERROR(c.EAGAIN)) continue;
                    try check(ires);
                    try check(c.sws_scale_frame(sws_ctx, rgba_frame, raw_frame));

                    c.glPixelStorei(c.GL_UNPACK_ROW_LENGTH, @divFloor(rgba_frame.*.linesize[0], @sizeOf(u8) * 4));

                    switch (texture.type) {
                        .TEXTURE_2D => {
                            texture.setData2D(codec_par.width, codec_par.height, rgba_frame.*.data[0]);
                            break;
                        },

                        .TEXTURE_3D,
                        .TEXTURE_2D_ARRAY,
                        => texture.setLayer3D(codec_par.width, codec_par.height, i, rgba_frame.*.data[0]),

                        else => unreachable,
                    }

                    i += 1;
                    progress.completeOne();
                }
                c.av_packet_unref(packet);
            } else {
                if (res != c.AVERROR_EOF) try check(res);
            }
        }
    } else unreachable;
}
