const std = @import("std");
const cimgui = @import("dcimgui");
const terminal = @import("../../terminal/main.zig");
const pagepkg = @import("../../terminal/page.zig");
const stylepkg = @import("../../terminal/style.zig");
const widgets = @import("../widgets.zig");

const PageList = terminal.PageList;

/// Cell pointers resolved against a Node.PreservedPage rather than the
/// PageList node.
/// Omitting the node makes it impossible for inspector helpers to
/// accidentally restore the original compressed page.
const InspectedCell = struct {
    row: *pagepkg.Row,
    cell: *pagepkg.Cell,
    row_idx: terminal.size.CellCountInt,
    col_idx: terminal.size.CellCountInt,
};

/// PageList inspector widget.
pub const Inspector = struct {
    pub const empty: Inspector = .{};

    pub fn draw(_: *const Inspector, pages: *PageList) void {
        cimgui.c.ImGui_TextWrapped(
            "PageList manages the backing pages that hold scrollback and the active " ++
                "terminal grid. Each page is a contiguous memory buffer with its " ++
                "own rows, cells, style set, grapheme map, and hyperlink storage.",
        );

        if (cimgui.c.ImGui_CollapsingHeader(
            "Overview",
            cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
        )) {
            summaryTable(pages);
        }

        if (cimgui.c.ImGui_CollapsingHeader(
            "Page Compression",
            cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
        )) {
            compressionTable(pages);
        }

        if (cimgui.c.ImGui_CollapsingHeader(
            "Scrollbar & Regions",
            cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
        )) {
            cimgui.c.ImGui_SeparatorText("Scrollbar");
            scrollbarInfo(pages);
            cimgui.c.ImGui_SeparatorText("Regions");
            regionsTable(pages);
        }

        if (cimgui.c.ImGui_CollapsingHeader(
            "Tracked Pins",
            cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
        )) {
            trackedPinsTable(pages);
        }

        if (cimgui.c.ImGui_CollapsingHeader(
            "Pages",
            cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
        )) {
            widgets.helpMarker(
                "Pages are shown most-recent first. Each page holds a grid of rows/cells " ++
                    "plus metadata tables for styles, graphemes, strings, and hyperlinks.",
            );

            const active_pin = pages.getTopLeft(.active);
            const viewport_pin = pages.getTopLeft(.viewport);

            var row_offset = pages.total_rows;
            var index: usize = pages.totalPages();
            var node = pages.pages.last;
            while (node) |page_node| : (node = page_node.prev) {
                const rows = page_node.rows();
                const resident = page_node.pageIfResident();
                const compressed = page_node.storage() == .compressed;
                row_offset -= rows;
                index -= 1;

                // We use our location as the ID so that even if reallocations
                // happen we remain open if we're open already.
                cimgui.c.ImGui_PushIDInt(@intCast(index));
                defer cimgui.c.ImGui_PopID();

                // Open up the tree node.
                if (!widgets.page.treeNode(.{
                    .cols = page_node.cols(),
                    .rows = rows,
                    .index = index,
                    .row_range = .{ row_offset, row_offset + rows - 1 },
                    .active = node == active_pin.node,
                    .viewport = node == viewport_pin.node,
                    .compressed = compressed,
                    .dirty = if (resident) |page| page.isDirty() else null,
                })) continue;
                defer cimgui.c.ImGui_TreePop();

                // Decode compressed contents into a temporary page. The
                // original node stays compressed while its entry is open.
                var preserved = page_node.pagePreservingState(
                    std.heap.page_allocator,
                ) catch {
                    cimgui.c.ImGui_TextDisabled(
                        "(unable to copy compressed page)",
                    );
                    continue;
                };
                defer preserved.deinit();
                widgets.page.inspector(preserved.page());
            }
        }
    }
};

