//! A virtual terminal widget
const Terminal = @This();

const std = @import("std");
const builtin = @import("builtin");
const ansi = @import("ansi.zig");
pub const Command = @import("Command.zig");
const Parser = @import("Parser.zig");
const Pty = @import("Pty.zig");
const vaxis = @import("../../main.zig");
const Winsize = vaxis.Winsize;
const Screen = @import("Screen.zig");
const Key = vaxis.Key;
const Queue = vaxis.Queue(Event, 16);
const key = @import("key.zig");

pub const Event = union(enum) {
    exited,
    redraw,
    bell,
    title_change: []const u8,
    pwd_change: []const u8,
};

const posix = std.posix;

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

pub const Options = struct {
    scrollback_size: u16 = 500,
    winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
    initial_working_directory: ?[]const u8 = null,
};

pub const Mode = struct {
    origin: bool = false,
    autowrap: bool = true,
    cursor: bool = true,
    sync: bool = false,
};

pub const InputEvent = union(enum) {
    key_press: vaxis.Key,
};

pub var global_io: std.Io = undefined;
pub var global_io_initialized: bool = false;
pub var global_vt_mutex: std.Io.Mutex = .init;
pub var global_vts: std.AutoHashMapUnmanaged(i32, *Terminal) = .empty;
pub var global_sigchild_installed: bool = false;

io: std.Io,
allocator: std.mem.Allocator,
scrollback_size: u16,

pty: Pty,
pty_writer: std.Io.File.Writer,
cmd: Command,
thread: ?std.Io.Future(void) = null,

/// the screen we draw from
front_screen: Screen,
front_mutex: std.Io.Mutex = .init,

/// the back screens
back_screen: *Screen = undefined,
back_screen_pri: Screen,
back_screen_alt: Screen,
// only applies to primary screen
scroll_offset: usize = 0,
back_mutex: std.Io.Mutex = .init,
// dirty is protected by back_mutex. Only access this field when you hold that mutex
dirty: bool = false,

should_quit: bool = false,

mode: Mode = .{},

tab_stops: std.ArrayList(u16),
title: std.ArrayList(u8) = .empty,
working_directory: std.ArrayList(u8) = .empty,

last_printed: []const u8 = "",

event_queue: Queue,

/// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the
/// screen
pub fn init(
    io: std.Io,
    allocator: std.mem.Allocator,
    argv: []const []const u8,
    env: *const std.process.Environ.Map,
    opts: Options,
    write_buf: []u8,
) !Terminal {
    if (!global_io_initialized) {
        global_io = io;
        global_io_initialized = true;
    }
    // Verify we have an absolute path
    if (opts.initial_working_directory) |pwd| {
        if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory;
    }
    const pty = try Pty.init(io);
    try pty.setSize(opts.winsize);
    const cmd: Command = .{
        .argv = argv,
        .env_map = env,
        .pty = pty,
        .working_directory = opts.initial_working_directory,
    };
    var tabs: std.ArrayList(u16) = try .initCapacity(allocator, opts.winsize.cols / 8);
    var col: u16 = 0;
    while (col < opts.winsize.cols) : (col += 8) {
        try tabs.append(allocator, col);
    }
    return .{
        .io = io,
        .allocator = allocator,
        .pty = pty,
        .pty_writer = pty.pty.writerStreaming(io, write_buf),
        .cmd = cmd,
        .scrollback_size = opts.scrollback_size,
        .front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
        .back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
        .back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
        .tab_stops = tabs,
        .event_queue = .init(io),
    };
}

/// release all resources of the Terminal
pub fn deinit(self: *Terminal) void {
    self.should_quit = true;

    pid: {
        global_vt_mutex.lock(self.io) catch break :pid;
        defer global_vt_mutex.unlock(self.io);
        if (self.cmd.pid) |pid|
            _ = global_vts.remove(pid);
        if (global_vts.count() == 0) {
            global_vts.deinit(self.allocator);
        }
    }
    self.cmd.kill();
    if (self.thread) |*thread| {
        // write an EOT into the tty to trigger a read on our thread
        const EOT = "\x04";
        self.pty.tty.writeStreamingAll(self.io, EOT) catch {};
        thread.await(self.io);
        self.thread = null;
    }
    self.pty.deinit(self.io);
    self.front_screen.deinit(self.allocator);
    self.back_screen_pri.deinit(self.allocator);
    self.back_screen_alt.deinit(self.allocator);
    self.tab_stops.deinit(self.allocator);
    self.title.deinit(self.allocator);
    self.working_directory.deinit(self.allocator);
}

