const std = @import("std");
const builtin = @import("builtin");

const vaxis = @import("main.zig");

const ctlseqs = vaxis.ctlseqs;
const posix = std.posix;
const windows = std.os.windows;

const Event = vaxis.Event;
const Key = vaxis.Key;
const Mouse = vaxis.Mouse;
const Parser = vaxis.Parser;
const Winsize = vaxis.Winsize;

/// The target TTY implementation
pub const Tty = if (builtin.is_test)
    TestTty
else switch (builtin.os.tag) {
    .windows => WindowsTty,
    else => PosixTty,
};

/// global tty instance, used in case of a panic. Not guaranteed to work if
/// for some reason there are multiple TTYs open under a single vaxis
/// compilation unit - but this is better than nothing
pub var global_tty: ?Tty = null;

pub const PosixTty = struct {
    io: std.Io,

    /// the original state of the terminal, prior to calling makeRaw
    termios: posix.termios,

    /// The file descriptor of the tty
    fd: std.Io.File,

    /// File.Writer for efficient buffered writing
    tty_writer: std.Io.File.Writer,

    pub const SignalHandler = struct {
        context: *anyopaque,
        callback: *const fn (context: *anyopaque) void,
    };

    /// global signal handlers
    var handlers: [8]SignalHandler = undefined;
    var handler_io: std.Io = undefined;
    var handler_mutex: std.Io.Mutex = .init;
    var handler_idx: usize = 0;

    var handler_installed: bool = false;

    /// initializes a Tty instance by opening /dev/tty and "making it raw". A
    /// signal handler is installed for SIGWINCH. No callbacks are installed, be
    /// sure to register a callback when initializing the event loop
    pub fn init(io: std.Io, buffer: []u8) !PosixTty {
        // Open our tty
        var f = try std.Io.Dir.openFileAbsolute(io, "/dev/tty", .{ .mode = .read_write });

        // Set the termios of the tty
        const termios = try makeRaw(f.handle);

        if (!handler_installed) {
            var act = posix.Sigaction{
                .handler = .{ .handler = PosixTty.handleWinch },
                .mask = switch (builtin.os.tag) {
                    .macos => 0,
                    else => posix.sigemptyset(),
                },
                .flags = 0,
            };
            posix.sigaction(posix.SIG.WINCH, &act, null);
            handler_io = io;
            handler_installed = true;
        }

        const self: PosixTty = .{
            .io = io,
            .fd = f,
            .termios = termios,
            .tty_writer = f.writerStreaming(io, buffer),
        };

        global_tty = self;

        return self;
    }

    /// release resources associated with the Tty return it to its original state
    pub fn deinit(self: PosixTty) void {
        posix.tcsetattr(self.fd.handle, .FLUSH, self.termios) catch |err| {
            std.log.err("couldn't restore terminal: {}", .{err});
        };
        if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos
            self.fd.close(self.io);
    }

    /// Resets the signal handler to it's default
    pub fn resetSignalHandler() void {
        if (!handler_installed) return;
        handler_installed = false;
        var act = posix.Sigaction{
            .handler = .{ .handler = posix.SIG.DFL },
            .mask = switch (builtin.os.tag) {
                .macos => 0,
                else => posix.sigemptyset(),
            },
            .flags = 0,
        };
        posix.sigaction(posix.SIG.WINCH, &act, null);
    }

    pub fn writer(self: *PosixTty) *std.Io.Writer {
        return &self.tty_writer.interface;
    }

    pub fn read(self: *const PosixTty, buf: []u8) !usize {
        return try self.fd.readStreaming(self.io, &.{buf});
    }

    /// Install a signal handler for winsize. A maximum of 8 handlers may be
    /// installed
    pub fn notifyWinsize(handler: SignalHandler) !void {
        try handler_mutex.lock(handler_io);
        defer handler_mutex.unlock(handler_io);
        if (handler_idx == handlers.len) return error.OutOfMemory;
        handlers[handler_idx] = handler;
        handler_idx += 1;
    }

    fn handleWinch(_: std.posix.SIG) callconv(.c) void {
        handler_mutex.lock(handler_io) catch @panic("unable to lock SIGWINCH handlers");
        defer handler_mutex.unlock(handler_io);
        var i: usize = 0;
        while (i < handler_idx) : (i += 1) {
            const handler = handlers[i];
            handler.callback(handler.context);
        }
    }

    /// makeRaw enters the raw state for the terminal.
    pub fn makeRaw(fd: posix.fd_t) !posix.termios {
        const state = try posix.tcgetattr(fd);
        var raw = state;
        // see termios(3)
        raw.iflag.IGNBRK = false;
        raw.iflag.BRKINT = false;
        raw.iflag.PARMRK = false;
        raw.iflag.ISTRIP = false;
        raw.iflag.INLCR = false;
        raw.iflag.IGNCR = false;
        raw.iflag.ICRNL = false;
        raw.iflag.IXON = false;

        raw.oflag.OPOST = false;

        raw.lflag.ECHO = false;
        raw.lflag.ECHONL = false;
        raw.lflag.ICANON = false;
        raw.lflag.ISIG = false;
        raw.lflag.IEXTEN = false;

        raw.cflag.CSIZE = .CS8;
        raw.cflag.PARENB = false;

        raw.cc[@intFromEnum(posix.V.MIN)] = 1;
        raw.cc[@intFromEnum(posix.V.TIME)] = 0;
        try posix.tcsetattr(fd, .FLUSH, raw);
        return state;
    }

    /// Get the window size from the kernel
    pub fn getWinsize(self: *PosixTty) !Winsize {
        var winsize = posix.winsize{
            .row = 0,
            .col = 0,
            .xpixel = 0,
            .ypixel = 0,
        };

        const err = posix.system.ioctl(self.fd.handle, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
        if (posix.errno(err) == .SUCCESS)
            return Winsize{
                .rows = winsize.row,
                .cols = winsize.col,
                .x_pixel = winsize.xpixel,
                .y_pixel = winsize.ypixel,
            };
        return error.IoctlError;
    }
};

pub const WindowsTty = struct {
    stdin: windows.HANDLE,
    stdout: windows.HANDLE,

    initial_codepage: c_uint,
    initial_input_mode: CONSOLE_MODE_INPUT,
    initial_output_mode: CONSOLE_MODE_OUTPUT,

    // a buffer to write key text into
    buf: [4]u8 = undefined,

    /// File.Writer for efficient buffered writing
    tty_writer: std.Io.File.Writer,

    /// The last mouse button that was pressed. We store the previous state of button presses on each
    /// mouse event so we can detect which button was released
    last_mouse_button_press: u16 = 0,

    const utf8_codepage: c_uint = 65001;

    /// The input mode set by init
    pub const input_raw_mode: CONSOLE_MODE_INPUT = .{
        .WINDOW_INPUT = 1, // resize events
        .MOUSE_INPUT = 1,
        .EXTENDED_FLAGS = 1, // allow mouse events
    };

    /// The output mode set by init
    pub const output_raw_mode: CONSOLE_MODE_OUTPUT = .{
        .PROCESSED_OUTPUT = 1, // handle control sequences
        .VIRTUAL_TERMINAL_PROCESSING = 1, // handle ANSI sequences
        .DISABLE_NEWLINE_AUTO_RETURN = 1, // disable inserting a new line when we write at the last column
        .ENABLE_LVB_GRID_WORLDWIDE = 1, // enables reverse video and underline
    };

    pub fn init(io: std.Io, buffer: []u8) !WindowsTty {
        const stdin: std.Io.File = .stdin();
        const stdout: std.Io.File = .stdout();

        // get initial modes
        const initial_output_codepage = GetConsoleOutputCP();
        const initial_input_mode = try getConsoleMode(CONSOLE_MODE_INPUT, stdin.handle);
        const initial_output_mode = try getConsoleMode(CONSOLE_MODE_OUTPUT, stdout.handle);

        // set new modes
        try setConsoleMode(stdin.handle, input_raw_mode);
        try setConsoleMode(stdout.handle, output_raw_mode);
        if (SetConsoleOutputCP(utf8_codepage) == .FALSE)
            return windows.unexpectedError(windows.GetLastError());

        const self: WindowsTty = .{
            .stdin = stdin.handle,
            .stdout = stdout.handle,
            .initial_codepage = initial_output_codepage,
            .initial_input_mode = initial_input_mode,
            .initial_output_mode = initial_output_mode,
            .tty_writer = .initStreaming(stdout, io, buffer),
        };

        // save a copy of this tty as the global_tty for panic handling
        global_tty = self;

        return self;
    }

    pub fn deinit(self: WindowsTty) void {
        _ = SetConsoleOutputCP(self.initial_codepage);
        setConsoleMode(self.stdin, self.initial_input_mode) catch {};
        setConsoleMode(self.stdout, self.initial_output_mode) catch {};
        windows.CloseHandle(self.stdin);
        windows.CloseHandle(self.stdout);
    }

    pub const CONSOLE_MODE_INPUT = packed struct(u32) {
        PROCESSED_INPUT: u1 = 0,
        LINE_INPUT: u1 = 0,
        ECHO_INPUT: u1 = 0,
        WINDOW_INPUT: u1 = 0,
        MOUSE_INPUT: u1 = 0,
        INSERT_MODE: u1 = 0,
        QUICK_EDIT_MODE: u1 = 0,
        EXTENDED_FLAGS: u1 = 0,
        AUTO_POSITION: u1 = 0,
        VIRTUAL_TERMINAL_INPUT: u1 = 0,
        _: u22 = 0,
    };

    pub const CONSOLE_MODE_OUTPUT = packed struct(u32) {
        PROCESSED_OUTPUT: u1 = 0,
        WRAP_AT_EOL_OUTPUT: u1 = 0,
        VIRTUAL_TERMINAL_PROCESSING: u1 = 0,
        DISABLE_NEWLINE_AUTO_RETURN: u1 = 0,
        ENABLE_LVB_GRID_WORLDWIDE: u1 = 0,
        _: u27 = 0,
    };

    pub fn getConsoleMode(comptime T: type, handle: windows.HANDLE) !T {
        var mode: u32 = undefined;
        if (GetConsoleMode(handle, &mode) == .FALSE) return switch (windows.GetLastError()) {
            .INVALID_HANDLE => error.InvalidHandle,
            else => |e| windows.unexpectedError(e),
        };
        return @bitCast(mode);
    }

    pub fn setConsoleMode(handle: windows.HANDLE, mode: anytype) !void {
        if (SetConsoleMode(handle, @bitCast(mode)) == .FALSE) return switch (windows.GetLastError()) {
            .INVALID_HANDLE => error.InvalidHandle,
            else => |e| windows.unexpectedError(e),
        };
    }

    pub fn writer(self: *WindowsTty) *std.Io.Writer {
        return &self.tty_writer.interface;
    }

    pub fn read(self: *const WindowsTty, buf: []u8) !usize {
        return posix.read(self.fd, buf);
    }

    pub fn nextEvent(self: *WindowsTty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
        // We use a loop so we can ignore certain events
        var state: EventState = .{};
        while (true) {
            var event_count: u32 = 0;
            var input_record: INPUT_RECORD = undefined;
            if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == .FALSE)
                return windows.unexpectedError(windows.GetLastError());

            if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
                return ev;
            }
        }
    }

    pub const EventState = struct {
        ansi_buf: [128]u8 = undefined,
        ansi_idx: usize = 0,
        utf16_buf: [2]u16 = undefined,
        utf16_half: bool = false,
    };

    pub const SMALL_RECT = extern struct {
        Left: windows.SHORT,
        Top: windows.SHORT,
        Right: windows.SHORT,
        Bottom: windows.SHORT,
    };

    pub const COORD = extern struct {
        X: windows.SHORT,
        Y: windows.SHORT,
    };

    pub const CONSOLE_SCREEN_BUFFER_INFO = extern struct {
        dwSize: COORD,
        dwCursorPosition: COORD,
        wAttributes: windows.WORD,
        srWindow: SMALL_RECT,
        dwMaximumWindowSize: COORD,
    };

    pub fn eventFromRecord(self: *WindowsTty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
        switch (record.EventType) {
            0x0001 => { // Key event
                const event = record.Event.KeyEvent;

                if (state.utf16_half) half: {
                    state.utf16_half = false;
                    state.utf16_buf[1] = event.uChar.UnicodeChar;
                    const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half;
                    const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;

                    const key: Key = .{
                        .codepoint = codepoint,
                        .base_layout_codepoint = codepoint,
                        .mods = translateMods(event.dwControlKeyState),
                        .text = self.buf[0..n],
                    };

                    switch (event.bKeyDown) {
                        .FALSE => return .{ .key_release = key },
                        else => return .{ .key_press = key },
                    }
                }

                const base_layout: u16 = switch (event.wVirtualKeyCode) {
                    0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
                        if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
                            break :blk event.uChar.UnicodeChar;
                        state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
                        state.ansi_idx += 1;
                        if (state.ansi_idx <= 2) return null;
                        const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
                        return if (result.n == 0) null else evt: {
                            state.ansi_idx = 0;
                            break :evt result.event;
                        };
                    },
                    0x08 => Key.backspace,
                    0x09 => Key.tab,
                    0x0D => Key.enter,
                    0x13 => Key.pause,
                    0x14 => Key.caps_lock,
                    0x1B => Key.escape,
                    0x20 => Key.space,
                    0x21 => Key.page_up,
                    0x22 => Key.page_down,
                    0x23 => Key.end,
                    0x24 => Key.home,
                    0x25 => Key.left,
                    0x26 => Key.up,
                    0x27 => Key.right,
                    0x28 => Key.down,
                    0x2c => Key.print_screen,
                    0x2d => Key.insert,
                    0x2e => Key.delete,
                    0x30...0x39 => |k| k,
                    0x41...0x5a => |k| k + 0x20, // translate to lowercase
                    0x5b => Key.left_meta,
                    0x5c => Key.right_meta,
                    0x60 => Key.kp_0,
                    0x61 => Key.kp_1,
                    0x62 => Key.kp_2,
                    0x63 => Key.kp_3,
                    0x64 => Key.kp_4,
                    0x65 => Key.kp_5,
                    0x66 => Key.kp_6,
                    0x67 => Key.kp_7,
                    0x68 => Key.kp_8,
                    0x69 => Key.kp_9,
                    0x6a => Key.kp_multiply,
                    0x6b => Key.kp_add,
                    0x6c => Key.kp_separator,
                    0x6d => Key.kp_subtract,
                    0x6e => Key.kp_decimal,
                    0x6f => Key.kp_divide,
                    0x70 => Key.f1,
                    0x71 => Key.f2,
                    0x72 => Key.f3,
                    0x73 => Key.f4,
                    0x74 => Key.f5,
                    0x75 => Key.f6,
                    0x76 => Key.f8,
                    0x77 => Key.f8,
                    0x78 => Key.f9,
                    0x79 => Key.f10,
                    0x7a => Key.f11,
                    0x7b => Key.f12,
                    0x7c => Key.f13,
                    0x7d => Key.f14,
                    0x7e => Key.f15,
                    0x7f => Key.f16,
                    0x80 => Key.f17,
                    0x81 => Key.f18,
                    0x82 => Key.f19,
                    0x83 => Key.f20,
                    0x84 => Key.f21,
                    0x85 => Key.f22,
                    0x86 => Key.f23,
                    0x87 => Key.f24,
                    0x90 => Key.num_lock,
                    0x91 => Key.scroll_lock,
                    0xa0 => Key.left_shift,
                    0x10 => Key.left_shift,
                    0xa1 => Key.right_shift,
                    0xa2 => Key.left_control,
                    0x11 => Key.left_control,
                    0xa3 => Key.right_control,
                    0xa4 => Key.left_alt,
                    0x12 => Key.left_alt,
                    0xa5 => Key.right_alt,
                    0xad => Key.mute_volume,
                    0xae => Key.lower_volume,
                    0xaf => Key.raise_volume,
                    0xb0 => Key.media_track_next,
                    0xb1 => Key.media_track_previous,
                    0xb2 => Key.media_stop,
                    0xb3 => Key.media_play_pause,
                    0xba => ';',
                    0xbb => '+',
                    0xbc => ',',
                    0xbd => '-',
                    0xbe => '.',
                    0xbf => '/',
                    0xc0 => '`',
                    0xdb => '[',
                    0xdc => '\\',
                    0xdf => '\\',
                    0xe2 => '\\',
                    0xdd => ']',
                    0xde => '\'',
                    else => {
                        const log = std.log.scoped(.vaxis);
                        log.warn("unknown wVirtualKeyCode: 0x{x}", .{event.wVirtualKeyCode});
                        return null;
                    },
                };

                if (std.unicode.utf16IsHighSurrogate(base_layout)) {
                    state.utf16_buf[0] = base_layout;
                    state.utf16_half = true;
                    return null;
                }
                if (std.unicode.utf16IsLowSurrogate(base_layout)) {
                    return null;
                }

                var codepoint: u21 = base_layout;
                var text: ?[]const u8 = null;
                switch (event.uChar.UnicodeChar) {
                    0x00...0x1F => {},
                    else => |cp| {
                        codepoint = cp;
                        const n = try std.unicode.utf8Encode(codepoint, &self.buf);
                        text = self.buf[0..n];
                    },
                }

                const key: Key = .{
                    .codepoint = codepoint,
                    .base_layout_codepoint = base_layout,
                    .mods = translateMods(event.dwControlKeyState),
                    .text = text,
                };

                switch (event.bKeyDown) {
                    .FALSE => return .{ .key_release = key },
                    else => return .{ .key_press = key },
                }
            },
            0x0002 => { // Mouse event
                // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str

                const event = record.Event.MouseEvent;

                // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
                // is wheel_down
                // Low word represents button state
                const mouse_wheel_direction: i16 = blk: {
                    const wheelu32: u32 = event.dwButtonState >> 16;
                    const wheelu16: u16 = @truncate(wheelu32);
                    break :blk @bitCast(wheelu16);
                };

                const buttons: u16 = @truncate(event.dwButtonState);
                // save the current state when we are done
                defer self.last_mouse_button_press = buttons;
                const button_xor = self.last_mouse_button_press ^ buttons;

                var event_type: Mouse.Type = .press;
                const btn: Mouse.Button = switch (button_xor) {
                    0x0000 => blk: {
                        // Check wheel event
                        if (event.dwEventFlags & 0x0004 > 0) {
                            if (mouse_wheel_direction > 0)
                                break :blk .wheel_up
                            else
                                break :blk .wheel_down;
                        }

                        // If we have no change but one of the buttons is still pressed we have a
                        // drag event. Find out which button is held down
                        if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
                            event_type = .drag;
                            if (buttons & 0x0001 > 0) break :blk .left;
                            if (buttons & 0x0002 > 0) break :blk .right;
                            if (buttons & 0x0004 > 0) break :blk .middle;
                            if (buttons & 0x0008 > 0) break :blk .button_8;
                            if (buttons & 0x0010 > 0) break :blk .button_9;
                        }

                        if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
                        break :blk .none;
                    },
                    0x0001 => blk: {
                        if (buttons & 0x0001 == 0) event_type = .release;
                        break :blk .left;
                    },
                    0x0002 => blk: {
                        if (buttons & 0x0002 == 0) event_type = .release;
                        break :blk .right;
                    },
                    0x0004 => blk: {
                        if (buttons & 0x0004 == 0) event_type = .release;
                        break :blk .middle;
                    },
                    0x0008 => blk: {
                        if (buttons & 0x0008 == 0) event_type = .release;
                        break :blk .button_8;
                    },
                    0x0010 => blk: {
                        if (buttons & 0x0010 == 0) event_type = .release;
                        break :blk .button_9;
                    },
                    else => {
                        std.log.warn("unknown mouse event: {}", .{event});
                        return null;
                    },
                };

                const shift: u32 = 0x0010;
                const alt: u32 = 0x0001 | 0x0002;
                const ctrl: u32 = 0x0004 | 0x0008;
                const mods: Mouse.Modifiers = .{
                    .shift = event.dwControlKeyState & shift > 0,
                    .alt = event.dwControlKeyState & alt > 0,
                    .ctrl = event.dwControlKeyState & ctrl > 0,
                };

                const mouse: Mouse = .{
                    .col = @as(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
                    .row = @as(i16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
                    .mods = mods,
                    .type = event_type,
                    .button = btn,
                };
                return .{ .mouse = mouse };
            },
            0x0004 => { // Screen resize events
                // NOTE: Even though the event comes with a size, it may not be accurate. We ask for
                // the size directly when we get this event
                var console_info: CONSOLE_SCREEN_BUFFER_INFO = undefined;
                if (GetConsoleScreenBufferInfo(self.stdout, &console_info) == .FALSE) {
                    return windows.unexpectedError(windows.GetLastError());
                }
                const window_rect = console_info.srWindow;
                const width = window_rect.Right - window_rect.Left + 1;
                const height = window_rect.Bottom - window_rect.Top + 1;
                return .{
                    .winsize = .{
                        .cols = @intCast(width),
                        .rows = @intCast(height),
                        .x_pixel = 0,
                        .y_pixel = 0,
                    },
                };
            },
            0x0010 => { // Focus events
                switch (record.Event.FocusEvent.bSetFocus) {
                    .FALSE => return .focus_out,
                    else => return .focus_in,
                }
            },
            else => {},
        }
        return null;
    }

    fn translateMods(mods: u32) Key.Modifiers {
        const left_alt: u32 = 0x0002;
        const right_alt: u32 = 0x0001;
        const left_ctrl: u32 = 0x0008;
        const right_ctrl: u32 = 0x0004;

        const caps: u32 = 0x0080;
        const num_lock: u32 = 0x0020;
        const shift: u32 = 0x0010;
        const alt: u32 = left_alt | right_alt;
        const ctrl: u32 = left_ctrl | right_ctrl;

        const altGr = (mods & right_alt > 0) and (mods & left_ctrl > 0);

        return .{
            .shift = mods & shift > 0,
            .alt = if (altGr) mods & left_alt > 0 else mods & alt > 0,
            .ctrl = if (altGr) mods & right_ctrl > 0 else mods & ctrl > 0,
            .caps_lock = mods & caps > 0,
            .num_lock = mods & num_lock > 0,
        };
    }

    pub fn getWinsize(self: *WindowsTty) !Winsize {
        var console_info: CONSOLE_SCREEN_BUFFER_INFO = undefined;
        if (GetConsoleScreenBufferInfo(self.stdout, &console_info) == .FALSE) {
            return windows.unexpectedError(windows.GetLastError());
        }
        const window_rect = console_info.srWindow;
        const width = window_rect.Right - window_rect.Left + 1;
        const height = window_rect.Bottom - window_rect.Top + 1;
        return .{
            .cols = @intCast(width),
            .rows = @intCast(height),
            .x_pixel = 0,
            .y_pixel = 0,
        };
    }

    // From gitub.com/ziglibs/zig-windows-console. Thanks :)
    //
    // Events
    const union_unnamed_248 = extern union {
        UnicodeChar: windows.WCHAR,
        AsciiChar: windows.CHAR,
    };

    pub const KEY_EVENT_RECORD = extern struct {
        bKeyDown: windows.BOOL,
        wRepeatCount: windows.WORD,
        wVirtualKeyCode: windows.WORD,
        wVirtualScanCode: windows.WORD,
        uChar: union_unnamed_248,
        dwControlKeyState: windows.DWORD,
    };

    pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD;

    pub const MOUSE_EVENT_RECORD = extern struct {
        dwMousePosition: windows.COORD,
        dwButtonState: windows.DWORD,
        dwControlKeyState: windows.DWORD,
        dwEventFlags: windows.DWORD,
    };

    pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD;

    pub const WINDOW_BUFFER_SIZE_RECORD = extern struct {
        dwSize: windows.COORD,
    };

    pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD;

    pub const MENU_EVENT_RECORD = extern struct {
        dwCommandId: windows.UINT,
    };

    pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD;

    pub const FOCUS_EVENT_RECORD = extern struct {
        bSetFocus: windows.BOOL,
    };

    pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD;

    const union_unnamed_249 = extern union {
        KeyEvent: KEY_EVENT_RECORD,
        MouseEvent: MOUSE_EVENT_RECORD,
        WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
        MenuEvent: MENU_EVENT_RECORD,
        FocusEvent: FOCUS_EVENT_RECORD,
    };

    pub const INPUT_RECORD = extern struct {
        EventType: windows.WORD,
        Event: union_unnamed_249,
    };

    pub const PINPUT_RECORD = *INPUT_RECORD;

    pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(.winapi) windows.BOOL;
    pub extern "kernel32" fn GetConsoleOutputCP() callconv(.winapi) windows.UINT;
    pub extern "kernel32" fn GetConsoleMode(kConsoleHandle: windows.HANDLE, lpMode: *windows.DWORD) callconv(.winapi) windows.BOOL;
    pub extern "kernel32" fn SetConsoleMode(hConsoleHandle: windows.HANDLE, dwMode: windows.DWORD) callconv(.winapi) windows.BOOL;
    pub extern "kernel32" fn SetConsoleOutputCP(wCodePageId: windows.UINT) callconv(.winapi) windows.BOOL;
    pub extern "kernel32" fn GetConsoleScreenBufferInfo(hConsoleOutpur: windows.HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) callconv(.winapi) windows.BOOL;
};

