const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const cimgui = @import("dcimgui");
const terminal = @import("../../terminal/main.zig");
const CircBuf = @import("../../datastruct/main.zig").CircBuf;
const Surface = @import("../../Surface.zig");
const screen = @import("screen.zig");

const log = std.log.scoped(.inspector_terminal_io);

/// VT event stream inspector widget.
pub const Stream = struct {
    events: VTEvent.Ring,
    parser_stream: VTHandler.Stream,

    /// The currently selected event sequence number for keyboard navigation
    selected_event_seq: ?u32 = null,

    /// Flag indicating whether we need to scroll to the selected item
    need_scroll_to_selected: bool = false,

    /// Flag indicating whether the selection was made by keyboard
    is_keyboard_selection: bool = false,

    pub fn init(alloc: Allocator) !Stream {
        var events: VTEvent.Ring = try .init(alloc, 2);
        errdefer events.deinit(alloc);

        var handler: VTHandler = .init;
        errdefer handler.deinit();

        return .{
            .events = events,
            .parser_stream = .init(.{
                .allocator = alloc,
                .handler = handler,
            }),
        };
    }

    pub fn deinit(self: *Stream, alloc: Allocator) void {
        var it = self.events.iterator(.forward);
        while (it.next()) |v| v.deinit(alloc);
        self.events.deinit(alloc);

        self.parser_stream.deinit();
    }

    pub fn recordPtyRead(
        self: *Stream,
        alloc: Allocator,
        t: *terminal.Terminal,
        data: []const u8,
    ) !void {
        self.parser_stream.handler.state = .{
            .alloc = alloc,
            .terminal = t,
            .events = &self.events,
        };
        defer self.parser_stream.handler.state = null;
        self.parser_stream.nextSlice(data);
    }

    pub fn draw(
        self: *Stream,
        surface: *Surface,
        palette: *const terminal.color.Palette,
    ) void {
        const alloc = surface.alloc;
        const events = &self.events;
        const handler = &self.parser_stream.handler;
        const popup_filter = "Filter";

        // Controls
        {
            const pause_play: [:0]const u8 = if (!handler.paused)
                "Pause##pause_play"
            else
                "Resume##pause_play";
            if (cimgui.c.ImGui_Button(pause_play.ptr)) {
                handler.paused = !handler.paused;
            }

            cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
            if (cimgui.c.ImGui_Button("Filter")) {
                cimgui.c.ImGui_OpenPopup(
                    popup_filter,
                    cimgui.c.ImGuiPopupFlags_None,
                );
            }

            if (!events.empty()) {
                cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
                if (cimgui.c.ImGui_Button("Clear")) {
                    var it = events.iterator(.forward);
                    while (it.next()) |v| v.deinit(alloc);
                    events.clear();

                    handler.current_seq = 1;
                }

                cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
                if (cimgui.c.ImGui_Button("Copy")) {
                    self.copyEvents(surface);
                }

                cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
                if (cimgui.c.ImGui_Button("Export to file")) {
                    self.exportEvents(surface);
                }
            }
        }

        // Events Table
        if (events.empty()) {
            cimgui.c.ImGui_Text("Waiting for events...");
        } else {
            // TODO: Eventually
            // eventTable(events);
        }

        {
            cimgui.c.ImGui_Separator();

            _ = cimgui.c.ImGui_BeginTable(
                "table_vt_events",
                3,
                cimgui.c.ImGuiTableFlags_RowBg |
                    cimgui.c.ImGuiTableFlags_Borders,
            );
            defer cimgui.c.ImGui_EndTable();

            cimgui.c.ImGui_TableSetupColumn(
                "Seq",
                cimgui.c.ImGuiTableColumnFlags_WidthFixed,
            );
            cimgui.c.ImGui_TableSetupColumn(
                "Kind",
                cimgui.c.ImGuiTableColumnFlags_WidthFixed,
            );
            cimgui.c.ImGui_TableSetupColumn(
                "Description",
                cimgui.c.ImGuiTableColumnFlags_WidthStretch,
            );

            // Handle keyboard navigation when window is focused
            if (cimgui.c.ImGui_IsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) {
                const key_pressed = getKeyAction();

                switch (key_pressed) {
                    .none => {},
                    .up, .down => {
                        // If no event is selected, select the first/last event based on direction
                        if (self.selected_event_seq == null) {
                            if (!events.empty()) {
                                var it = events.iterator(if (key_pressed == .up) .forward else .reverse);
                                if (it.next()) |ev| {
                                    self.selected_event_seq = @as(u32, @intCast(ev.seq));
                                }
                            }
                        } else {
                            // Find next/previous event based on current selection
                            var it = events.iterator(.reverse);
                            switch (key_pressed) {
                                .down => {
                                    var found = false;
                                    while (it.next()) |ev| {
                                        if (found) {
                                            self.selected_event_seq = @as(u32, @intCast(ev.seq));
                                            break;
                                        }
                                        if (ev.seq == self.selected_event_seq.?) {
                                            found = true;
                                        }
                                    }
                                },
                                .up => {
                                    var prev_ev: ?*const VTEvent = null;
                                    while (it.next()) |ev| {
                                        if (ev.seq == self.selected_event_seq.?) {
                                            if (prev_ev) |prev| {
                                                self.selected_event_seq = @as(u32, @intCast(prev.seq));
                                                break;
                                            }
                                        }
                                        prev_ev = ev;
                                    }
                                },
                                .none => unreachable,
                            }
                        }

                        // Mark that we need to scroll to the newly selected item
                        self.need_scroll_to_selected = true;
                        self.is_keyboard_selection = true;
                    },
                }
            }

            var it = events.iterator(.reverse);
            while (it.next()) |ev| {
                // Need to push an ID so that our selectable is unique.
                cimgui.c.ImGui_PushIDPtr(ev);
                defer cimgui.c.ImGui_PopID();

                cimgui.c.ImGui_TableNextRow();
                _ = cimgui.c.ImGui_TableNextColumn();

                // Store the previous selection state to detect changes
                const was_selected = ev.imgui_selected;

                // Update selection state based on keyboard navigation
                if (self.selected_event_seq) |seq| {
                    ev.imgui_selected = (@as(u32, @intCast(ev.seq)) == seq);
                }

                // Handle selectable widget
                if (cimgui.c.ImGui_SelectableBoolPtr(
                    "##select",
                    &ev.imgui_selected,
                    cimgui.c.ImGuiSelectableFlags_SpanAllColumns,
                )) {
                    // If selection state changed, update keyboard navigation state
                    if (ev.imgui_selected != was_selected) {
                        self.selected_event_seq = if (ev.imgui_selected)
                            @as(u32, @intCast(ev.seq))
                        else
                            null;
                        self.is_keyboard_selection = false;
                    }
                }

                cimgui.c.ImGui_SameLine();
                cimgui.c.ImGui_Text("%d", ev.seq);
                _ = cimgui.c.ImGui_TableNextColumn();
                cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr);
                _ = cimgui.c.ImGui_TableNextColumn();
                cimgui.c.ImGui_Text("%s", ev.raw_description.ptr);

                // If the event is selected, we render info about it. For now
                // we put this in the last column because that's the widest and
                // imgui has no way to make a column span.
                if (ev.imgui_selected) {
                    {
                        screen.cursorTable(&ev.cursor);
                        screen.cursorStyle(&ev.cursor, palette);

                        _ = cimgui.c.ImGui_BeginTable(
                            "details",
                            2,
                            cimgui.c.ImGuiTableFlags_None,
                        );
                        defer cimgui.c.ImGui_EndTable();
                        {
                            cimgui.c.ImGui_TableNextRow();
                            {
                                _ = cimgui.c.ImGui_TableSetColumnIndex(0);
                                cimgui.c.ImGui_Text("Scroll Region");
                            }
                            {
                                _ = cimgui.c.ImGui_TableSetColumnIndex(1);
                                cimgui.c.ImGui_Text(
                                    "T=%d B=%d L=%d R=%d",
                                    ev.scrolling_region.top,
                                    ev.scrolling_region.bottom,
                                    ev.scrolling_region.left,
                                    ev.scrolling_region.right,
                                );
                            }
                        }

                        var md_it = ev.metadata.iterator();
                        while (md_it.next()) |entry| {
                            var buf: [256]u8 = undefined;
                            const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch
                                "<internal error>";
                            cimgui.c.ImGui_TableNextRow();
                            _ = cimgui.c.ImGui_TableNextColumn();
                            cimgui.c.ImGui_Text("%s", key.ptr);
                            _ = cimgui.c.ImGui_TableNextColumn();
                            cimgui.c.ImGui_Text("%s", entry.value_ptr.ptr);
                        }
                    }

                    // If this is the selected event and scrolling is needed, scroll to it
                    if (self.need_scroll_to_selected and self.is_keyboard_selection) {
                        cimgui.c.ImGui_SetScrollHereY(0.5);
                        self.need_scroll_to_selected = false;
                    }
                }
            }
        } // table

        if (cimgui.c.ImGui_BeginPopupModal(
            popup_filter,
            null,
            cimgui.c.ImGuiWindowFlags_AlwaysAutoResize,
        )) {
            defer cimgui.c.ImGui_EndPopup();

            cimgui.c.ImGui_Text("Changed filter settings will only affect future events.");

            cimgui.c.ImGui_Separator();

            {
                _ = cimgui.c.ImGui_BeginTable(
                    "table_filter_kind",
                    3,
                    cimgui.c.ImGuiTableFlags_None,
                );
                defer cimgui.c.ImGui_EndTable();

                inline for (@typeInfo(terminal.Parser.Action.Tag).@"enum".fields) |field| {
                    const tag = @field(terminal.Parser.Action.Tag, field.name);
                    if (tag == .apc_put or tag == .dcs_put) continue;

                    _ = cimgui.c.ImGui_TableNextColumn();
                    var value = !handler.filter_exclude.contains(tag);
                    if (cimgui.c.ImGui_Checkbox(@tagName(tag).ptr, &value)) {
                        if (value) {
                            handler.filter_exclude.remove(tag);
                        } else {
                            handler.filter_exclude.insert(tag);
                        }
                    }
                }
            } // Filter kind table

            cimgui.c.ImGui_Separator();

            cimgui.c.ImGui_Text(
                "Filter by string. Empty displays all, \"abc\" finds lines\n" ++
                    "containing \"abc\", \"abc,xyz\" finds lines containing \"abc\"\n" ++
                    "or \"xyz\", \"-abc\" excludes lines containing \"abc\".",
            );
            _ = cimgui.c.ImGuiTextFilter_Draw(
                &handler.filter_text,
                "##filter_text",
                0,
            );

            cimgui.c.ImGui_Separator();
            if (cimgui.c.ImGui_Button("Close")) {
                cimgui.c.ImGui_CloseCurrentPopup();
            }
        } // filter popup
    }

    /// Copy all recorded events to the standard clipboard in chronological
    /// order.
    fn copyEvents(
        self: *const Stream,
        surface: *Surface,
    ) void {
        const alloc = surface.alloc;
        const contents = self.formatEvents(alloc) catch |err| {
            log.err("failed to format terminal IO events for copy err={}", .{err});
            return;
        };
        defer alloc.free(contents);

        surface.rt_surface.setClipboard(.standard, &.{.{
            .mime = "text/plain",
            .data = contents,
        }}, false) catch |err| {
            log.err("failed to copy terminal IO events err={}", .{err});
        };
    }

    /// Ask the application runtime to save all recorded events to a file.
    fn exportEvents(
        self: *const Stream,
        surface: *Surface,
    ) void {
        const alloc = surface.alloc;
        const contents = self.formatEvents(alloc) catch |err| {
            log.err("failed to format terminal IO events for export err={}", .{err});
            return;
        };
        defer alloc.free(contents);

        const handled = surface.rt_app.performAction(
            .{ .surface = surface },
            .export_terminal_io,
            .{ .contents = contents },
        ) catch |err| {
            log.err("failed to export terminal IO events err={}", .{err});
            return;
        };
        if (!handled) {
            log.warn("application runtime does not support exporting terminal IO events", .{});
        }
    }

    /// Format all recorded events as tab-separated text. Events are exported
    /// oldest-first even though the inspector displays the newest event first.
    fn formatEvents(
        self: *const Stream,
        alloc: Allocator,
    ) ![:0]u8 {
        var buf: std.Io.Writer.Allocating = .init(alloc);
        errdefer buf.deinit();

        try buf.writer.writeAll("Seq\tKind\tDescription\n");
        var it = self.events.iterator(.forward);
        while (it.next()) |ev| {
            try buf.writer.print(
                "{d}\t{s}\t{s}\n",
                .{ ev.seq, @tagName(ev.kind), ev.raw_description },
            );
        }

        return try buf.toOwnedSliceSentinel(0);
    }
};

