//! This file contains all the terminal modes that we support
//! and various support types for them: an enum of supported modes,
//! a packed struct to store mode values, a more generalized state
//! struct to store values plus handle save/restore, and much more.
//!
//! There is pretty heavy comptime usage and type generation here.
//! I don't love to have this sort of complexity but its a good way
//! to ensure all our various types and logic remain in sync.

const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("terminal_options");
const testing = std.testing;

/// A struct that maintains the state of all the settable modes.
pub const ModeState = struct {
    /// The values of the current modes.
    values: ModePacked = .{},

    /// The saved values. We only allow saving each mode once.
    /// This is in line with other terminals that implement XTSAVE
    /// and XTRESTORE. We can improve this in the future if it becomes
    /// a real-world issue but we need to be aware of a DoS vector.
    saved: ModePacked = .{},

    /// The default values for the modes. This is used to reset
    /// the modes to their default values during reset.
    default: ModePacked = .{},

    /// Reset the modes to their default values. This also clears the
    /// saved state.
    pub fn reset(self: *ModeState) void {
        self.values = self.default;
        self.saved = .{};
    }

    /// Set a mode to a value.
    pub fn set(self: *ModeState, mode: Mode, value: bool) void {
        setPacked(&self.values, mode, value);
    }

    /// Set the reset default and current value for a mode.
    pub fn setDefault(self: *ModeState, mode: Mode, value: bool) void {
        setPacked(&self.values, mode, value);
        setPacked(&self.default, mode, value);
    }

    /// Get the value of a mode.
    pub fn get(self: *const ModeState, mode: Mode) bool {
        return getPacked(&self.values, mode);
    }

    /// Save the state of the given mode. This can then be restored
    /// with restore. This will only be accurate if the previous
    /// mode was saved exactly once and not restored. Otherwise this
    /// will just keep restoring the last stored value in memory.
    pub fn save(self: *ModeState, mode: Mode) void {
        switch (mode) {
            inline else => |mode_comptime| {
                const entry = comptime entryForMode(mode_comptime);
                @field(self.saved, entry.name) = @field(self.values, entry.name);
            },
        }
    }

    /// See save. This will return the restored value.
    pub fn restore(self: *ModeState, mode: Mode) bool {
        switch (mode) {
            inline else => |mode_comptime| {
                const entry = comptime entryForMode(mode_comptime);
                @field(self.values, entry.name) = @field(self.saved, entry.name);
                return @field(self.values, entry.name);
            },
        }
    }

    /// Return a DECRPM report for the given mode tag. If the tag does
    /// not correspond to a known mode, the report state is .not_recognized.
    pub fn getReport(self: *const ModeState, tag: ModeTag) Report {
        // DECECM (Erase Color Mode, DEC private mode 117) controls whether erasing
        // and scrolling use the default background or the active background color.
        // Ghostty's behavior is fixed equivalent to DECECM reset, and DECRQM has a
        // "permanently reset" response for recognized modes that cannot be changed.
        // Report that instead of "not recognized" so applications can query and adapt
        // to Ghostty's erase-color behavior.
        //
        // See VT520/VT525 Programmer Information, "Erase Color" and DECRQM/DECRPM:
        // https://web.mit.edu/dosathena/doc/www/ek-vt520-rm.pdf

        if (!tag.ansi and tag.value == 117) {
            return .{ .tag = tag, .state = .permanently_reset };
        }
        const mode = modeFromInt(tag.value, tag.ansi) orelse return .{
            .tag = tag,
            .state = .not_recognized,
        };
        return .{
            .tag = tag,
            .state = if (self.get(mode)) .set else .reset,
        };
    }

    test {
        // We have this here so that we explicitly fail when we change the
        // size of modes. The size of modes is NOT particularly important,
        // we just want to be mentally aware when it happens.
        try std.testing.expectEqual(8, @sizeOf(ModePacked));
    }
};

fn setPacked(values: *ModePacked, mode: Mode, value: bool) void {
    switch (mode) {
        inline else => |mode_comptime| {
            const entry = comptime entryForMode(mode_comptime);
            @field(values, entry.name) = value;
        },
    }
}

fn getPacked(values: *const ModePacked, mode: Mode) bool {
    switch (mode) {
        inline else => |mode_comptime| {
            const entry = comptime entryForMode(mode_comptime);
            return @field(values, entry.name);
        },
    }
}