pub fn spawn(self: *Terminal) !void {
    if (self.thread != null) return;
    self.back_screen = &self.back_screen_pri;

    try self.cmd.spawn(self.io, self.allocator);

    self.working_directory.clearRetainingCapacity();
    if (self.cmd.working_directory) |pwd| {
        try self.working_directory.appendSlice(self.allocator, pwd);
    } else {
        const pwd: std.Io.Dir = .cwd();
        const out_path = try pwd.realPathFileAlloc(self.io, ".", self.allocator);
        try self.working_directory.appendSlice(self.allocator, out_path);
    }

    {
        // add to our global list
        try global_vt_mutex.lock(self.io);
        defer global_vt_mutex.unlock(self.io);
        if (self.cmd.pid) |pid|
            try global_vts.put(self.allocator, pid, self);
    }

    self.thread = try self.io.concurrent(Terminal.run, .{self});
}

/// resize the screen. Locks access to the back screen. Should only be called from the main thread.
/// This is safe to call every render cycle: there is a guard to only perform a resize if the size
/// of the window has changed.
pub fn resize(self: *Terminal, ws: Winsize) !void {
    // don't deinit with no size change
    if (ws.cols == self.front_screen.width and
        ws.rows == self.front_screen.height)
        return;

    try self.back_mutex.lock(self.io);
    defer self.back_mutex.unlock(self.io);

    self.front_screen.deinit(self.allocator);
    self.front_screen = try Screen.init(self.allocator, ws.cols, ws.rows);

    self.back_screen_pri.deinit(self.allocator);
    self.back_screen_alt.deinit(self.allocator);
    self.back_screen_pri = try Screen.init(self.allocator, ws.cols, ws.rows + self.scrollback_size);
    self.back_screen_alt = try Screen.init(self.allocator, ws.cols, ws.rows);

    try self.pty.setSize(ws);
}

pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void {
    if (self.back_mutex.tryLock()) {
        defer self.back_mutex.unlock(self.io);
        // We keep this as a separate condition so we don't deadlock by obtaining the lock but not
        // having sync
        if (!self.mode.sync) {
            try self.back_screen.copyTo(allocator, &self.front_screen);
            self.dirty = false;
        }
    }

    var row: u16 = 0;
    while (row < self.front_screen.height) : (row += 1) {
        var col: u16 = 0;
        while (col < self.front_screen.width) {
            const cell = self.front_screen.readCell(col, row) orelse continue;
            win.writeCell(col, row, cell);
            col += @max(cell.char.width, 1);
        }
    }

    if (self.mode.cursor) {
        win.setCursorShape(self.front_screen.cursor.shape);
        win.showCursor(self.front_screen.cursor.col, self.front_screen.cursor.row);
    }
}

pub fn tryEvent(self: *Terminal) !?Event {
    return try self.event_queue.tryPop();
}

pub fn update(self: *Terminal, event: InputEvent) !void {
    switch (event) {
        .key_press => |k| {
            const pty_writer = self.get_pty_writer();
            defer pty_writer.flush() catch {};
            try key.encode(pty_writer, k, true, self.back_screen.csi_u_flags);
        },
    }
}

pub fn get_pty_writer(self: *Terminal) *std.Io.Writer {
    return &self.pty_writer.interface;
}

fn reader(self: *const Terminal, buf: []u8) std.Io.File.Reader {
    return self.pty.pty.readerStreaming(self.io, buf);
}

/// process the output from the command on the pty
fn run(self: *Terminal) void {
    self._run() catch {};
}