/// Helper function to check keyboard state and determine navigation action.
fn getKeyAction() KeyAction {
    const keys = .{
        .{ .key = cimgui.c.ImGuiKey_J, .action = KeyAction.down },
        .{ .key = cimgui.c.ImGuiKey_DownArrow, .action = KeyAction.down },
        .{ .key = cimgui.c.ImGuiKey_K, .action = KeyAction.up },
        .{ .key = cimgui.c.ImGuiKey_UpArrow, .action = KeyAction.up },
    };

    inline for (keys) |k| {
        if (cimgui.c.ImGui_IsKeyPressed(k.key)) {
            return k.action;
        }
    }
    return .none;
}

pub fn eventTable(events: *const VTEvent.Ring) void {
    if (!cimgui.c.ImGui_BeginTable(
        "events",
        3,
        cimgui.c.ImGuiTableFlags_RowBg |
            cimgui.c.ImGuiTableFlags_Borders,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    cimgui.c.ImGui_TableSetupColumn(
        "Seq",
        cimgui.c.ImGuiTableColumnFlags_WidthFixed,
    );
    cimgui.c.ImGui_TableSetupColumn(
        "Kind",
        cimgui.c.ImGuiTableColumnFlags_WidthFixed,
    );
    cimgui.c.ImGui_TableSetupColumn(
        "Description",
        cimgui.c.ImGuiTableColumnFlags_WidthStretch,
    );

    var it = events.iterator(.reverse);
    while (it.next()) |ev| {
        // Need to push an ID so that our selectable is unique.
        cimgui.c.ImGui_PushIDPtr(ev);
        defer cimgui.c.ImGui_PopID();

        cimgui.c.ImGui_TableNextRow();
        _ = cimgui.c.ImGui_TableNextColumn();

        cimgui.c.ImGui_SameLine();
        cimgui.c.ImGui_Text("%d", ev.seq);
        _ = cimgui.c.ImGui_TableNextColumn();
        cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr);
        _ = cimgui.c.ImGui_TableNextColumn();
        cimgui.c.ImGui_Text("%s", ev.raw_description.ptr);
    }
}

/// VT event. This isn't public because this is just how we store internal
/// events.
const VTEvent = struct {
    /// The arena that all allocated memory for this event is stored.
    arena_state: ArenaAllocator.State,

    /// Sequence number, just monotonically increasing and wrapping if
    /// it ever overflows. It gives us a nice way to visualize progress.
    seq: usize = 1,

    /// Kind of event, for filtering
    kind: Kind,

    /// The description of the raw event in a more human-friendly format.
    /// For example for control sequences this is the full sequence but
    /// control characters are replaced with human-readable names, e.g.
    /// 0x07 (bell) becomes BEL.
    raw_description: [:0]const u8,

    /// Various metadata at the time of the event (before processing).
    cursor: terminal.Screen.Cursor,
    scrolling_region: terminal.Terminal.ScrollingRegion,
    metadata: Metadata.Unmanaged = .{},

    /// imgui selection state
    imgui_selected: bool = false,

    const Kind = enum { print, execute, csi, esc, osc, dcs, apc };
    const Metadata = std.StringHashMap([:0]const u8);

    /// Circular buffer of VT events.
    pub const Ring = CircBuf(VTEvent, undefined);

    /// Initialize the event information for the given parser action.
    pub fn init(
        alloc_gpa: Allocator,
        t: *const terminal.Terminal,
        action: terminal.Parser.Action,
    ) !VTEvent {
        var arena: ArenaAllocator = .init(alloc_gpa);
        errdefer arena.deinit();
        const alloc = arena.allocator();

        var md = Metadata.init(alloc);
        var buf: std.Io.Writer.Allocating = .init(alloc);
        try encodeAction(alloc, &buf.writer, &md, action);
        const desc = try buf.toOwnedSliceSentinel(0);

        const kind: Kind = switch (action) {
            .print => .print,
            .execute => .execute,
            .csi_dispatch => .csi,
            .esc_dispatch => .esc,
            .osc_dispatch => .osc,
            .dcs_hook, .dcs_put, .dcs_unhook => .dcs,
            .apc_start, .apc_put, .apc_end => .apc,
        };

        return .{
            .arena_state = arena.state,
            .kind = kind,
            .raw_description = desc,
            .cursor = t.screens.active.cursor,
            .scrolling_region = t.scrolling_region,
            .metadata = md.unmanaged,
        };
    }

    pub fn deinit(self: *VTEvent, alloc_gpa: Allocator) void {
        var arena = self.arena_state.promote(alloc_gpa);
        arena.deinit();
    }

    /// Returns true if the event passes the given filter.
    pub fn passFilter(
        self: *const VTEvent,
        filter: *const cimgui.c.ImGuiTextFilter,
    ) bool {
        // Check our main string
        if (cimgui.c.ImGuiTextFilter_PassFilter(
            filter,
            self.raw_description.ptr,
            null,
        )) return true;

        // We also check all metadata keys and values
        var it = self.metadata.iterator();
        while (it.next()) |entry| {
            var buf: [256]u8 = undefined;
            const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch continue;
            if (cimgui.c.ImGuiTextFilter_PassFilter(
                filter,
                key.ptr,
                null,
            )) return true;
            if (cimgui.c.ImGuiTextFilter_PassFilter(
                filter,
                entry.value_ptr.ptr,
                null,
            )) return true;
        }

        return false;
    }

    /// Encode a parser action as a string that we show in the logs.
    fn encodeAction(
        alloc: Allocator,
        writer: *std.Io.Writer,
        md: *Metadata,
        action: terminal.Parser.Action,
    ) !void {
        switch (action) {
            .print => try encodePrint(writer, action),
            .execute => try encodeExecute(writer, action),
            .csi_dispatch => |v| try encodeCSI(writer, v),
            .esc_dispatch => |v| try encodeEsc(writer, v),
            .osc_dispatch => |v| try encodeOSC(alloc, writer, md, v),
            else => try writer.print("{f}", .{action}),
        }
    }

    fn encodePrint(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
        const ch = action.print;
        try writer.print("'{u}' (U+{X})", .{ ch, ch });
    }

    fn encodeExecute(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
        const ch = action.execute;
        switch (ch) {
            0x00 => try writer.writeAll("NUL"),
            0x01 => try writer.writeAll("SOH"),
            0x02 => try writer.writeAll("STX"),
            0x03 => try writer.writeAll("ETX"),
            0x04 => try writer.writeAll("EOT"),
            0x05 => try writer.writeAll("ENQ"),
            0x06 => try writer.writeAll("ACK"),
            0x07 => try writer.writeAll("BEL"),
            0x08 => try writer.writeAll("BS"),
            0x09 => try writer.writeAll("HT"),
            0x0A => try writer.writeAll("LF"),
            0x0B => try writer.writeAll("VT"),
            0x0C => try writer.writeAll("FF"),
            0x0D => try writer.writeAll("CR"),
            0x0E => try writer.writeAll("SO"),
            0x0F => try writer.writeAll("SI"),
            else => try writer.writeAll("?"),
        }
        try writer.print(" (0x{X})", .{ch});
    }

    fn encodeCSI(writer: *std.Io.Writer, csi: terminal.Parser.Action.CSI) !void {
        for (csi.intermediates) |v| try writer.print("{c} ", .{v});
        for (csi.params, 0..) |v, i| {
            if (i != 0) try writer.writeByte(';');
            try writer.print("{d}", .{v});
        }
        if (csi.intermediates.len > 0 or csi.params.len > 0) try writer.writeByte(' ');
        try writer.writeByte(csi.final);
    }

    fn encodeEsc(writer: *std.Io.Writer, esc: terminal.Parser.Action.ESC) !void {
        for (esc.intermediates) |v| try writer.print("{c} ", .{v});
        try writer.writeByte(esc.final);
    }

    fn encodeOSC(
        alloc: Allocator,
        writer: *std.Io.Writer,
        md: *Metadata,
        osc: terminal.osc.Command,
    ) !void {
        // The description is just the tag
        try writer.print("{s} ", .{@tagName(osc)});

        // Add additional fields to metadata
        switch (osc) {
            inline else => |v, tag| if (tag == osc) {
                try encodeMetadata(alloc, md, v);
            },
        }
    }

    fn encodeMetadata(
        alloc: Allocator,
        md: *Metadata,
        v: anytype,
    ) !void {
        switch (@TypeOf(v)) {
            void => {},
            []const u8,
            [:0]const u8,
            => try md.put("data", try alloc.dupeZ(u8, v)),
            else => |T| switch (@typeInfo(T)) {
                .@"struct" => |info| inline for (info.fields) |field| {
                    try encodeMetadataSingle(
                        alloc,
                        md,
                        field.name,
                        @field(v, field.name),
                    );
                },

                .@"union" => |info| {
                    const Tag = info.tag_type orelse @compileError("Unions must have a tag");
                    const tag_name = @tagName(@as(Tag, v));
                    inline for (info.fields) |field| {
                        if (std.mem.eql(u8, field.name, tag_name)) {
                            if (field.type == void) {
                                break try md.put("data", tag_name);
                            } else {
                                break try encodeMetadataSingle(alloc, md, tag_name, @field(v, field.name));
                            }
                        }
                    }
                },

                else => {
                    @compileLog(T);
                    @compileError("unsupported type, see log");
                },
            },
        }
    }

    fn encodeMetadataSingle(
        alloc: Allocator,
        md: *Metadata,
        key: []const u8,
        value: anytype,
    ) !void {
        const Value = @TypeOf(value);
        const info = @typeInfo(Value);
        switch (info) {
            .optional => if (value) |unwrapped| {
                try encodeMetadataSingle(alloc, md, key, unwrapped);
            } else {
                try md.put(key, try alloc.dupeZ(u8, "(unset)"));
            },

            .bool => try md.put(
                key,
                try alloc.dupeZ(u8, if (value) "true" else "false"),
            ),

            .@"enum" => try md.put(
                key,
                try alloc.dupeZ(u8, @tagName(value)),
            ),

            .@"union" => |u| {
                const Tag = u.tag_type orelse @compileError("Unions must have a tag");
                const tag_name = @tagName(@as(Tag, value));
                inline for (u.fields) |field| {
                    if (std.mem.eql(u8, field.name, tag_name)) {
                        const s = if (field.type == void)
                            try alloc.dupeZ(u8, tag_name)
                        else if (field.type == [:0]const u8 or field.type == []const u8)
                            try std.fmt.allocPrintSentinel(alloc, "{s}={s}", .{
                                tag_name,
                                @field(value, field.name),
                            }, 0)
                        else
                            try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{
                                tag_name,
                                @field(value, field.name),
                            }, 0);

                        try md.put(key, s);
                    }
                }
            },

            .@"struct" => try md.put(
                key,
                try alloc.dupeZ(u8, @typeName(Value)),
            ),

            else => switch (Value) {
                []const u8,
                [:0]const u8,
                => try md.put(key, try alloc.dupeZ(u8, value)),

                else => |T| switch (@typeInfo(T)) {
                    .int => try md.put(
                        key,
                        try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0),
                    ),
                    else => {
                        @compileLog(T);
                        @compileError("unsupported type, see log");
                    },
                },
            },
        }
    }
};