/// A packed struct of all the settable modes. This shouldn't
/// be used directly but rather through the ModeState struct.
pub const ModePacked = packed_struct: {
    const StructField = std.builtin.Type.StructField;
    var names: [entries.len][]const u8 = undefined;
    var types = [_]type{bool} ** entries.len;
    var attrs: [entries.len]StructField.Attributes = undefined;

    for (entries, &names, &attrs) |entry, *name, *attr| {
        name.* = entry.name;
        attr.* = .{ .default_value_ptr = &entry.default };
    }

    break :packed_struct @Struct(.@"packed", null, &names, &types, &attrs);
};

/// An enum(u16) of the available modes. See entries for available values.
pub const Mode = mode_enum: {
    var names: [entries.len][]const u8 = undefined;
    var values: [entries.len]ModeTag.Backing = undefined;
    for (entries, &names, &values) |entry, *name, *value| {
        name.* = entry.name;
        value.* = @bitCast(ModeTag{
            .value = entry.value,
            .ansi = entry.ansi,
        });
    }

    break :mode_enum @Enum(ModeTag.Backing, .exhaustive, &names, &values);
};

/// The tag type for our enum is a u16 but we use a packed struct
/// in order to pack the ansi bit into the tag.
pub const ModeTag = packed struct(u16) {
    pub const Backing = @typeInfo(@This()).@"struct".backing_integer.?;
    value: u15,
    ansi: bool = false,

    pub fn fromMode(mode: Mode) ModeTag {
        return @bitCast(@intFromEnum(mode));
    }

    test "order" {
        const t: ModeTag = .{ .value = 1 };
        const int: Backing = @bitCast(t);
        try std.testing.expectEqual(@as(Backing, 1), int);
    }
};

pub fn modeFromInt(v: u16, ansi: bool) ?Mode {
    inline for (entries) |entry| {
        if (comptime !entry.disabled) {
            if (entry.value == v and entry.ansi == ansi) {
                const tag: ModeTag = .{ .ansi = ansi, .value = entry.value };
                const int: ModeTag.Backing = @bitCast(tag);
                return @enumFromInt(int);
            }
        }
    }

    return null;
}

/// A DECRPM mode report response.
pub const Report = struct {
    tag: ModeTag,
    state: State,

    pub const max_size = max_size: {
        // Construct the largest possible report in terms of values.
        const report: Report = .{
            .tag = .{
                .value = std.math.maxInt(u15),
                .ansi = false,
            },
            .state = .permanently_reset,
        };

        var discarding: std.Io.Writer.Discarding = .init(&.{});
        report.encode(&discarding.writer) catch unreachable;
        break :max_size discarding.count;
    };

    /// The state of a mode as reported in a DECRPM response.
    pub const State = enum(u8) {
        not_recognized = 0,
        set = 1,
        reset = 2,
        permanently_set = 3,
        permanently_reset = 4,
    };

    /// Encode the DECRPM report sequence.
    pub fn encode(
        self: Report,
        writer: *std.Io.Writer,
    ) std.Io.Writer.Error!void {
        try writer.print("\x1B[{s}{};{}$y", .{
            if (self.tag.ansi) "" else "?",
            self.tag.value,
            @intFromEnum(self.state),
        });
    }
};

fn entryForMode(comptime mode: Mode) ModeEntry {
    @setEvalBranchQuota(10_000);
    const name = @tagName(mode);
    for (entries) |entry| {
        if (std.mem.eql(u8, entry.name, name)) return entry;
    }

    unreachable;
}

/// A single entry of a possible mode we support. This is used to
/// dynamically define the enum and other tables.
const ModeEntry = struct {
    name: [:0]const u8,
    value: comptime_int,
    default: bool = false,

    /// True if this is an ANSI mode, false if its a DEC mode (?-prefixed).
    ansi: bool = false,

    /// If true, this mode is disabled and Ghostty will not allow it to be
    /// set or queried. The mode enum still has it, allowing Ghostty developers
    /// to develop a mode without exposing it to real users.
    disabled: bool = false,

    /// Whether an embedder may safely configure this bit as reset policy.
    /// Modes that perform a transition or mirror additional terminal state
    /// must use their semantic configuration API instead.
    default_configurable: bool = true,
};

