const std = @import("std");
const c = ffi.c;

const ffi = @import("../ffi.zig");
const util = @import("../util.zig");

const ChapterSet = std.AutoHashMap(isize, void);

skipped_chapters: ChapterSet,

const SBSkip = @This();

const blacklist = std.ComptimeStringMap(void, .{
    .{ "Endcards/Credits", {} },
    .{ "Interaction Reminder", {} },
    .{ "Intermission/Intro Animation", {} },
    .{ "Intro", {} },
    .{ "Outro", {} },
    .{ "Sponsor", {} },
    .{ "Unpaid/Self Promotion", {} },
});

pub fn onEvent(self: *SBSkip, mpv: *c.mpv_handle, ev: *c.mpv_event) !void {
    switch (ev.event_id) {
        c.MPV_EVENT_PROPERTY_CHANGE => {
            const evprop: *c.mpv_event_property = @ptrCast(@alignCast(ev.data));
            if (std.mem.eql(u8, std.mem.span(evprop.name), "chapter")) {
                const chapter_id_ptr = @as(?*i64, @ptrCast(@alignCast(evprop.data)));
                if (chapter_id_ptr) |chptr|
                    try self.onChapterChange(mpv, @intCast(chptr.*));
            }
        },
        c.MPV_EVENT_FILE_LOADED => self.skipped_chapters.clearRetainingCapacity(),
        else => {},
    }
}

fn onChapterChange(
    self: *SBSkip,
    mpv: *c.mpv_handle,
    chapter_id: isize,
) !void {
    if (chapter_id < 0 or self.skipped_chapters.contains(chapter_id))
        return;

    // fuck these ubiquitous duck typing implementations everywhere! we have structs, for fuck's sake!
    var chapter_list_node: c.mpv_node = undefined;
    try ffi.checkMpvError(c.mpv_get_property(
        mpv,
        "chapter-list",
        c.MPV_FORMAT_NODE,
        &chapter_list_node,
    ));
    defer c.mpv_free_node_contents(&chapter_list_node);
    std.debug.assert(chapter_list_node.format == c.MPV_FORMAT_NODE_ARRAY);

    const chapter_nodes = chapter_list_node.u.list.*.values[0..@intCast(chapter_list_node.u.list.*.num)];

    std.debug.assert(chapter_nodes[@intCast(chapter_id)].format == c.MPV_FORMAT_NODE_MAP);
    const chapter = Chapter.fromNodeMap(chapter_nodes[@intCast(chapter_id)].u.list.*);

    if (chapter.skipReason()) |reason| {
        const end_time = if (chapter_id != chapter_nodes.len - 1) end_time: {
            std.debug.assert(chapter_nodes[@as(usize, @intCast(chapter_id)) + 1].format ==
                c.MPV_FORMAT_NODE_MAP);
            const next_chapter = Chapter.fromNodeMap(
                chapter_nodes[@as(usize, @intCast(chapter_id)) + 1].u.list.*,
            );
            break :end_time next_chapter.time;
        } else end_time: {
            var end_time: f64 = 0.0;
            try ffi.checkMpvError(c.mpv_get_property(
                mpv,
                "duration",
                c.MPV_FORMAT_DOUBLE,
                &end_time,
            ));
            break :end_time end_time;
        };
        try ffi.checkMpvError(c.mpv_set_property(
            mpv,
            "time-pos",
            c.MPV_FORMAT_DOUBLE,
            @constCast(&end_time),
        ));
        try self.skipped_chapters.put(chapter_id, {});
        try util.msg(mpv, "skipped: {s}", .{reason});
    }
}

const Chapter = struct {
    title: [:0]const u8,
    time: f64,

    fn fromNodeMap(m: c.mpv_node_list) Chapter {
        var self = Chapter{ .title = "", .time = 0 };

        for (m.keys[0..@intCast(m.num)], m.values[0..@intCast(m.num)]) |k_c, v| {
            const k = std.mem.span(k_c);
            if (std.mem.eql(u8, k, "title")) {
                std.debug.assert(v.format == c.MPV_FORMAT_STRING);
                self.title = std.mem.span(v.u.string);
            } else if (std.mem.eql(u8, k, "time")) {
                std.debug.assert(v.format == c.MPV_FORMAT_DOUBLE);
                self.time = v.u.double_;
            }
        }

        return self;
    }

    /// Returns the reason for the chapter being skipped or null if the chapter should not be skipped.
    fn skipReason(self: Chapter) ?[]const u8 {
        const prefix = "[SponsorBlock]: ";
        if (self.title.len <= prefix.len or !std.mem.startsWith(u8, self.title, prefix))
            return null;

        const types = self.title[prefix.len..];
        var type_iter = std.mem.tokenize(u8, types, ",");
        while (type_iter.next()) |type_split| {
            const typestr = std.mem.trim(u8, type_split, &std.ascii.whitespace);
            if (blacklist.has(typestr))
                return typestr;
        }

        return null;
    }
};

pub fn setup(self: *SBSkip, mpv: *c.mpv_handle) !void {
    _ = self;
    try ffi.checkMpvError(c.mpv_observe_property(mpv, 0, "chapter", c.MPV_FORMAT_INT64));
}

pub fn create() SBSkip {
    return .{
        .skipped_chapters = ChapterSet.init(std.heap.c_allocator),
    };
}

pub fn deinit(self: *SBSkip) void {
    self.skipped_chapters.deinit();
}