/// Our VT stream handler for the Stream widget. This isn't public
/// because there is no reason to use this directly.
const VTHandler = struct {
    /// The capture state, must be set before use. If null, then
    /// events are dropped.
    state: ?State,

    /// True to pause this artificially.
    paused: bool,

    /// Current sequence number
    current_seq: usize,

    /// Exclude certain actions by tag.
    filter_exclude: ActionTagSet,
    filter_text: cimgui.c.ImGuiTextFilter,

    const Stream = terminal.Stream(VTHandler);

    pub const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);

    pub const State = struct {
        /// The allocator to use for the events.
        alloc: Allocator,

        /// The terminal state at the time of the event.
        terminal: *const terminal.Terminal,

        /// The event ring to write events to.
        events: *VTEvent.Ring,
    };

    pub const init: VTHandler = .{
        .state = null,
        .paused = false,
        .current_seq = 1,
        .filter_exclude = .initMany(&.{.print}),
        .filter_text = .{},
    };

    pub fn deinit(self: *VTHandler) void {
        // Required for the parser stream interface
        _ = self;
    }

    pub fn vt(
        self: *VTHandler,
        comptime action: VTHandler.Stream.Action.Tag,
        value: VTHandler.Stream.Action.Value(action),
    ) void {
        _ = self;
        _ = value;
    }

    /// This is called with every single terminal action.
    pub fn vtRaw(self: *VTHandler, action: terminal.Parser.Action) !bool {
        const state: *State = if (self.state) |*s| s else return true;
        const alloc = state.alloc;
        const vt_events = state.events;

        // We always increment the sequence number, even if we're paused or
        // filter out the event. This helps show the user that there is a gap
        // between events and roughly how large that gap was.
        defer self.current_seq +%= 1;

        // If we're manually paused, we ignore all events.
        if (self.paused) return true;

        // We ignore certain action types that are too noisy.
        switch (action) {
            .dcs_put, .apc_put => return true,
            else => {},
        }

        // If we requested a specific type to be ignored, ignore it.
        // We return true because we did "handle" it by ignoring it.
        if (self.filter_exclude.contains(std.meta.activeTag(action))) return true;

        // Build our event
        var ev: VTEvent = try .init(
            alloc,
            state.terminal,
            action,
        );
        ev.seq = self.current_seq;
        errdefer ev.deinit(alloc);

        // Check if the event passes the filter
        if (!ev.passFilter(&self.filter_text)) {
            ev.deinit(alloc);
            return true;
        }

        const max_capacity = 100;
        vt_events.append(ev) catch |err| switch (err) {
            error.OutOfMemory => if (vt_events.capacity() < max_capacity) {
                // We're out of memory, but we can allocate to our capacity.
                const new_capacity = @min(vt_events.capacity() * 2, max_capacity);
                try vt_events.resize(alloc, new_capacity);
                try vt_events.append(ev);
            } else {
                var it = vt_events.iterator(.forward);
                if (it.next()) |old_ev| old_ev.deinit(alloc);
                vt_events.deleteOldest(1);
                try vt_events.append(ev);
            },
        };

        // Do NOT skip it, because we want to record more information
        // about this event.
        return false;
    }
};

/// Enum representing keyboard navigation actions
const KeyAction = enum {
    down,
    none,
    up,
};
