unit module Terminal::API;

use NativeCall;

my class TermiosLinux is repr('CStruct') {
    has int32 $.iflag;
    has int32 $.oflag;
    has int32 $.cflag;
    has int32 $.lflag;
    has int8 $.line;
    has int8 $.cc_VINTR is rw;
    has int8 $.cc_QUIT is rw;
    has int8 $.cc_VERASE is rw;
    has int8 $.cc_VKILL is rw;
    has int8 $.cc_VEOF is rw;
    has int8 $.cc_VTIME is rw;
    has int8 $.cc_VMIN is rw;
    has int8 $.cc_VSWTC is rw;
    has int8 $.cc_VSTART is rw;
    has int8 $.cc_VSTOP is rw;
    has int8 $.cc_VSUSP is rw;
    has int8 $.cc_VEOL is rw;
    has int8 $.cc_VREPRINT is rw;
    has int8 $.cc_VDISCARD is rw;
    has int8 $.cc_VWERASE is rw;
    has int8 $.cc_VLNEXT is rw;
    has int8 $.cc_VEOL2 is rw;
    has int8 $.cc_17 is rw; has int8 $.cc_18 is rw; has int8 $.cc_19 is rw;
    has int8 $.cc_20 is rw; has int8 $.cc_21 is rw; has int8 $.cc_22 is rw;
    has int8 $.cc_23 is rw; has int8 $.cc_24 is rw; has int8 $.cc_25 is rw;
    has int8 $.cc_26 is rw; has int8 $.cc_27 is rw; has int8 $.cc_28 is rw;
    has int8 $.cc_29 is rw; has int8 $.cc_30 is rw; has int8 $.cc_31 is rw;
    has int32 $.ispeed is rw;
    has int32 $.ospeed is rw;
}

my class TermiosMac is repr('CStruct') {
    has uint64 $.iflag;
    has uint64 $.oflag;
    has uint64 $.cflag;
    has uint64 $.lflag;
    has uint8  $.cc_0 is rw; has uint8  $.cc_1 is rw; has uint8  $.cc_2 is rw;
    has uint8  $.cc_3 is rw; has uint8  $.cc_4 is rw; has uint8  $.cc_5 is rw;
    has uint8  $.cc_6 is rw; has uint8  $.cc_7 is rw; has uint8  $.cc_8 is rw;
    has uint8  $.cc_9 is rw; has uint8  $.cc_10 is rw; has uint8  $.cc_11 is rw;
    has uint8  $.cc_12 is rw; has uint8  $.cc_13 is rw; has uint8  $.cc_14 is rw;
    has uint8  $.cc_16 is rw; has uint8  $.cc_17 is rw; has uint8  $.cc_18 is rw;
    has uint8  $.cc_19 is rw;
    has uint64 $.ispeed is rw;
    has uint64 $.ospeed is rw;
};

my class TermiosBSD is repr('CStruct') {
    has uint32 $.iflag;
    has uint32 $.oflag;
    has uint32 $.cflag;
    has uint32 $.lflag;
    has uint8  $.cc_0 is rw; has uint8  $.cc_1 is rw; has uint8  $.cc_2 is rw;
    has uint8  $.cc_3 is rw; has uint8  $.cc_4 is rw; has uint8  $.cc_5 is rw;
    has uint8  $.cc_6 is rw; has uint8  $.cc_7 is rw; has uint8  $.cc_8 is rw;
    has uint8  $.cc_9 is rw; has uint8  $.cc_10 is rw; has uint8  $.cc_11 is rw;
    has uint8  $.cc_12 is rw; has uint8  $.cc_13 is rw; has uint8  $.cc_14 is rw;
    has uint8  $.cc_16 is rw; has uint8  $.cc_17 is rw; has uint8  $.cc_18 is rw;
    has uint8  $.cc_19 is rw;
    has uint32 $.ispeed is rw;
    has uint32 $.ospeed is rw;
};

my class Winsize is repr('CStruct') {
    has uint16 $.ws_row;
    has uint16 $.ws_col;
    has uint16 $.ws_xpixel; # unused
    has uint16 $.ws_ypixel; # unused
};

sub tcgetattr-linux(int32, TermiosLinux --> int32) is native is symbol('tcgetattr') {*}
sub tcsetattr-linux(int32, int32, TermiosLinux --> int32) is native is symbol('tcsetattr') {*}
sub cfmakeraw-linux(TermiosLinux) is native is symbol('cfmakeraw') {*}

sub tcgetattr-bsd(int32, TermiosBSD --> int32) is native is symbol('tcgetattr') {*}
sub tcsetattr-bsd(int32, int32, TermiosBSD --> int32) is native is symbol('tcsetattr') {*}
sub cfmakeraw-bsd(TermiosBSD) is native is symbol('cfmakeraw') {*}