/// Return whether a mode can safely be configured as a reset default by an
/// embedder. This excludes modes whose set/reset operations have side effects
/// beyond changing the mode bit.
pub fn defaultConfigurable(mode: Mode) bool {
    switch (mode) {
        inline else => |mode_comptime| {
            return comptime entryForMode(mode_comptime).default_configurable;
        },
    }
}

/// The full list of available entries. For documentation see how
/// they're used within Ghostty or google their values. It is not
/// valuable to redocument them all here.
///
/// NOTE: When adding a new mode entry, also add a corresponding
/// GHOSTTY_MODE_* macro in include/ghostty/vt/modes.h.
const entries: []const ModeEntry = &.{
    // ANSI
    .{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM
    .{ .name = "insert", .value = 4, .ansi = true },
    .{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM
    .{ .name = "linefeed", .value = 20, .ansi = true },

    // DEC
    .{ .name = "cursor_keys", .value = 1 }, // DECCKM
    .{ .name = "132_column", .value = 3, .default_configurable = false },
    .{ .name = "slow_scroll", .value = 4 },
    .{ .name = "reverse_colors", .value = 5 },
    .{ .name = "origin", .value = 6, .default_configurable = false },
    .{ .name = "wraparound", .value = 7, .default = true },
    .{ .name = "autorepeat", .value = 8 },
    .{ .name = "mouse_event_x10", .value = 9, .default_configurable = false },
    .{ .name = "cursor_blinking", .value = 12, .default_configurable = false },
    .{ .name = "cursor_visible", .value = 25, .default = true },
    .{ .name = "enable_mode_3", .value = 40 },
    .{ .name = "reverse_wrap", .value = 45 },
    .{ .name = "alt_screen_legacy", .value = 47, .default_configurable = false },
    .{ .name = "keypad_keys", .value = 66 },
    // DEC Backarrow Key Mode (DECBKM)
    // See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170
    // If `false` (the default), `backspace` emits 0x7f
    // If `true`, `backspace` emits 0x08
    .{ .name = "backarrow_key_mode", .value = 67 },
    .{ .name = "enable_left_and_right_margin", .value = 69, .default_configurable = false },
    .{ .name = "mouse_event_normal", .value = 1000, .default_configurable = false },
    .{ .name = "mouse_event_button", .value = 1002, .default_configurable = false },
    .{ .name = "mouse_event_any", .value = 1003, .default_configurable = false },
    .{ .name = "focus_event", .value = 1004 },
    .{ .name = "mouse_format_utf8", .value = 1005, .default_configurable = false },
    .{ .name = "mouse_format_sgr", .value = 1006, .default_configurable = false },
    .{ .name = "mouse_alternate_scroll", .value = 1007, .default = true },
    .{ .name = "mouse_format_urxvt", .value = 1015, .default_configurable = false },
    .{ .name = "mouse_format_sgr_pixels", .value = 1016, .default_configurable = false },
    .{ .name = "ignore_keypad_with_numlock", .value = 1035, .default = true },
    .{ .name = "alt_esc_prefix", .value = 1036, .default = true },
    .{ .name = "alt_sends_escape", .value = 1039 },
    .{ .name = "reverse_wrap_extended", .value = 1045 },
    .{ .name = "alt_screen", .value = 1047, .default_configurable = false },
    .{ .name = "save_cursor", .value = 1048, .default_configurable = false },
    .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049, .default_configurable = false },
    .{ .name = "bracketed_paste", .value = 2004 },
    .{ .name = "synchronized_output", .value = 2026, .default_configurable = false },
    .{ .name = "grapheme_cluster", .value = 2027 },
    .{ .name = "report_color_scheme", .value = 2031 },
    .{ .name = "report_visibility", .value = 2033, .default_configurable = false },
    .{ .name = "in_band_size_reports", .value = 2048 },
    // Kitty clipboard protocol paste events. When set, a user-initiated
    // paste sends an unsolicited OSC 5522 targets listing with a
    // one-time password instead of pasting the text.
    // See https://sw.kovidgoyal.net/kitty/clipboard/
    .{
        .name = "kitty_paste_events",
        .value = 5522,
        // The macOS app and libghostty-vt can both serve the follow-up
        // Kitty clipboard read that a paste event grants.
        .disabled = build_options.artifact != .lib and builtin.os.tag != .macos,
    },
};

test {
    _ = Mode;
    _ = ModePacked;
}

test modeFromInt {
    try testing.expect(modeFromInt(4, true).? == .insert);
    try testing.expect(modeFromInt(9, true) == null);
    try testing.expect(modeFromInt(9, false).? == .mouse_event_x10);
    try testing.expect(modeFromInt(14, true) == null);
}

test ModeState {
    var state: ModeState = .{};

    // Normal set/get
    try testing.expect(!state.get(.cursor_keys));
    state.set(.cursor_keys, true);
    try testing.expect(state.get(.cursor_keys));

    // Save/restore
    state.save(.cursor_keys);
    state.set(.cursor_keys, false);
    try testing.expect(!state.get(.cursor_keys));
    try testing.expect(state.restore(.cursor_keys));
    try testing.expect(state.get(.cursor_keys));
}

test "ModeState set default updates current and reset value" {
    var state: ModeState = .{};

    state.setDefault(.grapheme_cluster, true);
    try testing.expect(state.get(.grapheme_cluster));
    try testing.expect(state.default.grapheme_cluster);

    state.set(.grapheme_cluster, false);
    try testing.expect(!state.get(.grapheme_cluster));
    try testing.expect(state.default.grapheme_cluster);

    state.setDefault(.grapheme_cluster, true);
    try testing.expect(state.get(.grapheme_cluster));

    state.set(.grapheme_cluster, false);
    state.reset();
    try testing.expect(state.get(.grapheme_cluster));
}

test "default configurable modes" {
    try testing.expect(defaultConfigurable(.grapheme_cluster));
    try testing.expect(defaultConfigurable(.wraparound));
    try testing.expect(!defaultConfigurable(.alt_screen));
    try testing.expect(!defaultConfigurable(.cursor_blinking));
}

test "getReport known DEC mode" {
    var state: ModeState = .{};
    const report = state.getReport(.{ .value = 1 });
    try testing.expectEqual(Report.State.reset, report.state);
    try testing.expectEqual(false, report.tag.ansi);
    try testing.expectEqual(@as(u15, 1), report.tag.value);

    state.set(.cursor_keys, true);
    const report2 = state.getReport(.{ .value = 1 });
    try testing.expectEqual(Report.State.set, report2.state);
}

test "getReport known ANSI mode" {
    var state: ModeState = .{};
    state.set(.insert, true);
    const report = state.getReport(.{ .value = 4, .ansi = true });
    try testing.expectEqual(Report.State.set, report.state);
    try testing.expectEqual(true, report.tag.ansi);
}

test "getReport DECECM permanently reset" {
    const state: ModeState = .{};
    const report = state.getReport(.{ .value = 117, .ansi = false });
    try testing.expectEqual(Report.State.permanently_reset, report.state);
    try testing.expectEqual(false, report.tag.ansi);
}

test "getReport unknown mode" {
    const state: ModeState = .{};
    const report = state.getReport(.{ .value = 9999 });
    try testing.expectEqual(Report.State.not_recognized, report.state);
}

test "Report.encode DEC mode set" {
    var buf: [Report.max_size]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buf);
    const report: Report = .{ .tag = .{ .value = 1, .ansi = false }, .state = .set };
    try report.encode(&writer);
    try testing.expectEqualStrings("\x1B[?1;1$y", writer.buffered());
}

test "Report.encode DEC mode reset" {
    var buf: [Report.max_size]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buf);
    const report: Report = .{ .tag = .{ .value = 1, .ansi = false }, .state = .reset };
    try report.encode(&writer);
    try testing.expectEqualStrings("\x1B[?1;2$y", writer.buffered());
}

test "Report.encode ANSI mode" {
    var buf: [Report.max_size]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buf);
    const report: Report = .{ .tag = .{ .value = 4, .ansi = true }, .state = .set };
    try report.encode(&writer);
    try testing.expectEqualStrings("\x1B[4;1$y", writer.buffered());
}

test "Report.encode not recognized" {
    var buf: [Report.max_size]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buf);
    const report: Report = .{ .tag = .{ .value = 9999, .ansi = false }, .state = .not_recognized };
    try report.encode(&writer);
    try testing.expectEqualStrings("\x1B[?9999;0$y", writer.buffered());
}
