/// @module system

/// Terminal.
// Unix: see https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios-termios3-and-stty/
//
// Windows: see https://learn.microsoft.com/en-us/windows/console/console-reference
// @section terminal

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include "compat.h"
#include "bitflags.h"


#ifdef _WIN32
# include <windows.h>
# include <locale.h>
# ifndef _MSC_VER
#  include <conio.h>
#  include <unistd.h>
#  include <stddef.h>
# endif
#else
# include <termios.h>
# include <string.h>
# include <errno.h>
# include <fcntl.h>
# include <sys/ioctl.h>
# include <unistd.h>
# include <locale.h>
#endif
#include <wchar.h>


// Windows does not have a wcwidth function, so we use compatibilty code from
// http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c by Markus Kuhn
#include "wcwidth.h"


#ifdef _WIN32
// after an error is returned, GetLastError() result can be passed to this function to get a string
// representation of the error on the stack.
// result will be nil+error on the stack, always 2 results.
static void termFormatError(lua_State *L, DWORD errorCode, const char* prefix) {
//static void FormatErrorAndReturn(lua_State *L, DWORD errorCode, const char* prefix) {
    LPSTR messageBuffer = NULL;
    FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                   NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);

    lua_pushnil(L);
    if (messageBuffer) {
        if (prefix) {
            lua_pushfstring(L, "%s: %s", prefix, messageBuffer);
        } else {
            lua_pushstring(L, messageBuffer);
        }
        LocalFree(messageBuffer);
    } else {
        lua_pushfstring(L, "%sError code %d", prefix ? prefix : "", errorCode);
    }
}
#else
static int pusherror(lua_State *L, const char *info)
{
	lua_pushnil(L);
	if (info==NULL) {
		lua_pushstring(L, strerror(errno));
    } else {
		lua_pushfstring(L, "%s: %s", info, strerror(errno));
    }
	lua_pushinteger(L, errno);
	return 3;
}
#endif

/***
Checks if a file-handle is a TTY.

@function isatty
@tparam file file the file-handle to check, one of `io.stdin`, `io.stdout`, `io.stderr`.
@treturn boolean true if the file is a tty
@usage
local system = require('system')
if system.isatty(io.stdin) then
    -- enable ANSI coloring etc on Windows, does nothing in Posix.
    local flags = system.getconsoleflags(io.stdout)
    system.setconsoleflags(io.stdout, flags + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
end
*/
static int lst_isatty(lua_State* L) {
    FILE **fh = (FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE);
    lua_pushboolean(L, isatty(fileno(*fh)));
    return 1;
}


/*-------------------------------------------------------------------------
 * Windows Get/SetConsoleMode functions
 *-------------------------------------------------------------------------*/

typedef struct ls_RegConst {
  const char *name;
  DWORD value;
} ls_RegConst;

// Define a macro to check if a constant is defined and set it to 0 if not.
// This is needed because some flags are not defined on all platforms. So we
// still export the constants, but they will be all 0, and hence not do anything.
#ifdef _WIN32
 // check compatibility: Windows virtual terminal processing was added in 2015,
 // some older compiler suites don't have the proper headers.
 #ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
  #error Virtual terminal macros are undefined (eg. ENABLE_VIRTUAL_TERMINAL_INPUT). Update the toolchain or revert to Luasystem < 0.4
 #endif
 #define CHECK_WIN_FLAG_OR_ZERO(flag) flag
 #define CHECK_NIX_FLAG_OR_ZERO(flag) 0
#else
 #define CHECK_WIN_FLAG_OR_ZERO(flag) 0
 #define CHECK_NIX_FLAG_OR_ZERO(flag) flag
#endif