sub tcgetattr-mac(int32, TermiosMac --> int32) is native is symbol('tcgetattr') {*}
sub tcsetattr-mac(int32, int32, TermiosMac --> int32) is native is symbol('tcsetattr') {*}
sub cfmakeraw-mac(TermiosMac) is native is symbol('cfmakeraw') {*}

constant TIOCGWINSZ-linux = 0x5413; # 21523
constant TIOCGWINSZ-bsd = 0x40087468; # 1074295912
constant TIOCGWINSZ-mac = 0x40087468; # 1074295912
sub ioctl(int32, int32, Winsize --> int32) is native {*}

# Windows stuff ---------------------------------------------------------------

my class Coord is repr('CStruct') {
  has int16 $.X;
  has int16 $.Y;
};

my class SmallRect is repr('CStruct') {
  has int16 $.Left;
  has int16 $.Top;
  has int16 $.Right;
  has int16 $.Bottom;
};

my class ConsoleScreenBufferInfo is repr('CStruct') {
  HAS Coord      $.dwSize;
  HAS Coord      $.dwCursorPosition;
  has uint16     $.wAttributes;
  HAS SmallRect  $.srWindow;
  HAS Coord      $.dwMaximumWindowSize;
}

my class SecurityAttributes is repr('CStruct') {
    has uint32 $nLength;
    has Pointer[void] $lpSecurityDescriptor;
    has bool $bInheritHandle;
}

constant ENABLE_PROCESSED_INPUT        = 0x0001;
constant ENABLE_LINE_INPUT             = 0x0002;
constant ENABLE_ECHO_INPUT             = 0x0004;
constant ENABLE_WINDOW_INPUT           = 0x0008;
constant ENABLE_MOUSE_INPUT            = 0x0010;
constant ENABLE_INSERT_MODE            = 0x0020;
constant ENABLE_QUICK_EDIT_MODE        = 0x0040;
constant ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;

constant ENABLE_PROCESSED_OUTPUT = 0x0001;
constant ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002;
constant ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
constant DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
constant ENABLE_LVB_GRID_WORLDWIDE = 0x0010;

constant STD_INPUT_HANDLE = 4294967286;
constant STD_OUTPUT_HANDLE = 4294967285;
constant STD_ERROR_HANDLE = 4294967284;

constant GENERIC_READ = 0x80_000_000;
constant GENERIC_WRITE = 0x40_000_000;
constant FILE_SHARE_WRITE = 2;
constant OPEN_EXISTING = 3;

sub CreateFileA(Str $lpFileName,
                uint32 $dwDesiredAccess,
                uint32 $dwShareMode,
                SecurityAttributes $lpSecurityAttributes,
                uint32 $dwCreationDisposition,
                uint32 $dwFlagsAndAttributes,
                Pointer[void] $hTemplateFile
                --> Pointer[void]) is native('kernel32') {*}
sub _get_osfhandle(int32 $fd --> Pointer[void]) is native('msvcrt') {*}
sub SetConsoleMode(Pointer[void] $hConsoleHandle, uint32 $dwMode --> Bool) is native('kernel32') {*}
sub GetConsoleMode(Pointer[void] $hConsoleHandle, uint32 $lpMode is rw --> Bool) is native('kernel32') {*}
sub GetConsoleScreenBufferInfo(Pointer[void] $hConsoleOutput, ConsoleScreenBufferInfo $lpConsoleScreenBufferInfo --> Bool) is native('kernel32') {*}
sub GetLastError(--> uint32) is native('kernel32') {*}

# End of native call stuff ====================================================

enum TerminalConfigWhen <NOW DRAIN FLUSH>;

#|((
Retrieves an (opaque) terminal configuration object. This is useful to be able
to later restore the config via L<defn:restore-config> after messing with it.
Pass in a file descriptor. Defaults to $*IN.native-descriptor.
))
our sub get-config($fd-in = $*IN.native-descriptor) {
    if $*KERNEL.name eq 'linux' {
        my TermiosLinux $termios .= new;
        tcgetattr-linux($fd-in, $termios) and die "get-config failed";
        $termios
    }
    elsif $*DISTRO.name eq 'macos' {
        my TermiosMac $termios .= new;
        tcgetattr-mac($fd-in, $termios) and die "get-config failed";
        $termios
    }
    elsif $*DISTRO.is-win {
        my $in-handle = _get_osfhandle($fd-in);
        my uint32 $mode;
        GetConsoleMode($in-handle, $mode) or die "get-config failed";
        $mode
    }
    else {
        my TermiosBSD $termios .= new;
        tcgetattr-bsd($fd-in, $termios) and die "get-config failed";
        $termios
    }
}