pub const TestTty = switch (builtin.os.tag) {
    .linux => struct {
        const linux = std.os.linux;
        /// Used for API compat
        fd: posix.fd_t,
        pipe_read: posix.fd_t,
        pipe_write: posix.fd_t,
        tty_writer: *std.Io.Writer.Allocating,

        /// Initializes a TestTty.
        pub fn init(_: std.Io, buffer: []u8) !@This() {
            _ = buffer;

            const list = try std.testing.allocator.create(std.Io.Writer.Allocating);
            list.* = .init(std.testing.allocator);
            var fds: [2]i32 = undefined;
            const rc = linux.pipe(&fds);
            switch (linux.errno(rc)) {
                .SUCCESS => {},
                else => return error.PipeCreateFailed,
            }
            return .{
                .fd = fds[0],
                .pipe_read = fds[0],
                .pipe_write = fds[1],
                .tty_writer = list,
            };
        }

        pub fn deinit(self: *@This()) void {
            _ = linux.close(self.pipe_read);
            _ = linux.close(self.pipe_write);
            self.tty_writer.deinit();
            std.testing.allocator.destroy(self.tty_writer);
        }

        pub fn writer(self: *@This()) *std.Io.Writer {
            return &self.tty_writer.writer;
        }

        pub fn read(self: *const @This(), buf: []u8) !usize {
            return posix.read(self.fd, buf);
        }

        /// Get the window size from the kernel
        pub fn getWinsize(_: *@This()) !Winsize {
            return .{
                .rows = 40,
                .cols = 80,
                .x_pixel = 40 * 8,
                .y_pixel = 40 * 8 * 2,
            };
        }

        /// Implemented for the Windows API
        pub fn nextEvent(_: *@This(), _: *Parser, _: ?std.mem.Allocator) !Event {
            return error.SkipZigTest;
        }

        pub fn resetSignalHandler() void {
            return;
        }
    },
    else => struct {
        d: std.Io.Writer.Discarding,

        pub fn init(_: std.Io, buf: []u8) !@This() {
            return .{
                .d = .init(buf),
            };
        }

        pub fn deinit(_: *const @This()) void {}

        pub fn writer(self: *@This()) *std.Io.Writer {
            return &self.d.writer;
        }

        pub fn getWinsize(_: *@This()) !Winsize {
            return .{
                .rows = 40,
                .cols = 80,
                .x_pixel = 40 * 8,
                .y_pixel = 40 * 8 * 2,
            };
        }

        /// Implemented for the Windows API
        pub fn nextEvent(_: *@This(), _: *Parser, _: ?std.mem.Allocator) !Event {
            return error.SkipZigTest;
        }
    },
};

test {
    std.testing.refAllDecls(@This());
}