fn _run(self: *Terminal) !void {
    var parser: Parser = .{
        .buf = try .initCapacity(self.allocator, 128),
    };
    defer parser.buf.deinit();

    var reader_buf: [4096]u8 = undefined;
    var reader_ = self.reader(&reader_buf);

    while (!self.should_quit) {
        const event = try parser.parseReader(&reader_.interface);
        try self.back_mutex.lock(self.io);
        defer self.back_mutex.unlock(self.io);

        if (!self.dirty and try self.event_queue.tryPush(.redraw))
            self.dirty = true;

        switch (event) {
            .print => |str| {
                var iter = vaxis.unicode.graphemeIterator(str);
                while (iter.next()) |grapheme| {
                    const gr = grapheme.bytes(str);
                    // TODO: use actual instead of .unicode
                    const w = vaxis.gwidth.gwidth(gr, .unicode);
                    try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
                }
            },
            .c0 => |b| try self.handleC0(b),
            .escape => |esc| {
                const final = esc[esc.len - 1];
                switch (final) {
                    'B' => {}, // TODO: handle charsets
                    // Index
                    'D' => try self.back_screen.index(),
                    // Next Line
                    'E' => {
                        try self.back_screen.index();
                        self.carriageReturn();
                    },
                    // Horizontal Tab Set
                    'H' => {
                        const already_set: bool = for (self.tab_stops.items) |ts| {
                            if (ts == self.back_screen.cursor.col) break true;
                        } else false;
                        if (already_set) continue;
                        try self.tab_stops.append(self.allocator, @truncate(self.back_screen.cursor.col));
                        std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16));
                    },
                    // Reverse Index
                    'M' => try self.back_screen.reverseIndex(),
                    else => log.info("unhandled escape: {s}", .{esc}),
                }
            },
            .ss2 => |ss2| log.info("unhandled ss2: {c}", .{ss2}),
            .ss3 => |ss3| log.info("unhandled ss3: {c}", .{ss3}),
            .csi => |seq| {
                switch (seq.final) {
                    // Cursor up
                    'A', 'k' => {
                        var iter = seq.iterator(u16);
                        const delta = iter.next() orelse 1;
                        self.back_screen.cursorUp(delta);
                    },
                    // Cursor Down
                    'B' => {
                        var iter = seq.iterator(u16);
                        const delta = iter.next() orelse 1;
                        self.back_screen.cursorDown(delta);
                    },
                    // Cursor Right
                    'C' => {
                        var iter = seq.iterator(u16);
                        const delta = iter.next() orelse 1;
                        self.back_screen.cursorRight(delta);
                    },
                    // Cursor Left
                    'D', 'j' => {
                        var iter = seq.iterator(u16);
                        const delta = iter.next() orelse 1;
                        self.back_screen.cursorLeft(delta);
                    },
                    // Cursor Next Line
                    'E' => {
                        var iter = seq.iterator(u16);
                        const delta = iter.next() orelse 1;
                        self.back_screen.cursorDown(delta);
                        self.carriageReturn();
                    },
                    // Cursor Previous Line
                    'F' => {
                        var iter = seq.iterator(u16);
                        const delta = iter.next() orelse 1;
                        self.back_screen.cursorUp(delta);
                        self.carriageReturn();
                    },
                    // Horizontal Position Absolute
                    'G', '`' => {
                        var iter = seq.iterator(u16);
                        const col = iter.next() orelse 1;
                        self.back_screen.cursor.col = col -| 1;
                        if (self.back_screen.cursor.col < self.back_screen.scrolling_region.left)
                            self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
                        if (self.back_screen.cursor.col > self.back_screen.scrolling_region.right)
                            self.back_screen.cursor.col = self.back_screen.scrolling_region.right;
                        self.back_screen.cursor.pending_wrap = false;
                    },
                    // Cursor Absolute Position
                    'H', 'f' => {
                        var iter = seq.iterator(u16);
                        const row = iter.next() orelse 1;
                        const col = iter.next() orelse 1;
                        self.back_screen.cursor.col = col -| 1;
                        self.back_screen.cursor.row = row -| 1;
                        self.back_screen.cursor.pending_wrap = false;
                    },
                    // Cursor Horizontal Tab
                    'I' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        self.horizontalTab(n);
                    },
                    // Erase In Display
                    'J' => {
                        // TODO: selective erase (private_marker == '?')
                        var iter = seq.iterator(u16);
                        const kind = iter.next() orelse 0;
                        switch (kind) {
                            0 => self.back_screen.eraseBelow(),
                            1 => self.back_screen.eraseAbove(),
                            2 => self.back_screen.eraseAll(),
                            3 => {},
                            else => {},
                        }
                    },
                    // Erase in Line
                    'K' => {
                        // TODO: selective erase (private_marker == '?')
                        var iter = seq.iterator(u8);
                        const ps = iter.next() orelse 0;
                        switch (ps) {
                            0 => self.back_screen.eraseRight(),
                            1 => self.back_screen.eraseLeft(),
                            2 => self.back_screen.eraseLine(),
                            else => continue,
                        }
                    },
                    // Insert Lines
                    'L' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        try self.back_screen.insertLine(n);
                    },
                    // Delete Lines
                    'M' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        try self.back_screen.deleteLine(n);
                    },
                    // Delete Character
                    'P' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        try self.back_screen.deleteCharacters(n);
                    },
                    // Scroll Up
                    'S' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        const cur_row = self.back_screen.cursor.row;
                        const cur_col = self.back_screen.cursor.col;
                        const wrap = self.back_screen.cursor.pending_wrap;
                        defer {
                            self.back_screen.cursor.row = cur_row;
                            self.back_screen.cursor.col = cur_col;
                            self.back_screen.cursor.pending_wrap = wrap;
                        }
                        self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
                        self.back_screen.cursor.row = self.back_screen.scrolling_region.top;
                        try self.back_screen.deleteLine(n);
                    },
                    // Scroll Down
                    'T' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        try self.back_screen.scrollDown(n);
                    },
                    // Tab Control
                    'W' => {
                        if (seq.private_marker) |pm| {
                            if (pm != '?') continue;
                            var iter = seq.iterator(u16);
                            const n = iter.next() orelse continue;
                            if (n != 5) continue;
                            self.tab_stops.clearRetainingCapacity();
                            var col: u16 = 0;
                            while (col < self.back_screen.width) : (col += 8) {
                                try self.tab_stops.append(self.allocator, col);
                            }
                        }
                    },
                    'X' => {
                        self.back_screen.cursor.pending_wrap = false;
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        const start = self.back_screen.cursor.row * self.back_screen.width + self.back_screen.cursor.col;
                        const end = @max(
                            self.back_screen.cursor.row * self.back_screen.width + self.back_screen.width,
                            n,
                            1, // In case n == 0
                        );
                        var i: usize = start;
                        while (i < end) : (i += 1) {
                            self.back_screen.buf[i].erase(self.allocator, self.back_screen.cursor.style.bg);
                        }
                    },
                    'Z' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        self.horizontalBackTab(n);
                    },
                    // Cursor Horizontal Position Relative
                    'a' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        self.back_screen.cursor.pending_wrap = false;
                        const max_end = if (self.mode.origin)
                            self.back_screen.scrolling_region.right
                        else
                            self.back_screen.width - 1;
                        self.back_screen.cursor.col = @min(
                            self.back_screen.cursor.col + max_end,
                            self.back_screen.cursor.col + n,
                        );
                    },
                    // Repeat Previous Character
                    'b' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        // TODO: maybe not .unicode
                        const w = vaxis.gwidth.gwidth(self.last_printed, .unicode);
                        var i: usize = 0;
                        while (i < n) : (i += 1) {
                            try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);
                        }
                    },
                    // Device Attributes
                    'c' => {
                        const pty_writer = self.get_pty_writer();
                        defer pty_writer.flush() catch {};
                        if (seq.private_marker) |pm| {
                            switch (pm) {
                                // Secondary
                                '>' => try pty_writer.writeAll("\x1B[>1;69;0c"),
                                '=' => try pty_writer.writeAll("\x1B[=0000c"),
                                else => log.info("unhandled CSI: {f}", .{seq}),
                            }
                        } else {
                            // Primary
                            try pty_writer.writeAll("\x1B[?62;22c");
                        }
                    },
                    // Cursor Vertical Position Absolute
                    'd' => {
                        self.back_screen.cursor.pending_wrap = false;
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        const max = if (self.mode.origin)
                            self.back_screen.scrolling_region.bottom
                        else
                            self.back_screen.height -| 1;
                        self.back_screen.cursor.pending_wrap = false;
                        self.back_screen.cursor.row = @min(
                            max,
                            n -| 1,
                        );
                    },
                    // Cursor Vertical Position Absolute
                    'e' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 1;
                        self.back_screen.cursor.pending_wrap = false;
                        self.back_screen.cursor.row = @min(
                            self.back_screen.width -| 1,
                            n -| 1,
                        );
                    },
                    // Tab Clear
                    'g' => {
                        var iter = seq.iterator(u16);
                        const n = iter.next() orelse 0;
                        switch (n) {
                            0 => {
                                const current = try self.tab_stops.toOwnedSlice(self.allocator);
                                defer self.allocator.free(current);
                                self.tab_stops.clearRetainingCapacity();
                                for (current) |stop| {
                                    if (stop == self.back_screen.cursor.col) continue;
                                    try self.tab_stops.append(self.allocator, stop);
                                }
                            },
                            3 => self.tab_stops.clearAndFree(self.allocator),
                            else => log.info("unhandled CSI: {f}", .{seq}),
                        }
                    },
                    'h', 'l' => {
                        var iter = seq.iterator(u16);
                        const mode = iter.next() orelse continue;
                        // There is only one collision (mode = 4), and we don't support the private
                        // version of it
                        if (seq.private_marker != null and mode == 4) continue;
                        self.setMode(mode, seq.final == 'h');
                    },
                    'm' => {
                        if (seq.intermediate == null and seq.private_marker == null) {
                            self.back_screen.sgr(seq);
                        }
                        // TODO: private marker and intermediates
                    },
                    'n' => {
                        var iter = seq.iterator(u16);
                        const ps = iter.next() orelse 0;
                        if (seq.intermediate == null and seq.private_marker == null) {
                            const pty_writer = self.get_pty_writer();
                            defer pty_writer.flush() catch {};
                            switch (ps) {
                                5 => try pty_writer.writeAll("\x1b[0n"),
                                6 => try pty_writer.print("\x1b[{d};{d}R", .{
                                    self.back_screen.cursor.row + 1,
                                    self.back_screen.cursor.col + 1,
                                }),
                                else => log.info("unhandled CSI: {f}", .{seq}),
                            }
                        }
                    },
                    'p' => {
                        var iter = seq.iterator(u16);
                        const ps = iter.next() orelse 0;
                        if (seq.intermediate) |int| {
                            switch (int) {
                                // report mode
                                '$' => {
                                    const pty_writer = self.get_pty_writer();
                                    defer pty_writer.flush() catch {};
                                    switch (ps) {
                                        2026 => try pty_writer.writeAll("\x1b[?2026;2$p"),
                                        else => {
                                            std.log.warn("unhandled mode: {}", .{ps});
                                            try pty_writer.print("\x1b[?{d};0$p", .{ps});
                                        },
                                    }
                                },
                                else => log.info("unhandled CSI: {f}", .{seq}),
                            }
                        }
                    },
                    'q' => {
                        if (seq.intermediate) |int| {
                            switch (int) {
                                ' ' => {
                                    var iter = seq.iterator(u8);
                                    const shape = iter.next() orelse 0;
                                    self.back_screen.cursor.shape = @enumFromInt(shape);
                                },
                                else => {},
                            }
                        }
                        if (seq.private_marker) |pm| {
                            const pty_writer = self.get_pty_writer();
                            defer pty_writer.flush() catch {};
                            switch (pm) {
                                // XTVERSION
                                '>' => try pty_writer.print(
                                    "\x1bP>|libvaxis {s}\x1B\\",
                                    .{"dev"},
                                ),
                                else => log.info("unhandled CSI: {f}", .{seq}),
                            }
                        }
                    },
                    'r' => {
                        if (seq.intermediate) |_| {
                            // TODO: XTRESTORE
                            continue;
                        }
                        if (seq.private_marker) |_| {
                            // TODO: DECCARA
                            continue;
                        }
                        // DECSTBM
                        var iter = seq.iterator(u16);
                        const top = iter.next() orelse 1;
                        const bottom = iter.next() orelse self.back_screen.height;
                        self.back_screen.scrolling_region.top = top -| 1;
                        self.back_screen.scrolling_region.bottom = bottom -| 1;
                        self.back_screen.cursor.pending_wrap = false;
                        if (self.mode.origin) {
                            self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
                            self.back_screen.cursor.row = self.back_screen.scrolling_region.top;
                        } else {
                            self.back_screen.cursor.col = 0;
                            self.back_screen.cursor.row = 0;
                        }
                    },
                    else => log.info("unhandled CSI: {f}", .{seq}),
                }
            },
            .osc => |osc| {
                const semicolon = std.mem.indexOfScalar(u8, osc, ';') orelse {
                    log.info("unhandled osc: {s}", .{osc});
                    continue;
                };
                const ps = std.fmt.parseUnsigned(u8, osc[0..semicolon], 10) catch {
                    log.info("unhandled osc: {s}", .{osc});
                    continue;
                };
                switch (ps) {
                    0 => {
                        self.title.clearRetainingCapacity();
                        try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]);
                        try self.event_queue.push(.{ .title_change = self.title.items });
                    },
                    7 => {
                        // OSC 7 ; file:// <hostname> <pwd>
                        log.err("osc: {s}", .{osc});
                        self.working_directory.clearRetainingCapacity();
                        const scheme = "file://";
                        const start = std.mem.indexOfScalarPos(u8, osc, semicolon + 2 + scheme.len + 1, '/') orelse {
                            log.info("unknown OSC 7 format: {s}", .{osc});
                            continue;
                        };
                        const enc = osc[start..];
                        var i: usize = 0;
                        while (i < enc.len) : (i += 1) {
                            const b = if (enc[i] == '%') blk: {
                                defer i += 2;
                                break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16);
                            } else enc[i];
                            try self.working_directory.append(self.allocator, b);
                        }
                        try self.event_queue.push(.{ .pwd_change = self.working_directory.items });
                    },
                    else => log.info("unhandled osc: {s}", .{osc}),
                }
            },
            .apc => |apc| log.info("unhandled apc: {s}", .{apc}),
        }
    }
}

inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
    switch (b) {
        .NUL, .SOH, .STX => {},
        .EOT => {}, // we send EOT to quit the read thread
        .ENQ => {},
        .BEL => try self.event_queue.push(.bell),
        .BS => self.back_screen.cursorLeft(1),
        .HT => self.horizontalTab(1),
        .LF, .VT, .FF => try self.back_screen.index(),
        .CR => self.carriageReturn(),
        .SO => {}, // TODO: Charset shift out
        .SI => {}, // TODO: Charset shift in
        else => log.warn("unhandled C0: 0x{x}", .{@intFromEnum(b)}),
    }
}

pub fn setMode(self: *Terminal, mode: u16, val: bool) void {
    switch (mode) {
        7 => self.mode.autowrap = val,
        25 => self.mode.cursor = val,
        1049 => {
            if (val)
                self.back_screen = &self.back_screen_alt
            else
                self.back_screen = &self.back_screen_pri;
            var i: usize = 0;
            while (i < self.back_screen.buf.len) : (i += 1) {
                self.back_screen.buf[i].dirty = true;
            }
        },
        2026 => self.mode.sync = val,
        else => return,
    }
}

pub fn carriageReturn(self: *Terminal) void {
    self.back_screen.cursor.pending_wrap = false;
    self.back_screen.cursor.col = if (self.mode.origin)
        self.back_screen.scrolling_region.left
    else if (self.back_screen.cursor.col >= self.back_screen.scrolling_region.left)
        self.back_screen.scrolling_region.left
    else
        0;
}

pub fn horizontalTab(self: *Terminal, n: usize) void {
    // Get the current cursor position
    const col = self.back_screen.cursor.col;

    // Find desired final position
    var i: usize = 0;
    const final = for (self.tab_stops.items) |ts| {
        if (ts <= col) continue;
        i += 1;
        if (i == n) break ts;
    } else self.back_screen.width - 1;

    // Move right the delta
    self.back_screen.cursorRight(final -| col);
}

pub fn horizontalBackTab(self: *Terminal, n: usize) void {
    // Get the current cursor position
    const col = self.back_screen.cursor.col;

    // Find the index of the next backtab
    const idx = for (self.tab_stops.items, 0..) |ts, i| {
        if (ts <= col) continue;
        break i;
    } else self.tab_stops.items.len - 1;

    const final = if (self.mode.origin)
        @max(self.tab_stops.items[idx -| (n -| 1)], self.back_screen.scrolling_region.left)
    else
        self.tab_stops.items[idx -| (n -| 1)];

    // Move left the delta
    self.back_screen.cursorLeft(final - col);
}