#|((
Restore a config previously saved via L<defn:get-config>.
))
our sub restore-config(
    $config,
    $fd-in = $*IN.native-descriptor,
    TerminalConfigWhen :$when = NOW  #< One of C<NOW>, C<DRAIN>, C<FLUSH>. See L<man:tcsetattr> for a description of the args. Has no effect on Windows.
) {
    if $*KERNEL.name eq 'linux' {
        tcsetattr-linux($fd-in, $when.value, $config) and die "restore-config failed";
    }
    elsif $*DISTRO.name eq 'macos' {
        tcsetattr-mac($fd-in, $when.value, $config) and die "restore-config failed";
    }
    elsif $*DISTRO.is-win {
        my $in-handle = _get_osfhandle($fd-in);
        SetConsoleMode($in-handle, $config) or die "restore-config failed";
    }
    else {
        tcsetattr-bsd($fd-in, $when.value, $config) and die "restore-config failed";
    }
}

#|((
Turns the input into a raw input, suitable for working with VT codes directly.
))
our sub make-raw($fd-in = $*IN.native-descriptor, TerminalConfigWhen :$when = NOW) {
    if $*KERNEL.name eq 'linux' {
        my $config = get-config($fd-in);
        cfmakeraw-linux($config);
        tcsetattr-linux($fd-in, $when.value, $config);
    }
    elsif $*DISTRO.name eq 'macos' {
        my $config = get-config($fd-in);
        cfmakeraw-mac($config);
        tcsetattr-mac($fd-in, $when.value, $config);
    }
    elsif $*DISTRO.is-win {
        my $in-handle = _get_osfhandle($fd-in);
        my uint32 $mode;
        GetConsoleMode($in-handle, $mode) or die "GetConsoleMode failed";
        SetConsoleMode($in-handle,
            $mode +& +^ENABLE_PROCESSED_INPUT
                    +& +^ENABLE_LINE_INPUT
                    +& +^ENABLE_ECHO_INPUT
                    #+|   ENABLE_MOUSE_INPUT
                    +|   ENABLE_VIRTUAL_TERMINAL_INPUT
        );
    }
    else {
        my $config = get-config($fd-in);
        cfmakeraw-bsd($config);
        tcsetattr-bsd($fd-in, $when.value, $config);
    }
}

class TermSize {
    has Int $.cols;
    has Int $.rows;
}
#|((
Returns a L<defn:TermSize> object with cols and rows for the given output.
))
our sub get-window-size($fd-out? is copy) {
    if $*DISTRO.is-win {
        my $out-handle;
        if $fd-out {
            $out-handle = _get_osfhandle($fd-out);
        }
        else {
            my $securityAttributes = SecurityAttributes.new:
                nLength => nativesizeof(SecurityAttributes),
                lpSecurityDescriptor => 0,
                bInheritHandle => True,
            ;
            $out-handle = CreateFileA('CONOUT$', GENERIC_READ +| GENERIC_WRITE,
                                      FILE_SHARE_WRITE, $securityAttributes,
                                      OPEN_EXISTING, 0, 0);
        }

        my ConsoleScreenBufferInfo $info .= new;
        if GetConsoleScreenBufferInfo($out-handle, $info) == 0 {
            my $errno = GetLastError();
            die "GetConsoleScreenBufferInfo failed: $errno";
        }
        TermSize.new:
            cols => $info.srWindow.Right - $info.srWindow.Left + 1,
            rows => $info.srWindow.Bottom - $info.srWindow.Top + 1,
    }
    else {
        if $*RAKU.compiler ~~ /rakudo/ { # && $*RAKU.compiler.version < v2025.06
            # Older Rakudos don't support var-args in native call functions.
            my $cols = q:x{ tput cols  } .chomp.Int;
            my $rows = q:x{ tput lines } .chomp.Int;
            TermSize.new:
                :$cols,
                :$rows,
        }
        else {
            my $tty;
            unless $fd-out {
                $tty = "/dev/tty".IO.open(:a);
                $fd-out = $tty.native-descriptor;
            }
            my Winsize $size .= new;
            if $*KERNEL.name eq 'linux' {
                ioctl($fd-out, TIOCGWINSZ-linux, $size) and die "ioctl(TIOCGWINSZ) failed";
            }
            elsif $*DISTRO.name eq 'macos' {
                ioctl($fd-out, TIOCGWINSZ-mac, $size) and die "ioctl(TIOCGWINSZ) failed";
            }
            else {
                ioctl($fd-out, TIOCGWINSZ-bsd, $size) and die "ioctl(TIOCGWINSZ) failed";
            }
            $_.close with $tty;
            TermSize.new:
                cols => $size.ws_col,
                rows => $size.ws_row,
        }
    }
}