// Export Windows constants to Lua
static const struct ls_RegConst win_console_in_flags[] = {
    // Console Input Flags
    {"CIF_ECHO_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_ECHO_INPUT)},
    {"CIF_INSERT_MODE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_INSERT_MODE)},
    {"CIF_LINE_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_LINE_INPUT)},
    {"CIF_MOUSE_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_MOUSE_INPUT)},
    {"CIF_PROCESSED_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_PROCESSED_INPUT)},
    {"CIF_QUICK_EDIT_MODE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_QUICK_EDIT_MODE)},
    {"CIF_WINDOW_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_WINDOW_INPUT)},
    {"CIF_VIRTUAL_TERMINAL_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_VIRTUAL_TERMINAL_INPUT)},
    {"CIF_EXTENDED_FLAGS", CHECK_WIN_FLAG_OR_ZERO(ENABLE_EXTENDED_FLAGS)},
    {"CIF_AUTO_POSITION", CHECK_WIN_FLAG_OR_ZERO(ENABLE_AUTO_POSITION)},
    {NULL, 0}
};

static const struct ls_RegConst win_console_out_flags[] = {
    // Console Output Flags
    {"COF_PROCESSED_OUTPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_PROCESSED_OUTPUT)},
    {"COF_WRAP_AT_EOL_OUTPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_WRAP_AT_EOL_OUTPUT)},
    {"COF_VIRTUAL_TERMINAL_PROCESSING", CHECK_WIN_FLAG_OR_ZERO(ENABLE_VIRTUAL_TERMINAL_PROCESSING)},
    {"COF_DISABLE_NEWLINE_AUTO_RETURN", CHECK_WIN_FLAG_OR_ZERO(DISABLE_NEWLINE_AUTO_RETURN)},
    {"COF_ENABLE_LVB_GRID_WORLDWIDE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_LVB_GRID_WORLDWIDE)},
    {NULL, 0}
};


// Export Unix constants to Lua
static const struct ls_RegConst nix_tcsetattr_actions[] = {
    // The optional actions for tcsetattr
    {"TCSANOW", CHECK_NIX_FLAG_OR_ZERO(TCSANOW)},
    {"TCSADRAIN", CHECK_NIX_FLAG_OR_ZERO(TCSADRAIN)},
    {"TCSAFLUSH", CHECK_NIX_FLAG_OR_ZERO(TCSAFLUSH)},
    {NULL, 0}
};

static const struct ls_RegConst nix_console_i_flags[] = {
    // Input flags (c_iflag)
    {"I_IGNBRK", CHECK_NIX_FLAG_OR_ZERO(IGNBRK)},
    {"I_BRKINT", CHECK_NIX_FLAG_OR_ZERO(BRKINT)},
    {"I_IGNPAR", CHECK_NIX_FLAG_OR_ZERO(IGNPAR)},
    {"I_PARMRK", CHECK_NIX_FLAG_OR_ZERO(PARMRK)},
    {"I_INPCK", CHECK_NIX_FLAG_OR_ZERO(INPCK)},
    {"I_ISTRIP", CHECK_NIX_FLAG_OR_ZERO(ISTRIP)},
    {"I_INLCR", CHECK_NIX_FLAG_OR_ZERO(INLCR)},
    {"I_IGNCR", CHECK_NIX_FLAG_OR_ZERO(IGNCR)},
    {"I_ICRNL", CHECK_NIX_FLAG_OR_ZERO(ICRNL)},
#ifdef IUCLC
    {"I_IUCLC", CHECK_NIX_FLAG_OR_ZERO(IUCLC)},
#else
    {"I_IUCLC", 0},
#endif
    {"I_IXON", CHECK_NIX_FLAG_OR_ZERO(IXON)},
    {"I_IXANY", CHECK_NIX_FLAG_OR_ZERO(IXANY)},
    {"I_IXOFF", CHECK_NIX_FLAG_OR_ZERO(IXOFF)},
    {"I_IMAXBEL", CHECK_NIX_FLAG_OR_ZERO(IMAXBEL)},
    {NULL, 0}
};

static const struct ls_RegConst nix_console_o_flags[] = {
    // Output flags (c_oflag)
    {"O_OPOST", CHECK_NIX_FLAG_OR_ZERO(OPOST)},
#ifdef OLCUC
    {"O_OLCUC", CHECK_NIX_FLAG_OR_ZERO(OLCUC)},
#else
    {"O_OLCUC", 0},
#endif
    {"O_ONLCR", CHECK_NIX_FLAG_OR_ZERO(ONLCR)},
    {"O_OCRNL", CHECK_NIX_FLAG_OR_ZERO(OCRNL)},
    {"O_ONOCR", CHECK_NIX_FLAG_OR_ZERO(ONOCR)},
    {"O_ONLRET", CHECK_NIX_FLAG_OR_ZERO(ONLRET)},
#ifdef OFILL
    {"O_OFILL", CHECK_NIX_FLAG_OR_ZERO(OFILL)},
#else
    {"O_OFILL", 0},
#endif
#ifdef OFDEL
    {"O_OFDEL", CHECK_NIX_FLAG_OR_ZERO(OFDEL)},
#else
    {"O_OFDEL", 0},
#endif
#ifdef NLDLY
    {"O_NLDLY", CHECK_NIX_FLAG_OR_ZERO(NLDLY)},
#else
    {"O_NLDLY", 0},
#endif
#ifdef CRDLY
    {"O_CRDLY", CHECK_NIX_FLAG_OR_ZERO(CRDLY)},
#else
    {"O_CRDLY", 0},
#endif
#ifdef TABDLY
    {"O_TABDLY", CHECK_NIX_FLAG_OR_ZERO(TABDLY)},
#else
    {"O_TABDLY", 0},
#endif
#ifdef BSDLY
    {"O_BSDLY", CHECK_NIX_FLAG_OR_ZERO(BSDLY)},
#else
    {"O_BSDLY", 0},
#endif
#ifdef VTDLY
    {"O_VTDLY", CHECK_NIX_FLAG_OR_ZERO(VTDLY)},
#else
    {"O_VTDLY", 0},
#endif
#ifdef FFDLY
    {"O_FFDLY", CHECK_NIX_FLAG_OR_ZERO(FFDLY)},
#else
    {"O_FFDLY", 0},
#endif
    {NULL, 0}
};

static const struct ls_RegConst nix_console_l_flags[] = {
    // Local flags (c_lflag)
    {"L_ISIG", CHECK_NIX_FLAG_OR_ZERO(ISIG)},
    {"L_ICANON", CHECK_NIX_FLAG_OR_ZERO(ICANON)},
#ifdef XCASE
    {"L_XCASE", CHECK_NIX_FLAG_OR_ZERO(XCASE)},
#else
    {"L_XCASE", 0},
#endif
    {"L_ECHO", CHECK_NIX_FLAG_OR_ZERO(ECHO)},
    {"L_ECHOE", CHECK_NIX_FLAG_OR_ZERO(ECHOE)},
    {"L_ECHOK", CHECK_NIX_FLAG_OR_ZERO(ECHOK)},
    {"L_ECHONL", CHECK_NIX_FLAG_OR_ZERO(ECHONL)},
    {"L_NOFLSH", CHECK_NIX_FLAG_OR_ZERO(NOFLSH)},
    {"L_TOSTOP", CHECK_NIX_FLAG_OR_ZERO(TOSTOP)},
    {"L_ECHOCTL", CHECK_NIX_FLAG_OR_ZERO(ECHOCTL)},
    {"L_ECHOPRT", CHECK_NIX_FLAG_OR_ZERO(ECHOPRT)},
    {"L_ECHOKE", CHECK_NIX_FLAG_OR_ZERO(ECHOKE)},
    {"L_FLUSHO", CHECK_NIX_FLAG_OR_ZERO(FLUSHO)},
    {"L_PENDIN", CHECK_NIX_FLAG_OR_ZERO(PENDIN)},
    {"L_IEXTEN", CHECK_NIX_FLAG_OR_ZERO(IEXTEN)},
    {NULL, 0}
};

static DWORD win_valid_in_flags = 0;
static DWORD win_valid_out_flags = 0;
static DWORD nix_valid_i_flags = 0;
static DWORD nix_valid_o_flags = 0;
static DWORD nix_valid_l_flags = 0;
static void initialize_valid_flags()
{
    win_valid_in_flags = 0;
    for (int i = 0; win_console_in_flags[i].name != NULL; i++)
    {
        win_valid_in_flags |= win_console_in_flags[i].value;
    }
    win_valid_out_flags = 0;
    for (int i = 0; win_console_out_flags[i].name != NULL; i++)
    {
        win_valid_out_flags |= win_console_out_flags[i].value;
    }
    nix_valid_i_flags = 0;
    for (int i = 0; nix_console_i_flags[i].name != NULL; i++)
    {
        nix_valid_i_flags |= nix_console_i_flags[i].value;
    }
    nix_valid_o_flags = 0;
    for (int i = 0; nix_console_o_flags[i].name != NULL; i++)
    {
        nix_valid_o_flags |= nix_console_o_flags[i].value;
    }
    nix_valid_l_flags = 0;
    for (int i = 0; nix_console_l_flags[i].name != NULL; i++)
    {
        nix_valid_l_flags |= nix_console_l_flags[i].value;
    }
}

#ifdef _WIN32
// first item on the stack should be io.stdin, io.stderr, or io.stdout, second item
// should be the flags to validate.
// If it returns NULL, then it leaves nil+err on the stack
static HANDLE get_console_handle(lua_State *L, int flags_optional)
{
    if (lua_gettop(L) < 1) {
        luaL_argerror(L, 1, "expected file handle");
    }

    HANDLE handle;
    DWORD valid;
    FILE *file = *(FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE);
    if (file == stdin && file != NULL) {
        handle = GetStdHandle(STD_INPUT_HANDLE);
        valid = win_valid_in_flags;

    } else if (file == stdout && file != NULL) {
        handle =  GetStdHandle(STD_OUTPUT_HANDLE);
        valid = win_valid_out_flags;

    } else if (file == stderr && file != NULL) {
        handle =  GetStdHandle(STD_ERROR_HANDLE);
        valid = win_valid_out_flags;

    } else {
        luaL_argerror(L, 1, "invalid file handle"); // does not return
    }

    if (handle == INVALID_HANDLE_VALUE) {
        termFormatError(L, GetLastError(), "failed to retrieve std handle");
        lua_error(L); // does not return
    }

    if (handle == NULL) {
        lua_pushnil(L);
        lua_pushliteral(L, "failed to get console handle");
        return NULL;
    }

    if (flags_optional && lua_gettop(L) < 2) {
        return handle;
    }

    if (lua_gettop(L) < 2) {
        luaL_argerror(L, 2, "expected flags");
    }

    LSBF_BITFLAG flags = lsbf_checkbitflags(L, 2);
    if ((flags & ~valid) != 0) {
        luaL_argerror(L, 2, "invalid flags");
    }

    return handle;
}
#else
// first item on the stack should be io.stdin, io.stderr, or io.stdout. Throws a
// Lua error if the file is not one of these.
static int get_console_handle(lua_State *L)
{
    FILE **file = (FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE);
    if (file == NULL || *file == NULL) {
        return luaL_argerror(L, 1, "expected file handle"); // call doesn't return
    }

    // Check if the file is stdin, stdout, or stderr
    if (*file == stdin || *file == stdout || *file == stderr) {
        // Push the file descriptor onto the Lua stack
        return fileno(*file);
    }

    return luaL_argerror(L, 1, "invalid file handle"); // does not return
}
#endif



/***
Sets the console flags (Windows).
The `CIF_` and `COF_` constants are available on the module table. Where `CIF` are the
input flags (for use with `io.stdin`) and `COF` are the output flags (for use with
`io.stdout`/`io.stderr`).

To see flag status and constant names check `listconsoleflags`.

Note: not all combinations of flags are allowed, as some are mutually exclusive or mutually required.
See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
@function setconsoleflags
@tparam file file file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
@tparam bitflags bitflags the flags to set/unset
@treturn[1] boolean `true` on success
@treturn[2] nil
@treturn[2] string error message
@within Terminal_Windows
@usage
local system = require('system')
system.listconsoleflags(io.stdout) -- List all the available flags and their current status

local flags = system.getconsoleflags(io.stdout)
assert(system.setconsoleflags(io.stdout,
        flags + system.COF_VIRTUAL_TERMINAL_PROCESSING)

system.listconsoleflags(io.stdout) -- List again to check the differences
*/
static int lst_setconsoleflags(lua_State *L)
{
#ifdef _WIN32
    HANDLE console_handle = get_console_handle(L, 0);
    if (console_handle == NULL) {
        return 2; // error message is already on the stack
    }
    LSBF_BITFLAG new_console_mode = lsbf_checkbitflags(L, 2);

    if (!SetConsoleMode(console_handle, new_console_mode)) {
        termFormatError(L, GetLastError(), "failed to set console mode");
        return 2;
    }

#else
    get_console_handle(L); // to validate args
#endif

    lua_pushboolean(L, 1);
    return 1;
}



/***
Gets console flags (Windows).
The `CIF_` and `COF_` constants are available on the module table. Where `CIF` are the
input flags (for use with `io.stdin`) and `COF` are the output flags (for use with
`io.stdout`/`io.stderr`).

_Note_: See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
for more information on the flags.

@function getconsoleflags
@tparam file file file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
@treturn[1] bitflags the current console flags.
@treturn[2] nil
@treturn[2] string error message
@within Terminal_Windows
@usage
local system = require('system')

local flags = system.getconsoleflags(io.stdout)
print("Current stdout flags:", tostring(flags))

if flags:has_all_of(system.COF_VIRTUAL_TERMINAL_PROCESSING + system.COF_PROCESSED_OUTPUT) then
    print("Both flags are set")
else
    print("At least one flag is not set")
end
*/
static int lst_getconsoleflags(lua_State *L)
{
    DWORD console_mode = 0;

#ifdef _WIN32
    HANDLE console_handle = get_console_handle(L, 1);
    if (console_handle == NULL) {
        return 2; // error message is already on the stack
    }

    if (GetConsoleMode(console_handle, &console_mode) == 0)
    {
        termFormatError(L, GetLastError(), "failed to get console mode");
        return 2;
    }
#else
    get_console_handle(L); // to validate args

#endif
    lsbf_pushbitflags(L, console_mode);
    return 1;
}



/*-------------------------------------------------------------------------
 * Unix tcgetattr/tcsetattr functions
 *-------------------------------------------------------------------------*/
// Code modified from the LuaPosix library by Gary V. Vaughan
// see https://github.com/luaposix/luaposix

/***
Get termios state (Posix).
The terminal attributes is a table with the following fields:

- `iflag` input flags
- `oflag` output flags
- `lflag` local flags
- `cflag` control flags
- `ispeed` input speed
- `ospeed` output speed
- `cc` control characters

@function tcgetattr
@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
@treturn[1] termios terminal attributes, if successful. On Windows the bitflags are all 0, and the `cc` table is empty.
@treturn[2] nil
@treturn[2] string error message
@treturn[2] int errnum
@return error message if failed
@within Terminal_Posix
@usage
local system = require('system')

local status = assert(tcgetattr(io.stdin))
if status.iflag:has_all_of(system.I_IGNBRK) then
    print("Ignoring break condition")
end
*/
static int lst_tcgetattr(lua_State *L)
{
#ifndef _WIN32
    int r, i;
    struct termios t;
    int fd = get_console_handle(L);

    r = tcgetattr(fd, &t);
    if (r == -1) return pusherror(L, NULL);

    lua_newtable(L);
    lsbf_pushbitflags(L, t.c_iflag);
    lua_setfield(L, -2, "iflag");

    lsbf_pushbitflags(L, t.c_oflag);
    lua_setfield(L, -2, "oflag");

    lsbf_pushbitflags(L, t.c_lflag);
    lua_setfield(L, -2, "lflag");

    lsbf_pushbitflags(L, t.c_cflag);
    lua_setfield(L, -2, "cflag");

    lua_pushinteger(L, cfgetispeed(&t));
    lua_setfield(L, -2, "ispeed");

    lua_pushinteger(L, cfgetospeed(&t));
    lua_setfield(L, -2, "ospeed");

    lua_newtable(L);
    for (i=0; i<NCCS; i++)
    {
        lua_pushinteger(L, i);
        lua_pushinteger(L, t.c_cc[i]);
        lua_settable(L, -3);
    }
    lua_setfield(L, -2, "cc");

#else
    lua_settop(L, 1); // remove all but file handle
    get_console_handle(L, 1); //check args

    lua_newtable(L);
    lsbf_pushbitflags(L, 0);
    lua_setfield(L, -2, "iflag");
    lsbf_pushbitflags(L, 0);
    lua_setfield(L, -2, "oflag");
    lsbf_pushbitflags(L, 0);
    lua_setfield(L, -2, "lflag");
    lsbf_pushbitflags(L, 0);
    lua_setfield(L, -2, "cflag");
    lua_pushinteger(L, 0);
    lua_setfield(L, -2, "ispeed");
    lua_pushinteger(L, 0);
    lua_setfield(L, -2, "ospeed");
    lua_newtable(L);
    lua_setfield(L, -2, "cc");

#endif
    return 1;
}



/***
Set termios state (Posix).
This function will set the flags as given.

The `I_`, `O_`, and `L_` constants are available on the module table. They are the respective
flags for the `iflags`, `oflags`, and `lflags` bitmasks.

To see flag status and constant names check `listtermflags`. For their meaning check
[the manpage](https://www.man7.org/linux/man-pages/man3/termios.3.html).

_Note_: only `iflag`, `oflag`, and `lflag` are supported at the moment. The other fields are ignored.
@function tcsetattr
@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
@int actions one of `TCSANOW`, `TCSADRAIN`, `TCSAFLUSH`
@tparam table termios a table with bitflag fields:
@tparam[opt] bitflags termios.iflag if given will set the input flags
@tparam[opt] bitflags termios.oflag if given will set the output flags
@tparam[opt] bitflags termios.lflag if given will set the local flags
@treturn[1] bool `true`, if successful. Always returns `true` on Windows.
@return[2] nil
@treturn[2] string error message
@treturn[2] int errnum
@within Terminal_Posix
@usage
local system = require('system')

local status = assert(tcgetattr(io.stdin))
if not status.lflag:has_all_of(system.L_ECHO) then
    -- if echo is off, turn echoing newlines on
    tcsetattr(io.stdin, system.TCSANOW, { lflag = status.lflag + system.L_ECHONL }))
end
*/
static int lst_tcsetattr(lua_State *L)
{
#ifndef _WIN32
    struct termios t;
    int r;
    int fd = get_console_handle(L);     // first is the console handle
    int act = luaL_checkinteger(L, 2);  // second is the action to take

    r = tcgetattr(fd, &t);
    if (r == -1) return pusherror(L, NULL);

    t.c_iflag = lsbf_checkbitflagsfield(L, 3, "iflag", t.c_iflag);
    t.c_oflag = lsbf_checkbitflagsfield(L, 3, "oflag", t.c_oflag);
    t.c_lflag = lsbf_checkbitflagsfield(L, 3, "lflag", t.c_lflag);

    // Skipping the others for now

    // lua_getfield(L, 3, "cflag"); t.c_cflag = optint(L, -1, 0); lua_pop(L, 1);
    // lua_getfield(L, 3, "ispeed"); cfsetispeed( &t, optint(L, -1, B0) ); lua_pop(L, 1);
    // lua_getfield(L, 3, "ospeed"); cfsetospeed( &t, optint(L, -1, B0) ); lua_pop(L, 1);

    // lua_getfield(L, 3, "cc");
    // for (i=0; i<NCCS; i++)
    // {
    //     lua_pushinteger(L, i);
    //     lua_gettable(L, -2);
    //     t.c_cc[i] = optint(L, -1, 0);
    //     lua_pop(L, 1);
    // }

    r = tcsetattr(fd, act, &t);
    if (r == -1) return pusherror(L, NULL);

#else
    // Windows does not have a tcsetattr function, but we check arguments anyway
    luaL_checkinteger(L, 2);
    lsbf_checkbitflagsfield(L, 3, "iflag", 0);
    lsbf_checkbitflagsfield(L, 3, "oflag", 0);
    lsbf_checkbitflagsfield(L, 3, "lflag", 0);
    lua_settop(L, 1); // remove all but file handle
    get_console_handle(L, 1);
#endif

    lua_pushboolean(L, 1);
    return 1;
}



#ifndef _WIN32
/*
reopen FDs for independent file descriptions.
*/
static void reopen_fd(lua_State *L, int fd, int flags) {
    char path[64];
    int newfd = -1;

    if (fd != STDOUT_FILENO && fd != STDERR_FILENO) {
        luaL_error(L, "Invalid file descriptor: %d. Only stdout (1) and stderr (2) are supported.", fd);
    }

    const char *fallback_path = (fd == STDOUT_FILENO) ? "/dev/stdout" :
                                (fd == STDERR_FILENO) ? "/dev/stderr" : NULL;

    // If fd is a terminal, reopen its actual device (e.g. /dev/ttys003)
    // Works on all POSIX platforms that have terminals (macOS, Linux, BSD, etc.)
    if (isatty(fd)) {
        const char *tty = ttyname(fd);
        if (tty) {
            newfd = open(tty, flags);
        }
    }

    if (newfd < 0) {
        // For non-tty: try /dev/fd/N — POSIX-compliant and standard on macOS, Linux, BSD.
        // This gives a new file description even if the target is a file or pipe.
        snprintf(path, sizeof(path), "/dev/fd/%d", fd);
        newfd = open(path, flags);
    }

    if (newfd < 0) {
        // Fallback: for platforms/environments where /dev/fd/N doesn't exist.
        // /dev/stdout and /dev/stderr are standard on Linux, but may not create new descriptions.
        if (fallback_path) {
            newfd = open(fallback_path, flags);
        }
    }

    if (newfd < 0) {
        // All attempts failed — raise error with detailed info (call will not return)
        luaL_error(L, "Failed to reopen fd %d: tried ttyname(), /dev/fd/%d, and fallback %s: %s",
                    fd, fd, fallback_path ? fallback_path : "(none)", strerror(errno));
    }

    // Replace the original fd with the new one
    if (dup2(newfd, fd) < 0) {
        close(newfd);
        luaL_error(L, "dup2 failed for fd %d: %s", fd, strerror(errno));
    }

    close(newfd); // Close the new fd, as dup2 has replaced the original fd with it
}
#endif



/***
Creates new file descriptions for `stdout` and `stderr`.
Even if the file descriptors are unique, they still might point to the same
file description, and hence share settings like `O_NONBLOCK`. This means that
if one of them is set to non-blocking, the other will be as well. This can
lead to unexpected behavior.

This function is used to detach `stdout` and `stderr` from the original
file descriptions, and create new file descriptions for them. This allows
independent control of flags (e.g., `O_NONBLOCK`) on `stdout` and `stderr`,
avoiding shared side effects.

Does not modify `stdin` (fd 0), and does nothing on Windows.
@function detachfds
@return boolean `true` on success, or throws an error on failure.
@within Terminal_Posix
@see setnonblock
*/
static int lst_detachfds(lua_State *L) {
    static int already_detached = 0; // detaching is once per process(not per thread or Lua state)
    if (already_detached) {
        lua_pushnil(L);
        lua_pushliteral(L, "stdout and stderr already detached");
        return 1;
    }
    already_detached = 1;

#ifndef _WIN32
    // Reopen stdout and stderr with new file descriptions
    reopen_fd(L, STDOUT_FILENO, O_WRONLY);
    reopen_fd(L, STDERR_FILENO, O_WRONLY);
#endif

    lua_pushboolean(L, 1);
    return 1;
}



/***
Enables or disables non-blocking mode for a file (Posix).
Check `detachfds` in case there are shared file descriptions.
@function setnonblock
@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
@tparam boolean make_non_block a truthy value will enable non-blocking mode, a falsy value will disable it.
@treturn[1] bool `true`, if successful
@treturn[2] nil
@treturn[2] string error message
@treturn[2] int errnum
@within Terminal_Posix
@see getnonblock
@see detachfds
@usage
local sys = require('system')
sys.detachfds() -- detach stdout and stderr, so only stdin becomes non-blocking

-- set io.stdin to non-blocking mode
local old_setting = sys.getnonblock(io.stdin)
sys.setnonblock(io.stdin, true)

-- do stuff

-- restore old setting
sys.setnonblock(io.stdin, old_setting)
*/
static int lst_setnonblock(lua_State *L)
{
#ifndef _WIN32

    int fd = get_console_handle(L);

    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        return pusherror(L, "Error getting handle flags: ");
    }
    if (lua_toboolean(L, 2)) {
        // truthy: set non-blocking
        flags |= O_NONBLOCK;
    } else {
        // falsy: set disable non-blocking
        flags &= ~O_NONBLOCK;
    }
    if (fcntl(fd, F_SETFL, flags) == -1) {
        return pusherror(L, "Error changing O_NONBLOCK: ");
    }

#else
    if (lua_gettop(L) > 1) {
        lua_settop(L, 1); // use one argument, because the second boolean will fail as get_console_flags expects bitflags
    }
    HANDLE console_handle = get_console_handle(L, 1);
    if (console_handle == NULL) {
        return 2; // error message is already on the stack
    }

#endif

    lua_pushboolean(L, 1);
    return 1;
}



/***
Gets non-blocking mode status for a file (Posix).
@function getnonblock
@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
@treturn[1] bool `true` if set to non-blocking, `false` if not. Always returns `false` on Windows.
@treturn[2] nil
@treturn[2] string error message
@treturn[2] int errnum
@within Terminal_Posix
@see setnonblock
*/
static int lst_getnonblock(lua_State *L)
{
#ifndef _WIN32

    int fd = get_console_handle(L);

    // Set O_NONBLOCK
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        return pusherror(L, "Error getting handle flags: ");
    }
    if (flags & O_NONBLOCK) {
        lua_pushboolean(L, 1);
    } else {
        lua_pushboolean(L, 0);
    }

#else
    if (lua_gettop(L) > 1) {
        lua_settop(L, 1); // use one argument, because the second boolean will fail as get_console_flags expects bitflags
    }
    HANDLE console_handle = get_console_handle(L, 1);
    if (console_handle == NULL) {
        return 2; // error message is already on the stack
    }

    lua_pushboolean(L, 0);

#endif
    return 1;
}



/*-------------------------------------------------------------------------
 * Reading keyboard input
 *-------------------------------------------------------------------------*/

#ifdef _WIN32
// Define a static buffer for UTF-8 characters
static char utf8_buffer[4];
static int utf8_buffer_len = 0;
static int utf8_buffer_index = 0;
#endif


/***
Reads a key from the console non-blocking. This function should not be called
directly, but through the `system.readkey` or `system.readansi` functions. It
will return the next byte from the input stream, or `nil` if no key was pressed.

On Posix, `io.stdin` must be set to non-blocking mode using `setnonblock`
and canonical mode must be turned off using `tcsetattr`,
before calling this function. Otherwise it will block. No conversions are
done on Posix, so the byte read is returned as-is.

On Windows this reads a wide character and converts it to UTF-8. Multi-byte
sequences will be buffered internally and returned one byte at a time.

@function _readkey
@treturn[1] integer the byte read from the input stream
@treturn[2] nil if no key was pressed
@treturn[3] nil on error
@treturn[3] string error message
@treturn[3] int errnum (on posix)
@within Terminal_Input
*/
static int lst_readkey(lua_State *L) {
#ifdef _WIN32
    if (utf8_buffer_len > 0) {
        // Buffer not empty, return the next byte
        lua_pushinteger(L, (unsigned char)utf8_buffer[utf8_buffer_index]);
        utf8_buffer_index++;
        utf8_buffer_len--;
        // printf("returning from buffer: %d\n", luaL_checkinteger(L, -1));
        if (utf8_buffer_len == 0) {
            utf8_buffer_index = 0;
        }
        return 1;
    }

    if (!_kbhit()) {
        return 0;
    }

    wchar_t wc = _getwch();
    // printf("----\nread wchar_t: %x\n", (unsigned int)wc);

    if (wc == 0x00 || wc == 0xE0) {
        // printf("Ignoring scan code: %x\n", (unsigned int)wc);
        // On Windows, especially with key-repeat, we can get native scancodes even if we've set the console
        // to ANSI mode. Something, something, terminal not keeping up... These are not valid wide characters,
        // so we ignore them. Scan codes start with either 0x00 or 0xE0, and have a follow up byte, which we all ignore.
        // These codes are unique, so we do not risk dropping valid data.
        _getwch(); // Discard 2nd half of the scan code
        return 0;
    }

    if (wc == WEOF) {
        lua_pushnil(L);
        lua_pushliteral(L, "read error");
        return 2;
    }

    if (sizeof(wchar_t) == 2) {
        // printf("2-byte wchar_t\n");
        // only 2 bytes wide, not 4
        if (wc >= 0xD800 && wc <= 0xDBFF) {
            // printf("2-byte wchar_t, received high, getting low...\n");

            // we got a high surrogate, so we need to read the next one as the low surrogate
            if (!_kbhit()) {
                lua_pushnil(L);
                lua_pushliteral(L, "incomplete surrogate pair");
                return 2;
            }

            wchar_t wc2 = _getwch();
            // printf("read wchar_t 2: %x\n", (unsigned int)wc2);
            if (wc2 == WEOF) {
                lua_pushnil(L);
                lua_pushliteral(L, "read error");
                return 2;
            }

            if (wc2 < 0xDC00 || wc2 > 0xDFFF) {
                lua_pushnil(L);
                lua_pushliteral(L, "invalid surrogate pair");
                return 2;
            }
            // printf("2-byte pair complete now\n");
            wchar_t wch_pair[2] = { wc, wc2 };
            utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, wch_pair, 2, utf8_buffer, sizeof(utf8_buffer), NULL, NULL);

        } else {
            // printf("2-byte wchar_t, no surrogate pair\n");
            // not a high surrogate, so we can handle just the 2 bytes directly
            utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8_buffer, sizeof(utf8_buffer), NULL, NULL);
        }

    } else {
        // printf("4-byte wchar_t\n");
        // 4 bytes wide, so handle as UTF-32 directly
        utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8_buffer, sizeof(utf8_buffer), NULL, NULL);
    }
    // printf("utf8_buffer_len: %d\n", utf8_buffer_len);
    utf8_buffer_index = 0;
    if (utf8_buffer_len <= 0) {
        termFormatError(L, GetLastError(), "UTF-8 conversion error");
        return 2;
    }

    lua_pushinteger(L, (unsigned char)utf8_buffer[utf8_buffer_index]);
    utf8_buffer_index++;
    utf8_buffer_len--;
    // printf("returning from buffer: %x\n", luaL_checkinteger(L, -1));
    return 1;

#else
    // Posix implementation
    char ch;
    ssize_t bytes_read = read(STDIN_FILENO, &ch, 1);
    if (bytes_read > 0) {
        lua_pushinteger(L, (unsigned char)ch);
        return 1;

    } else if (bytes_read == 0) {
        return 0;  // End of file or stream closed

    } else {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            // Resource temporarily unavailable, no data available to read
            return 0;
        } else {
            return pusherror(L, "read error");
        }
    }

#endif
}



/*-------------------------------------------------------------------------
 * Retrieve terminal size
 *-------------------------------------------------------------------------*/


/***
Get the size of the terminal in rows and columns.
@function termsize
@treturn[1] int the number of rows
@treturn[1] int the number of columns
@treturn[2] nil
@treturn[2] string error message
*/
static int lst_termsize(lua_State *L) {
    int columns, rows;

#ifdef _WIN32
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    if (!GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
        termFormatError(L, GetLastError(), "Failed to get terminal size.");
        return 2;
    }
    columns = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;

#else
    struct winsize ws;
    if (ioctl(1, TIOCGWINSZ, &ws) == -1) {
        return pusherror(L, "Failed to get terminal size.");
    }
    columns = ws.ws_col;
    rows = ws.ws_row;

#endif
    lua_pushinteger(L, rows);
    lua_pushinteger(L, columns);
    return 2;
}



/*-------------------------------------------------------------------------
 * utf8 conversion and support
 *-------------------------------------------------------------------------*/

// Function to convert a single UTF-8 character to a Unicode code point (uint32_t)
// To prevent having to do codepage/locale changes, we use a custom implementation
int utf8_to_wchar(const char *utf8, size_t len, mk_wchar_t *codepoint) {
    if (len == 0) {
        return -1; // No input provided
    }

    unsigned char c = (unsigned char)utf8[0];
    if (c <= 0x7F) {
        *codepoint = c;
        return 1;
    } else if ((c & 0xE0) == 0xC0) {
        if (len < 2) return -1; // Not enough bytes
        *codepoint = ((utf8[0] & 0x1F) << 6) | (utf8[1] & 0x3F);
        return 2;
    } else if ((c & 0xF0) == 0xE0) {
        if (len < 3) return -1; // Not enough bytes
        *codepoint = ((utf8[0] & 0x0F) << 12) | ((utf8[1] & 0x3F) << 6) | (utf8[2] & 0x3F);
        return 3;
    } else if ((c & 0xF8) == 0xF0) {
        if (len < 4) return -1; // Not enough bytes
        *codepoint = ((utf8[0] & 0x07) << 18) | ((utf8[1] & 0x3F) << 12) | ((utf8[2] & 0x3F) << 6) | (utf8[3] & 0x3F);
        return 4;
    } else {
        // Invalid UTF-8 character
        return -1;
    }
}


/***
Get the width of a utf8 character for terminal display.
@function utf8cwidth
@tparam string|int utf8_char the utf8 character, or unicode codepoint, to check, only the width of the first character will be returned
@tparam[opt=1] int ambiguous_width the width to return for ambiguous width characters (usually 1 or 2)
@treturn[1] int the display width in columns of the first character in the string (0 for an empty string)
@treturn[2] nil
@treturn[2] string error message
@within Terminal_UTF-8
*/
int lst_utf8cwidth(lua_State *L) {
    int width = 0;
    mk_wchar_t wc;
    int ambiguous_width = luaL_optinteger(L, 2, 1);

    if (lua_type(L, 1) == LUA_TSTRING) {
        // Handle UTF8 as string input
        const char *utf8_char;
        size_t utf8_len;
        utf8_char = luaL_checklstring(L, 1, &utf8_len);

        if (utf8_len == 0) {
            lua_pushinteger(L, 0);
            return 1;
        }

        // Convert the UTF-8 string to a wide character
        int bytes_processed = utf8_to_wchar(utf8_char, utf8_len, &wc);
        if (bytes_processed == -1) {
            lua_pushnil(L);
            lua_pushstring(L, "Invalid UTF-8 character");
            return 2;
        }

    } else if (lua_type(L, 1) == LUA_TNUMBER) {
        // Handle codepoint input
        int codepoint = luaL_checkinteger(L, 1);

        if (codepoint < 0 || codepoint > 0x10FFFF) {
            lua_pushnil(L);
            lua_pushstring(L, "Invalid Unicode codepoint");
            return 2;
        }
        wc = (mk_wchar_t)codepoint;

    } else {
        return luaL_argerror(L, 1, "Expected UTF-8-string or codepoint-integer as first argument");
    }

    // Get the width of the wide character
    width = mk_wcwidth(wc, ambiguous_width);
    if (width == -1) {
        lua_pushnil(L);
        lua_pushstring(L, "Control characters have no width");
        return 2;
    }

    lua_pushinteger(L, width);
    return 1;
}




/***
Get the width of a utf8 string for terminal display.
@function utf8swidth
@tparam string utf8_string the utf8 string to check
@tparam[opt=1] int ambiguous_width the width to return for ambiguous width characters (1 or 2)
@treturn[1] int the display width of the string in columns (0 for an empty string)
@treturn[2] nil
@treturn[2] string error message
@within Terminal_UTF-8
*/
int lst_utf8swidth(lua_State *L) {
    const char *utf8_str;
    size_t utf8_len;
    utf8_str = luaL_checklstring(L, 1, &utf8_len);
    int ambiguous_width = luaL_optinteger(L, 2, 1);
    if (ambiguous_width != 1 && ambiguous_width != 2) {
        return luaL_argerror(L, 2, "Ambiguous width must be 1 or 2");
    }
    int total_width = 0;

    if (utf8_len == 0) {
        lua_pushinteger(L, 0);
        return 1;
    }

    int bytes_processed = 0;
    size_t i = 0;
    mk_wchar_t wc;

    while (i < utf8_len) {
        bytes_processed = utf8_to_wchar(utf8_str + i, utf8_len - i, &wc);
        if (bytes_processed == -1) {
            lua_pushnil(L);
            lua_pushstring(L, "Invalid UTF-8 character");
            return 2;
        }

        int width = mk_wcwidth(wc, ambiguous_width);
        if (width == -1) {
            lua_pushnil(L);
            lua_pushstring(L, "Control characters have no width");
            return 2;
        }

        total_width += width;
        i += bytes_processed;
    }

    lua_pushinteger(L, total_width);
    return 1;
}



/*-------------------------------------------------------------------------
 * Windows codepage functions
 *-------------------------------------------------------------------------*/


/***
Gets the current console code page (Windows).
@function getconsolecp
@treturn[1] int the current code page (always 65001 on Posix systems)
@treturn[2] nil
@treturn[2] string error message
@within Terminal_UTF-8
*/
static int lst_getconsolecp(lua_State *L) {
    unsigned int cp = 65001;
#ifdef _WIN32
    cp = GetConsoleCP();
    if (cp == 0) {
        termFormatError(L, GetLastError(), "failed to get console code page");
        return 2;
    }
#endif
    lua_pushinteger(L, cp);
    return 1;
}



/***
Sets the current console code page (Windows).
@function setconsolecp
@tparam int cp the code page to set, use `system.CODEPAGE_UTF8` (65001) for UTF-8
@treturn[1] bool `true` on success (always `true` on Posix systems)
@treturn[2] nil
@treturn[2] string error message
@within Terminal_UTF-8
*/
static int lst_setconsolecp(lua_State *L) {
    unsigned int cp = (unsigned int)luaL_checkinteger(L, 1);
#ifdef _WIN32
    if (!SetConsoleCP(cp)) {
        termFormatError(L, GetLastError(), "failed to set console code page");
        return 2;
    }
#else
    (void)cp;
#endif
    lua_pushboolean(L, 1);
    return 1;
}



/***
Gets the current console output code page (Windows).
@function getconsoleoutputcp
@treturn[1] int the current code page (always 65001 on Posix systems)
@treturn[2] nil
@treturn[2] string error message
@within Terminal_UTF-8
*/
static int lst_getconsoleoutputcp(lua_State *L) {
    unsigned int cp = 65001;
#ifdef _WIN32
    cp = GetConsoleOutputCP();
    if (cp == 0) {
        termFormatError(L, GetLastError(), "failed to get console output code page");
        return 2;
    }
#endif
    lua_pushinteger(L, cp);
    return 1;
}



/***
Sets the current console output code page (Windows).
@function setconsoleoutputcp
@tparam int cp the code page to set, use `system.CODEPAGE_UTF8` (65001) for UTF-8
@treturn[1] bool `true` on success (always `true` on Posix systems)
@treturn[2] nil
@treturn[2] string error message
@within Terminal_UTF-8
*/
static int lst_setconsoleoutputcp(lua_State *L) {
    unsigned int cp = (unsigned int)luaL_checkinteger(L, 1);
#ifdef _WIN32
    if (!SetConsoleOutputCP(cp)) {
        termFormatError(L, GetLastError(), "failed to set console output code page");
        return 2;
    }
#else
    (void)cp;
#endif
    lua_pushboolean(L, 1);
    return 1;
}



/*-------------------------------------------------------------------------
 * Initializes module
 *-------------------------------------------------------------------------*/

static luaL_Reg func[] = {
    { "isatty", lst_isatty },
    { "getconsoleflags", lst_getconsoleflags },
    { "setconsoleflags", lst_setconsoleflags },
    { "tcgetattr", lst_tcgetattr },
    { "tcsetattr", lst_tcsetattr },
    { "detachfds", lst_detachfds },
    { "getnonblock", lst_getnonblock },
    { "setnonblock", lst_setnonblock },
    { "_readkey", lst_readkey },
    { "termsize", lst_termsize },
    { "utf8cwidth", lst_utf8cwidth },
    { "utf8swidth", lst_utf8swidth },
    { "getconsolecp", lst_getconsolecp },
    { "setconsolecp", lst_setconsolecp },
    { "getconsoleoutputcp", lst_getconsoleoutputcp },
    { "setconsoleoutputcp", lst_setconsoleoutputcp },
    { NULL, NULL }
};



void term_open(lua_State *L) {
    // set up constants and export the constants in module table
    initialize_valid_flags();
    // Windows flags
    for (int i = 0; win_console_in_flags[i].name != NULL; i++)
    {
        lsbf_pushbitflags(L, win_console_in_flags[i].value);
        lua_setfield(L, -2, win_console_in_flags[i].name);
    }
    for (int i = 0; win_console_out_flags[i].name != NULL; i++)
    {
        lsbf_pushbitflags(L, win_console_out_flags[i].value);
        lua_setfield(L, -2, win_console_out_flags[i].name);
    }
    // Unix flags
    for (int i = 0; nix_console_i_flags[i].name != NULL; i++)
    {
        lsbf_pushbitflags(L, nix_console_i_flags[i].value);
        lua_setfield(L, -2, nix_console_i_flags[i].name);
    }
    for (int i = 0; nix_console_o_flags[i].name != NULL; i++)
    {
        lsbf_pushbitflags(L, nix_console_o_flags[i].value);
        lua_setfield(L, -2, nix_console_o_flags[i].name);
    }
    for (int i = 0; nix_console_l_flags[i].name != NULL; i++)
    {
        lsbf_pushbitflags(L, nix_console_l_flags[i].value);
        lua_setfield(L, -2, nix_console_l_flags[i].name);
    }
    // Unix tcsetattr actions
    for (int i = 0; nix_tcsetattr_actions[i].name != NULL; i++)
    {
        lua_pushinteger(L, nix_tcsetattr_actions[i].value);
        lua_setfield(L, -2, nix_tcsetattr_actions[i].name);
    }

    // export functions
    luaL_setfuncs(L, func, 0);
}