fn summaryTable(pages: *const PageList) void {
    if (!cimgui.c.ImGui_BeginTable(
        "pagelist_summary",
        3,
        cimgui.c.ImGuiTableFlags_BordersInnerV |
            cimgui.c.ImGuiTableFlags_RowBg |
            cimgui.c.ImGuiTableFlags_SizingFixedFit,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Active Grid");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Active viewport size in columns x rows.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%dc x %dr", pages.cols, pages.rows);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Pages");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Total number of pages in the linked list.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%zu", pages.totalPages());

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Total Rows");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Total rows represented by scrollback + active area.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%zu", pages.total_rows);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Scrollback Limit");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker(
        \\Maximum uncompressed logical page memory before the oldest
        \\history is evicted. Minimum page allocation sizes can make
        \\current usage temporarily exceed this value.
    );
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    var limit_buf: [64]u8 = undefined;
    const limit = formatBytes(&limit_buf, pages.limits.max(.bytes));
    cimgui.c.ImGui_TextUnformatted(limit.ptr);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Viewport");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Current viewport anchoring mode.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr);
}

fn compressionTable(pages: *const PageList) void {
    const memory = pages.memoryStats();

    if (!cimgui.c.ImGui_BeginTable(
        "pagelist_compression",
        3,
        cimgui.c.ImGuiTableFlags_BordersInnerV |
            cimgui.c.ImGuiTableFlags_RowBg |
            cimgui.c.ImGuiTableFlags_SizingFixedFit,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    compressionTextRow(
        "Platform Support",
        "Whether this target can discard physical page memory while retaining " ++
            "its virtual address range. The scrollback-compression setting may " ++
            "still disable automatic compression on a supported platform.",
        if (terminal.compression_enabled) "supported" else "unsupported",
    );

    var state_buf: [96]u8 = undefined;
    const state = std.fmt.bufPrintZ(
        &state_buf,
        "{d} compressed, {d} resident",
        .{ memory.compressed_pages, memory.resident_pages },
    ) catch unreachable;
    compressionTextRow(
        "Page States",
        "Compressed pages retain an encoded allocation while their raw mapping " ++
            "is decommitted. Resident pages still have physical raw backing. " ++
            "Active, visible, and recently changed pages are expected to be resident.",
        state,
    );

    var raw_buf: [64]u8 = undefined;
    compressionTextRow(
        "Uncompressed Size",
        "Total size of all raw page mappings. This is the approximate page " ++
            "backing memory required if no pages were compressed and is also " ++
            "the virtual address space retained across compression.",
        formatBytes(&raw_buf, memory.raw_bytes),
    );

    var encoded_buf: [64]u8 = undefined;
    var compressed_raw_buf: [64]u8 = undefined;
    var storage_buf: [160]u8 = undefined;
    const storage = if (memory.decommitted_raw_bytes > 0) storage: {
        const ratio = percentage(
            memory.encoded_bytes,
            memory.decommitted_raw_bytes,
        );
        break :storage std.fmt.bufPrintZ(
            &storage_buf,
            "{s} encoded / {s} raw ({d:.1}%)",
            .{
                formatBytes(&encoded_buf, memory.encoded_bytes),
                formatBytes(
                    &compressed_raw_buf,
                    memory.decommitted_raw_bytes,
                ),
                ratio,
            },
        ) catch unreachable;
    } else "none";
    compressionTextRow(
        "Compressed Storage",
        "Encoded allocation size compared with the original raw size of only " ++
            "the compressed pages. The percentage is the compression ratio, " ++
            "so smaller is better. Raw physical pages have been discarded.",
        storage,
    );

    var resident_buf: [64]u8 = undefined;
    compressionTextRow(
        "Estimated Resident",
        "Physical page backing estimated to remain after compression: resident " ++
            "raw allocations, including unused pool tails, plus encoded storage. " ++
            "Node and allocator metadata and unrelated terminal memory are excluded.",
        formatBytes(&resident_buf, memory.estimatedResidentBytes()),
    );

    var savings_bytes_buf: [64]u8 = undefined;
    var savings_buf: [128]u8 = undefined;
    const savings = memory.estimatedSavings();
    const savings_text = std.fmt.bufPrintZ(
        &savings_buf,
        "{s} ({d:.1}% of raw page memory)",
        .{
            formatBytes(&savings_bytes_buf, savings),
            percentage(savings, memory.raw_bytes),
        },
    ) catch unreachable;
    compressionTextRow(
        "Estimated Savings",
        "Physical page backing avoided across the complete PageList. This is " ++
            "decommitted raw memory minus replacement encoded storage and is " ++
            "not a measurement of total process RSS.",
        savings_text,
    );
}

fn compressionTextRow(
    label: [:0]const u8,
    help: [:0]const u8,
    value: [:0]const u8,
) void {
    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("%s", label.ptr);
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker(help);
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_TextUnformatted(value.ptr);
}

fn formatBytes(buf: []u8, bytes: usize) [:0]const u8 {
    if (bytes >= 1024 * 1024) {
        const value: f64 = @as(f64, @floatFromInt(bytes)) / (1024 * 1024);
        return std.fmt.bufPrintZ(buf, "{d:.2} MiB", .{value}) catch unreachable;
    }

    if (bytes >= 1024) {
        const value: f64 = @as(f64, @floatFromInt(bytes)) / 1024;
        return std.fmt.bufPrintZ(buf, "{d:.1} KiB", .{value}) catch unreachable;
    }

    return std.fmt.bufPrintZ(buf, "{d} B", .{bytes}) catch unreachable;
}

fn percentage(numerator: usize, denominator: usize) f64 {
    if (denominator == 0) return 0;
    return 100 *
        @as(f64, @floatFromInt(numerator)) /
        @as(f64, @floatFromInt(denominator));
}

fn scrollbarInfo(pages: *PageList) void {
    const scrollbar = pages.scrollbar();

    // If we have a scrollbar, show it.
    if (scrollbar.total > 0) {
        var delta_row: isize = 0;
        scrollbarWidget(&scrollbar, &delta_row);
        if (delta_row != 0) {
            pages.scroll(.{ .delta_row = delta_row });
        }
    }

    if (!cimgui.c.ImGui_BeginTable(
        "scrollbar_info",
        3,
        cimgui.c.ImGuiTableFlags_BordersInnerV |
            cimgui.c.ImGuiTableFlags_RowBg |
            cimgui.c.ImGuiTableFlags_SizingFixedFit,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Total");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Total number of scrollable rows including scrollback and active area.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%d", scrollbar.total);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Offset");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Current scroll position as row offset from the top of scrollback.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%d", scrollbar.offset);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Length");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker("Number of rows visible in the viewport.");
    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    cimgui.c.ImGui_Text("%d", scrollbar.len);
}

fn regionsTable(pages: *PageList) void {
    if (!cimgui.c.ImGui_BeginTable(
        "pagelist_regions",
        4,
        cimgui.c.ImGuiTableFlags_BordersInnerV |
            cimgui.c.ImGuiTableFlags_RowBg |
            cimgui.c.ImGuiTableFlags_SizingFixedFit,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    cimgui.c.ImGui_TableSetupColumn("Region", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("Top-Left", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("Bottom-Right", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
    cimgui.c.ImGui_TableHeadersRow();

    inline for (comptime std.meta.tags(terminal.point.Tag)) |tag| {
        regionRow(pages, tag);
    }
}

fn regionRow(pages: *const PageList, comptime tag: terminal.point.Tag) void {
    const tl_pin = pages.getTopLeft(tag);
    const br_pin = pages.getBottomRight(tag);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("%s", @tagName(tag).ptr);

    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    widgets.helpMarker(comptime regionHelpText(tag));

    _ = cimgui.c.ImGui_TableSetColumnIndex(2);
    if (pages.pointFromPin(tag, tl_pin)) |pt| {
        const coord = pt.coord();
        cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
    } else {
        cimgui.c.ImGui_TextDisabled("(n/a)");
    }

    _ = cimgui.c.ImGui_TableSetColumnIndex(3);
    if (br_pin) |br| {
        if (pages.pointFromPin(tag, br)) |pt| {
            const coord = pt.coord();
            cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
        } else {
            cimgui.c.ImGui_TextDisabled("(n/a)");
        }
    } else {
        cimgui.c.ImGui_TextDisabled("(empty)");
    }
}

fn regionHelpText(comptime tag: terminal.point.Tag) [:0]const u8 {
    return switch (tag) {
        .active => "The active area where a running program can jump the cursor " ++
            "and make changes. This is the 'editable' part of the screen. " ++
            "Bottom-right includes the full height of the screen, including " ++
            "rows that may not be written yet.",
        .viewport => "The visible viewport. If the user has scrolled, top-left changes. " ++
            "Bottom-right is the last written row from the top-left.",
        .screen => "Top-left is the furthest back in scrollback history. Bottom-right " ++
            "is the last written row. Unlike 'active', this only contains " ++
            "written rows.",
        .history => "Same top-left as 'screen' but bottom-right is the line just before " ++
            "the top of 'active'. Contains only the scrollback history.",
    };
}

fn trackedPinsTable(pages: *const PageList) void {
    if (!cimgui.c.ImGui_BeginTable(
        "tracked_pins",
        5,
        cimgui.c.ImGuiTableFlags_Borders |
            cimgui.c.ImGuiTableFlags_RowBg |
            cimgui.c.ImGuiTableFlags_SizingFixedFit,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    cimgui.c.ImGui_TableSetupColumn("Index", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("Pin", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("Context", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("Dirty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableSetupColumn("State", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
    cimgui.c.ImGui_TableHeadersRow();

    const active_pin = pages.getTopLeft(.active);
    const viewport_pin = pages.getTopLeft(.viewport);

    for (pages.trackedPins(), 0..) |tracked, idx| {
        const pin = tracked.*;
        cimgui.c.ImGui_TableNextRow();
        _ = cimgui.c.ImGui_TableSetColumnIndex(0);
        cimgui.c.ImGui_Text("%d", idx);

        _ = cimgui.c.ImGui_TableSetColumnIndex(1);
        if (pin.garbage) {
            cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.5, .z = 0.3, .w = 1.0 }, "(%d, %d)", pin.x, pin.y);
        } else {
            cimgui.c.ImGui_Text("(%d, %d)", pin.x, pin.y);
        }

        _ = cimgui.c.ImGui_TableSetColumnIndex(2);
        if (pages.pointFromPin(.screen, pin)) |pt| {
            const coord = pt.coord();
            cimgui.c.ImGui_Text(
                "screen (%d, %d)",
                coord.x,
                coord.y,
            );
        } else {
            cimgui.c.ImGui_TextDisabled("screen (out of range)");
        }

        _ = cimgui.c.ImGui_TableSetColumnIndex(3);
        if (pin.node.pageIfResident()) |page| {
            const dirty = page.dirty or
                page.getRowAndCell(pin.x, pin.y).row.dirty;
            if (dirty) {
                cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty");
            } else {
                cimgui.c.ImGui_TextDisabled("clean");
            }
        } else {
            // Dirty state lives in the discarded mapping. Keep inspector
            // traversal metadata-only rather than restoring this page.
            cimgui.c.ImGui_TextDisabled("compressed");
        }

        _ = cimgui.c.ImGui_TableSetColumnIndex(4);
        if (pin.eql(active_pin)) {
            cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active top");
        } else if (pin.eql(viewport_pin)) {
            cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport top");
        } else if (pin.garbage) {
            cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.5, .z = 0.3, .w = 1.0 }, "garbage");
        } else if (tracked == pages.viewport_pin) {
            cimgui.c.ImGui_Text("viewport pin");
        } else {
            cimgui.c.ImGui_TextDisabled("tracked");
        }
    }
}

fn scrollbarWidget(
    scrollbar: *const PageList.Scrollbar,
    delta_row: *isize,
) void {
    delta_row.* = 0;

    const avail_width = cimgui.c.ImGui_GetContentRegionAvail().x;
    const bar_height: f32 = cimgui.c.ImGui_GetFrameHeight();
    const cursor_pos = cimgui.c.ImGui_GetCursorScreenPos();

    const total_f: f32 = @floatFromInt(scrollbar.total);
    const offset_f: f32 = @floatFromInt(scrollbar.offset);
    const len_f: f32 = @floatFromInt(scrollbar.len);

    const grab_start = (offset_f / total_f) * avail_width;
    const grab_width = @max((len_f / total_f) * avail_width, 4.0);

    const draw_list = cimgui.c.ImGui_GetWindowDrawList();
    const bg_color = cimgui.c.ImGui_GetColorU32(cimgui.c.ImGuiCol_ScrollbarBg);
    const grab_color = cimgui.c.ImGui_GetColorU32(cimgui.c.ImGuiCol_ScrollbarGrab);

    const bg_min: cimgui.c.ImVec2 = cursor_pos;
    const bg_max: cimgui.c.ImVec2 = .{ .x = cursor_pos.x + avail_width, .y = cursor_pos.y + bar_height };
    cimgui.c.ImDrawList_AddRectFilledEx(
        draw_list,
        bg_min,
        bg_max,
        bg_color,
        0,
        0,
    );

    const grab_min: cimgui.c.ImVec2 = .{
        .x = cursor_pos.x + grab_start,
        .y = cursor_pos.y,
    };
    const grab_max: cimgui.c.ImVec2 = .{
        .x = cursor_pos.x + grab_start + grab_width,
        .y = cursor_pos.y + bar_height,
    };
    cimgui.c.ImDrawList_AddRectFilledEx(
        draw_list,
        grab_min,
        grab_max,
        grab_color,
        0,
        0,
    );
    _ = cimgui.c.ImGui_InvisibleButton(
        "scrollbar_drag",
        .{ .x = avail_width, .y = bar_height },
        0,
    );
    if (cimgui.c.ImGui_IsItemActive()) {
        const drag_delta = cimgui.c.ImGui_GetMouseDragDelta(
            cimgui.c.ImGuiMouseButton_Left,
            0.0,
        );
        if (drag_delta.x != 0) {
            const row_delta = (drag_delta.x / avail_width) * total_f;
            delta_row.* = @intFromFloat(row_delta);
            cimgui.c.ImGui_ResetMouseDragDelta();
        }
    }

    if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
        cimgui.c.ImGui_SetTooltip(
            "offset=%d len=%d total=%d",
            scrollbar.offset,
            scrollbar.len,
            scrollbar.total,
        );
    }
}

/// Grid inspector widget for choosing and inspecting a specific cell.
pub const CellChooser = struct {
    lookup_region: terminal.point.Tag,
    lookup_coord: terminal.point.Coordinate,
    cell_info: CellInfo,

    pub const empty: CellChooser = .{
        .lookup_region = .viewport,
        .lookup_coord = .{ .x = 0, .y = 0 },
        .cell_info = .empty,
    };

    pub fn draw(
        self: *CellChooser,
        pages: *const PageList,
    ) void {
        cimgui.c.ImGui_TextWrapped(
            "Inspect a cell by choosing a coordinate space and entering the X/Y position. " ++
                "The inspector resolves the point into the page list and displays the cell contents.",
        );

        cimgui.c.ImGui_SeparatorText("Cell Inspector");

        const region_max = maxCoord(pages, self.lookup_region);
        if (region_max) |coord| {
            self.lookup_coord.x = @min(self.lookup_coord.x, coord.x);
            self.lookup_coord.y = @min(self.lookup_coord.y, coord.y);
        } else {
            self.lookup_coord = .{ .x = 0, .y = 0 };
        }

        {
            const disabled = region_max == null;
            cimgui.c.ImGui_BeginDisabled(disabled);
            defer cimgui.c.ImGui_EndDisabled();

            const preview = @tagName(self.lookup_region);
            const combo_width = comptime blk: {
                var max_len: usize = 0;
                for (std.meta.tags(terminal.point.Tag)) |tag| {
                    max_len = @max(max_len, @tagName(tag).len);
                }
                break :blk max_len + 4;
            };
            cimgui.c.ImGui_SetNextItemWidth(cimgui.c.ImGui_CalcTextSize("X" ** combo_width).x);
            if (cimgui.c.ImGui_BeginCombo(
                "##grid_region",
                preview.ptr,
                cimgui.c.ImGuiComboFlags_HeightSmall,
            )) {
                inline for (comptime std.meta.tags(terminal.point.Tag)) |tag| {
                    const selected = tag == self.lookup_region;
                    if (cimgui.c.ImGui_SelectableEx(
                        @tagName(tag).ptr,
                        selected,
                        cimgui.c.ImGuiSelectableFlags_None,
                        .{ .x = 0, .y = 0 },
                    )) {
                        self.lookup_region = tag;
                    }
                    if (selected) cimgui.c.ImGui_SetItemDefaultFocus();
                }
                cimgui.c.ImGui_EndCombo();
            }

            cimgui.c.ImGui_SameLine();

            const width = cimgui.c.ImGui_CalcTextSize("00000").x;
            var x_value: terminal.size.CellCountInt = self.lookup_coord.x;
            var y_value: u32 = self.lookup_coord.y;
            var changed = false;

            cimgui.c.ImGui_AlignTextToFramePadding();
            cimgui.c.ImGui_Text("x:");
            cimgui.c.ImGui_SameLine();
            cimgui.c.ImGui_SetNextItemWidth(width);
            if (cimgui.c.ImGui_InputScalar(
                "##grid_x",
                cimgui.c.ImGuiDataType_U16,
                &x_value,
            )) changed = true;

            cimgui.c.ImGui_SameLine();
            cimgui.c.ImGui_AlignTextToFramePadding();
            cimgui.c.ImGui_Text("y:");
            cimgui.c.ImGui_SameLine();
            cimgui.c.ImGui_SetNextItemWidth(width);
            if (cimgui.c.ImGui_InputScalar(
                "##grid_y",
                cimgui.c.ImGuiDataType_U32,
                &y_value,
            )) changed = true;

            cimgui.c.ImGui_SameLine();
            widgets.helpMarker("Choose the coordinate space and X/Y position (0-indexed).");

            if (changed) {
                if (region_max) |coord| {
                    self.lookup_coord.x = @min(x_value, coord.x);
                    self.lookup_coord.y = @min(y_value, coord.y);
                }
            }
        }

        if (region_max) |coord| {
            cimgui.c.ImGui_TextDisabled(
                "Range: x 0..%d, y 0..%d",
                coord.x,
                coord.y,
            );
        } else {
            cimgui.c.ImGui_TextDisabled("(region has no rows)");
            return;
        }

        const pt = switch (self.lookup_region) {
            .active => terminal.Point{ .active = self.lookup_coord },
            .viewport => terminal.Point{ .viewport = self.lookup_coord },
            .screen => terminal.Point{ .screen = self.lookup_coord },
            .history => terminal.Point{ .history = self.lookup_coord },
        };

        const pin = pages.pin(pt) orelse {
            cimgui.c.ImGui_TextDisabled("(cell out of range)");
            return;
        };

        // Cell pointers must come from the same page whose auxiliary tables
        // we inspect. For compressed nodes this is an independent preserved
        // page, leaving the PageList node compressed across inspector frames.
        var preserved = pin.node.pagePreservingState(
            std.heap.page_allocator,
        ) catch {
            cimgui.c.ImGui_TextDisabled("(unable to copy compressed page)");
            return;
        };
        defer preserved.deinit();

        const page = preserved.page();
        const rac = page.getRowAndCell(pin.x, pin.y);
        const cell: InspectedCell = .{
            .row = rac.row,
            .cell = rac.cell,
            .row_idx = pin.y,
            .col_idx = pin.x,
        };

        self.cell_info.draw(cell, pt, page);

        if (cell.cell.style_id != stylepkg.default_id) {
            cimgui.c.ImGui_SeparatorText("Style");
            const style = page.styles.get(
                page.memory,
                cell.cell.style_id,
            ).*;
            widgets.style.table(style, null);
        }

        if (cell.cell.hyperlink) {
            cimgui.c.ImGui_SeparatorText("Hyperlink");
            hyperlinkTable(cell, page);
        }

        if (cell.cell.hasGrapheme()) {
            cimgui.c.ImGui_SeparatorText("Grapheme");
            graphemeTable(cell, page);
        }
    }
};

fn maxCoord(
    pages: *const PageList,
    tag: terminal.point.Tag,
) ?terminal.point.Coordinate {
    const br_pin = pages.getBottomRight(tag) orelse return null;
    const br_point = pages.pointFromPin(tag, br_pin) orelse return null;
    return br_point.coord();
}

fn hyperlinkTable(cell: InspectedCell, page: *const terminal.Page) void {
    if (!cimgui.c.ImGui_BeginTable(
        "cell_hyperlink",
        2,
        cimgui.c.ImGuiTableFlags_None,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    const link_id = page.lookupHyperlink(cell.cell) orelse {
        cimgui.c.ImGui_TableNextRow();
        _ = cimgui.c.ImGui_TableSetColumnIndex(0);
        cimgui.c.ImGui_Text("Status");
        _ = cimgui.c.ImGui_TableSetColumnIndex(1);
        cimgui.c.ImGui_TextDisabled("(missing link data)");
        return;
    };

    const entry = page.hyperlink_set.get(page.memory, link_id);

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("ID");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    switch (entry.id) {
        .implicit => |value| cimgui.c.ImGui_Text("implicit %d", value),
        .explicit => |slice| {
            const id = slice.slice(page.memory);
            if (id.len == 0) {
                cimgui.c.ImGui_TextDisabled("(empty)");
            } else {
                cimgui.c.ImGui_Text("%.*s", id.len, id.ptr);
            }
        },
    }

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("URI");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    const uri = entry.uri.slice(page.memory);
    if (uri.len == 0) {
        cimgui.c.ImGui_TextDisabled("(empty)");
    } else {
        cimgui.c.ImGui_Text("%.*s", uri.len, uri.ptr);
    }

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Ref Count");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    const refs = page.hyperlink_set.refCount(page.memory, link_id);
    cimgui.c.ImGui_Text("%d", refs);
}

fn graphemeTable(cell: InspectedCell, page: *const terminal.Page) void {
    if (!cimgui.c.ImGui_BeginTable(
        "cell_grapheme",
        2,
        cimgui.c.ImGuiTableFlags_None,
    )) return;
    defer cimgui.c.ImGui_EndTable();

    const cps = page.lookupGrapheme(cell.cell) orelse {
        cimgui.c.ImGui_TableNextRow();
        _ = cimgui.c.ImGui_TableSetColumnIndex(0);
        cimgui.c.ImGui_Text("Status");
        _ = cimgui.c.ImGui_TableSetColumnIndex(1);
        cimgui.c.ImGui_TextDisabled("(missing grapheme data)");
        return;
    };

    cimgui.c.ImGui_TableNextRow();
    _ = cimgui.c.ImGui_TableSetColumnIndex(0);
    cimgui.c.ImGui_Text("Extra Codepoints");
    _ = cimgui.c.ImGui_TableSetColumnIndex(1);
    if (cps.len == 0) {
        cimgui.c.ImGui_TextDisabled("(none)");
        return;
    }

    var buf: [96]u8 = undefined;
    if (cimgui.c.ImGui_BeginListBox("##grapheme_list", .{ .x = 0, .y = 0 })) {
        defer cimgui.c.ImGui_EndListBox();
        for (cps) |cp| {
            const label = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch "U+?";
            _ = cimgui.c.ImGui_SelectableEx(
                label.ptr,
                false,
                cimgui.c.ImGuiSelectableFlags_None,
                .{ .x = 0, .y = 0 },
            );
        }
    }
}

/// Cell inspector widget.
pub const CellInfo = struct {
    pub const empty: CellInfo = .{};

    pub fn draw(
        _: *const CellInfo,
        cell: InspectedCell,
        point: terminal.Point,
        page: *const terminal.Page,
    ) void {
        if (!cimgui.c.ImGui_BeginTable(
            "cell_info",
            3,
            cimgui.c.ImGuiTableFlags_BordersInnerV |
                cimgui.c.ImGuiTableFlags_RowBg |
                cimgui.c.ImGuiTableFlags_SizingFixedFit,
        )) return;
        defer cimgui.c.ImGui_EndTable();

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Grid Position");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("The cell's X/Y coordinates in the selected region.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            const coord = point.coord();
            cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Page Location");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Row and column indices within the backing page.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            cimgui.c.ImGui_Text("row=%d col=%d", cell.row_idx, cell.col_idx);
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Content");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Content tag describing how the cell data is stored.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            cimgui.c.ImGui_Text("%s", @tagName(cell.cell.content_tag).ptr);
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Codepoint");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Primary Unicode codepoint for the cell.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            const cp = cell.cell.codepoint();
            if (cp == 0) {
                cimgui.c.ImGui_TextDisabled("(empty)");
            } else {
                cimgui.c.ImGui_Text("U+%04X", @as(u32, cp));
            }
        }

        if (cell.cell.hasGrapheme()) {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Grapheme");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Extra codepoints that combine with the primary codepoint to form the grapheme cluster.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            if (cimgui.c.ImGui_BeginListBox("##cell_grapheme", .{ .x = 0, .y = 0 })) {
                defer cimgui.c.ImGui_EndListBox();
                if (page.lookupGrapheme(cell.cell)) |cps| {
                    var buf: [96]u8 = undefined;
                    for (cps) |cp| {
                        const label = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch "U+?";
                        _ = cimgui.c.ImGui_SelectableEx(
                            label.ptr,
                            false,
                            cimgui.c.ImGuiSelectableFlags_None,
                            .{ .x = 0, .y = 0 },
                        );
                    }
                } else {
                    _ = cimgui.c.ImGui_SelectableEx(
                        "(missing)",
                        false,
                        cimgui.c.ImGuiSelectableFlags_None,
                        .{ .x = 0, .y = 0 },
                    );
                }
            }
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Width Property");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Character width property (narrow, wide, spacer, etc.).");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            cimgui.c.ImGui_Text("%s", @tagName(cell.cell.wide).ptr);
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Row Flags");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Flags set on the row containing this cell.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            const row = cell.row;
            if (row.wrap or row.wrap_continuation or row.grapheme or row.styled or row.hyperlink) {
                if (row.wrap) {
                    cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "wrap");
                    cimgui.c.ImGui_SameLine();
                }
                if (row.wrap_continuation) {
                    cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "cont");
                    cimgui.c.ImGui_SameLine();
                }
                if (row.grapheme) {
                    cimgui.c.ImGui_TextColored(.{ .x = 0.9, .y = 0.7, .z = 0.3, .w = 1.0 }, "grapheme");
                    cimgui.c.ImGui_SameLine();
                }
                if (row.styled) {
                    cimgui.c.ImGui_TextColored(.{ .x = 0.7, .y = 0.9, .z = 0.5, .w = 1.0 }, "styled");
                    cimgui.c.ImGui_SameLine();
                }
                if (row.hyperlink) {
                    cimgui.c.ImGui_TextColored(.{ .x = 0.8, .y = 0.6, .z = 1.0, .w = 1.0 }, "link");
                    cimgui.c.ImGui_SameLine();
                }
            } else {
                cimgui.c.ImGui_TextDisabled("(none)");
            }
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Style ID");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Internal style reference ID for this cell.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            cimgui.c.ImGui_Text("%d", cell.cell.style_id);
        }

        {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Style");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("Resolved style for the cell (colors, attributes, etc.).");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);
            if (cell.cell.style_id == stylepkg.default_id) {
                cimgui.c.ImGui_TextDisabled("(default)");
            } else {
                cimgui.c.ImGui_TextDisabled("(see below)");
            }
        }

        if (cell.cell.hyperlink) {
            cimgui.c.ImGui_TableNextRow();
            _ = cimgui.c.ImGui_TableSetColumnIndex(0);
            cimgui.c.ImGui_Text("Hyperlink");
            _ = cimgui.c.ImGui_TableSetColumnIndex(1);
            widgets.helpMarker("OSC8 hyperlink ID associated with this cell.");
            _ = cimgui.c.ImGui_TableSetColumnIndex(2);

            const link_id = page.lookupHyperlink(cell.cell) orelse 0;
            cimgui.c.ImGui_Text("id=%d", link_id);
        }
    }
};
