#include <compat.h> // IWYU pragma: keep

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <uv.h>

#ifdef _WIN32
#include <io.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#define ANT_ISATTY _isatty
#define ANT_STDIN_FD 0
#define ANT_STDOUT_FD 1
#define ANT_STDERR_FD 2
#else
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#define ANT_ISATTY isatty
#define ANT_STDIN_FD STDIN_FILENO
#define ANT_STDOUT_FD STDOUT_FILENO
#define ANT_STDERR_FD STDERR_FILENO
#endif

#include "ant.h"
#include "gc/roots.h"
#include "descriptors.h"
#include "errors.h"
#include "internal.h"
#include "runtime.h"
#include "tty_ctrl.h"
#include "silver/engine.h"

#include "modules/stream.h"
#include "modules/symbol.h"
#include "modules/tty.h"

static ant_value_t g_tty_readstream_proto = 0;
static ant_value_t g_tty_readstream_ctor = 0;
static ant_value_t g_tty_writestream_proto = 0;
static ant_value_t g_tty_writestream_ctor = 0;

static bool parse_fd(ant_value_t value, int *fd_out) {
  int fd = 0;
  if (!tty_ctrl_parse_int_value(value, &fd)) return false;
  if (fd < 0) return false;
  *fd_out = fd;
  return true;
}

static bool is_tty_fd(int fd) {
  if (fd < 0) return false;
  return uv_guess_handle(fd) == UV_TTY;
}

static int stream_fd_from_this(ant_t *js, int fallback_fd) {
  ant_value_t this_obj = js_getthis(js);
  if (!is_special_object(this_obj)) return fallback_fd;

  ant_value_t fd_val = js_get(js, this_obj, "fd");
  int fd = 0;
  if (!parse_fd(fd_val, &fd)) return fallback_fd;
  return fd;
}

static void get_tty_size(int fd, int *rows, int *cols) {
  int out_rows = 24;
  int out_cols = 80;

#ifdef _WIN32
  HANDLE handle = INVALID_HANDLE_VALUE;
  if (fd == ANT_STDOUT_FD) {
    handle = GetStdHandle(STD_OUTPUT_HANDLE);
  } else if (fd == ANT_STDERR_FD) {
    handle = GetStdHandle(STD_ERROR_HANDLE);
  } else {
    intptr_t os_handle = _get_osfhandle(fd);
    if (os_handle != -1) handle = (HANDLE)os_handle;
  }

  if (handle != INVALID_HANDLE_VALUE) {
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    if (GetConsoleScreenBufferInfo(handle, &csbi)) {
      int width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
      int height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
      if (height > 0) out_rows = height;
      if (width > 0) out_cols = width;
    }
  }
#else
  struct winsize ws;
  if (ioctl(fd, TIOCGWINSZ, &ws) == 0) {
    if (ws.ws_row > 0) out_rows = (int)ws.ws_row;
    if (ws.ws_col > 0) out_cols = (int)ws.ws_col;
  }
#endif

  if (rows) *rows = out_rows;
  if (cols) *cols = out_cols;
}

static bool str_case_eq(const char *a, const char *b) {
  if (!a || !b) return false;
  while (*a && *b) {
    int ca = tolower((unsigned char)*a);
    int cb = tolower((unsigned char)*b);
    if (ca != cb) return false;
    a++;
    b++;
  }
  return *a == '\0' && *b == '\0';
}

static bool str_case_contains(const char *haystack, const char *needle) {
  if (!haystack || !needle) return false;
  size_t needle_len = strlen(needle);
  if (needle_len == 0) return true;

  size_t hay_len = strlen(haystack);
  if (needle_len > hay_len) return false;

  for (size_t i = 0; i + needle_len <= hay_len; i++) {
    bool match = true;
    for (size_t j = 0; j < needle_len; j++) {
      int ca = tolower((unsigned char)haystack[i + j]);
      int cb = tolower((unsigned char)needle[j]);
      if (ca != cb) {
        match = false;
        break;
      }
    }
    if (match) return true;
  }
  return false;
}

static bool read_env_object_value(
  ant_t *js,
  ant_value_t env_obj,
  const char *key,
  char *buf,
  size_t buf_len
) {
  if (!is_special_object(env_obj) || !key || !buf || buf_len == 0) return false;

  ant_value_t value = js_get(js, env_obj, key);
  if (vtype(value) == T_UNDEF || vtype(value) == T_NULL) return false;

  ant_value_t str_val = js_tostring_val(js, value);
  size_t len = 0;
  char *str = js_getstr(js, str_val, &len);
  if (!str) return false;

  if (len >= buf_len) len = buf_len - 1;
  memcpy(buf, str, len);
  buf[len] = '\0';
  return true;
}

static const char *get_env_value(
  ant_t *js,
  ant_value_t env_obj,
  const char *key,
  char *buf,
  size_t buf_len
) {
  if (read_env_object_value(js, env_obj, key, buf, buf_len)) return buf;
  return getenv(key);
}

static int force_color_depth(const char *force_color) {
  if (!force_color) return 0;
  if (*force_color == '\0') return 4;
  if (strcmp(force_color, "0") == 0) return 1;
  if (strcmp(force_color, "1") == 0) return 4;
  if (strcmp(force_color, "2") == 0) return 8;
  if (strcmp(force_color, "3") == 0) return 24;
  return 4;
}

static int detect_color_depth(ant_t *js, int fd, ant_value_t env_obj) {
  char scratch[128];
  const char *force_color = get_env_value(js, env_obj, "FORCE_COLOR", scratch, sizeof(scratch));
  
  int forced = force_color_depth(force_color);
  if (forced > 0) return forced;

  const char *no_color = get_env_value(js, env_obj, "NO_COLOR", scratch, sizeof(scratch));
  if (no_color && *no_color) return 1;
  if (!is_tty_fd(fd)) return 1;

  const char *colorterm = get_env_value(js, env_obj, "COLORTERM", scratch, sizeof(scratch));
  if (colorterm) {
    if (str_case_contains(colorterm, "truecolor") || str_case_contains(colorterm, "24bit")) return 24;
  }

#ifdef _WIN32
  return 24;
#else
  const char *term = get_env_value(js, env_obj, "TERM", scratch, sizeof(scratch));
  if (!term) return 4;
  if (str_case_eq(term, "dumb")) return 1;
  if (str_case_contains(term, "256color")) return 8;
  if (str_case_contains(term, "color")
    || str_case_contains(term, "xterm")
    || str_case_contains(term, "screen")
    || str_case_contains(term, "ansi")
    || str_case_contains(term, "linux")
    || str_case_contains(term, "cygwin")
    || str_case_contains(term, "vt100")
  ) return 4;
  return 4;
#endif
}

static int palette_size_for_depth(int depth) {
  switch (depth) {
    case 1: return 2;
    case 4: return 16;
    case 8: return 256;
    case 24: return 16777216;
    default: return 2;
  }
}

static ant_value_t make_stream_error(ant_t *js, const char *op, int fd) {
  return js_mkerr_typed(js, JS_ERR_GENERIC, "tty stream %s failed for fd %d", op, fd);
}

static void invoke_callback_if_needed(ant_t *js, ant_value_t cb, ant_value_t arg) {
  if (!is_callable(cb)) return;
  ant_value_t cb_args[1] = { arg };
  sv_vm_call(js->vm, js, cb, js_mkundef(), cb_args, 1, NULL, false);
}

static ant_value_t maybe_callback_or_throw(
  ant_t *js,
  ant_value_t this_obj,
  ant_value_t cb,
  bool ok,
  const char *op,
  int fd
) {
  if (is_callable(cb)) {
    if (ok) {
      invoke_callback_if_needed(js, cb, js_mknull());
    } else {
      ant_value_t err = make_stream_error(js, op, fd);
      invoke_callback_if_needed(js, cb, err);
    }
    return this_obj;
  }
  if (!ok) return make_stream_error(js, op, fd);
  return this_obj;
}

#ifndef _WIN32
static struct {
  int fd;
  bool active;
  struct termios saved;
} raw_state = { .fd = -1, .active = false };
#endif

static bool set_raw_mode_fd(int fd, bool enable) {
#ifdef _WIN32
  intptr_t os_handle = _get_osfhandle(fd);
  if (os_handle == -1) return false;
  HANDLE handle = (HANDLE)os_handle;

  DWORD mode = 0;
  if (!GetConsoleMode(handle, &mode)) return false;

  if (enable) {
    mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
  } else {
    mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
  }

  return SetConsoleMode(handle, mode) != 0;
#else
  if (!is_tty_fd(fd)) return false;

  if (enable) {
    if (raw_state.active && raw_state.fd == fd) return true;

    struct termios saved;
    if (tcgetattr(fd, &saved) == -1) return false;

    struct termios raw = saved;
    raw.c_lflag &= ~(ICANON | ECHO | ISIG);
    raw.c_iflag &= ~(IXON | ICRNL);
    raw.c_cc[VMIN] = 1;
    raw.c_cc[VTIME] = 0;

    if (tcsetattr(fd, TCSANOW, &raw) == -1) return false;
    raw_state.fd = fd;
    raw_state.saved = saved;
    raw_state.active = true;
    return true;
  }

  if (!(raw_state.active && raw_state.fd == fd)) return true;
  if (tcsetattr(fd, TCSANOW, &raw_state.saved) == -1) return false;
  raw_state.fd = -1;
  raw_state.active = false;
  return true;
#endif
}

static ant_value_t get_process_stream(ant_t *js, const char *name) {
  ant_value_t process_obj = js_get(js, js_glob(js), "process");
  if (!is_special_object(process_obj)) return js_mkundef();

  ant_value_t stream = js_get(js, process_obj, name);
  if (!is_special_object(stream)) return js_mkundef();
  return stream;
}

static void ensure_stream_common_props(ant_t *js, ant_value_t stream, int fd) {
  if (!is_special_object(stream)) return;
  js_set(js, stream, "fd", js_mknum((double)fd));
  js_set(js, stream, "isTTY", js_bool(is_tty_fd(fd)));
}

static ant_value_t tty_isatty(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_false;

  int fd = 0;
  if (!parse_fd(args[0], &fd)) return js_false;
  return js_bool(ANT_ISATTY(fd) != 0);
}

static ant_value_t tty_stream_write(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this_obj = js_getthis(js);
  if (nargs < 1) return js_false;

  size_t len = 0;
  char *data = js_getstr(js, args[0], &len);
  if (!data) return js_false;

  ant_value_t cb = js_mkundef();
  if (nargs > 1 && is_callable(args[1])) cb = args[1];

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  bool ok = tty_ctrl_write_fd(fd, data, len);

  if (is_callable(cb)) {
    if (ok) invoke_callback_if_needed(js, cb, js_mknull());
    else invoke_callback_if_needed(js, cb, make_stream_error(js, "write", fd));
  }

  if (!ok) return js_false;
  (void)this_obj;
  return js_true;
}

static ant_value_t tty_write_stream_rows_getter(ant_t *js, ant_value_t *args, int nargs) {
  (void)args; (void)nargs;

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  if (!is_tty_fd(fd)) return js_mkundef();

  int rows = 0;
  int cols = 0;
  get_tty_size(fd, &rows, &cols);
  return js_mknum((double)rows);
}

static ant_value_t tty_write_stream_columns_getter(ant_t *js, ant_value_t *args, int nargs) {
  (void)args; (void)nargs;

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  if (!is_tty_fd(fd)) return js_mkundef();

  int rows = 0;
  int cols = 0;
  get_tty_size(fd, &rows, &cols);
  return js_mknum((double)cols);
}

static ant_value_t tty_write_stream_get_window_size(ant_t *js, ant_value_t *args, int nargs) {
  (void)args; (void)nargs;

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  int rows = 0;
  int cols = 0;
  get_tty_size(fd, &rows, &cols);

  ant_value_t arr = js_mkarr(js);
  js_arr_push(js, arr, js_mknum((double)cols));
  js_arr_push(js, arr, js_mknum((double)rows));
  return arr;
}

static ant_value_t tty_write_stream_clear_line(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this_obj = js_getthis(js);

  int dir = 0;
  if (!tty_ctrl_parse_clear_line_dir(args, nargs, 0, &dir)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "clearLine(dir) requires a numeric dir");
  }

  ant_value_t cb = js_mkundef();
  if (nargs > 1 && is_callable(args[1])) cb = args[1];

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  size_t seq_len = 0;
  const char *seq = tty_ctrl_clear_line_seq(dir, &seq_len);
  bool ok = tty_ctrl_write_fd(fd, seq, seq_len);
  return maybe_callback_or_throw(js, this_obj, cb, ok, "clearLine", fd);
}

static ant_value_t tty_write_stream_clear_screen_down(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this_obj = js_getthis(js);

  ant_value_t cb = js_mkundef();
  if (nargs > 0 && is_callable(args[0])) cb = args[0];

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  size_t seq_len = 0;
  const char *seq = tty_ctrl_clear_screen_down_seq(&seq_len);
  bool ok = tty_ctrl_write_fd(fd, seq, seq_len);
  return maybe_callback_or_throw(js, this_obj, cb, ok, "clearScreenDown", fd);
}

static ant_value_t tty_write_stream_cursor_to(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this_obj = js_getthis(js);
  int x = 0;
  if (nargs < 1 || !tty_ctrl_parse_int_value(args[0], &x)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "cursorTo(x[, y][, callback]) requires numeric x");
  }

  x = tty_ctrl_normalize_coord(x);

  bool has_y = false;
  int y = 0;
  ant_value_t cb = js_mkundef();

  if (nargs > 1) {
    if (is_callable(args[1])) {
      cb = args[1];
    } else if (vtype(args[1]) == T_UNDEF) {
      // no-op
    } else if (tty_ctrl_parse_int_value(args[1], &y)) {
      has_y = true;
      y = tty_ctrl_normalize_coord(y);
      if (nargs > 2 && is_callable(args[2])) cb = args[2];
    } else {
      return js_mkerr_typed(js, JS_ERR_TYPE, "cursorTo y must be a number when provided");
    }
  }

  char seq[64];
  size_t seq_len = 0;
  if (!tty_ctrl_build_cursor_to(seq, sizeof(seq), x, has_y, y, &seq_len)) {
    return js_mkerr(js, "Failed to build cursor sequence");
  }

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  bool ok = tty_ctrl_write_fd(fd, seq, seq_len);
  return maybe_callback_or_throw(js, this_obj, cb, ok, "cursorTo", fd);
}

static ant_value_t tty_write_stream_move_cursor(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this_obj = js_getthis(js);
  if (nargs < 2) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "moveCursor(dx, dy[, callback]) requires dx and dy");
  }

  int dx = 0;
  int dy = 0;
  if (!tty_ctrl_parse_move_cursor_args(args, nargs, 0, 1, &dx, &dy)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "moveCursor(dx, dy[, callback]) requires numeric dx and dy");
  }

  ant_value_t cb = js_mkundef();
  if (nargs > 2 && is_callable(args[2])) cb = args[2];

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  bool ok = true;

  if (dx != 0) {
    char seq_x[32];
    size_t len_x = 0;
    if (!tty_ctrl_build_move_cursor_axis(seq_x, sizeof(seq_x), dx, true, &len_x)) {
      return js_mkerr(js, "Failed to build moveCursor sequence");
    }
    if (!tty_ctrl_write_fd(fd, seq_x, len_x)) ok = false;
  }

  if (ok && dy != 0) {
    char seq_y[32];
    size_t len_y = 0;
    if (!tty_ctrl_build_move_cursor_axis(seq_y, sizeof(seq_y), dy, false, &len_y)) {
      return js_mkerr(js, "Failed to build moveCursor sequence");
    }
    if (!tty_ctrl_write_fd(fd, seq_y, len_y)) ok = false;
  }

  return maybe_callback_or_throw(js, this_obj, cb, ok, "moveCursor", fd);
}

static ant_value_t tty_write_stream_get_color_depth(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t env_obj = js_mkundef();
  if (nargs > 0 && is_special_object(args[0])) env_obj = args[0];

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  int depth = detect_color_depth(js, fd, env_obj);
  return js_mknum((double)depth);
}

static ant_value_t tty_write_stream_has_colors(ant_t *js, ant_value_t *args, int nargs) {
  int count = 16;
  ant_value_t env_obj = js_mkundef();

  if (nargs > 0) {
    if (vtype(args[0]) == T_NUM) {
      int parsed_count = 16;
      if (!tty_ctrl_parse_int_value(args[0], &parsed_count)) {
        return js_mkerr_typed(js, JS_ERR_TYPE, "hasColors(count[, env]) count must be an integer");
      }
      count = parsed_count;
    } else if (is_special_object(args[0])) {
      env_obj = args[0];
    } else if (vtype(args[0]) != T_UNDEF) {
      return js_mkerr_typed(js, JS_ERR_TYPE, "hasColors(count[, env]) invalid first argument");
    }
  }

  if (nargs > 1 && is_special_object(args[1])) env_obj = args[1];
  if (count < 1) count = 1;

  int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
  int depth = detect_color_depth(js, fd, env_obj);
  int max_colors = palette_size_for_depth(depth);
  return js_bool(max_colors >= count);
}

static ant_value_t tty_read_stream_set_raw_mode(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this_obj = js_getthis(js);
  if (!is_special_object(this_obj)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "setRawMode() requires a ReadStream receiver");
  }

  bool enable = nargs > 0 ? js_truthy(js, args[0]) : true;

  ant_value_t native_fn = js_get(js, this_obj, "__antNativeSetRawMode");
  if (is_callable(native_fn)) {
    ant_value_t call_args[1];
    int call_nargs = 0;
    if (nargs > 0) {
      call_args[0] = args[0];
      call_nargs = 1;
    }
    ant_value_t result = sv_vm_call(
      js->vm, js, native_fn, this_obj,
      call_nargs > 0 ? call_args : NULL, call_nargs,
      NULL, false
    );
    if (vtype(result) == T_ERR) return result;
    js_set(js, this_obj, "isRaw", js_bool(js_truthy(js, result) && enable));
    return this_obj;
  }

  int fd = stream_fd_from_this(js, ANT_STDIN_FD);
  if (!set_raw_mode_fd(fd, enable)) {
    return js_mkerr_typed(js, JS_ERR_GENERIC, "Failed to set raw mode for fd %d", fd);
  }
  js_set(js, this_obj, "isRaw", js_bool(enable));
  return this_obj;
}

static ant_value_t tty_read_stream_constructor(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream(fd) requires a file descriptor");

  int fd = 0;
  if (!parse_fd(args[0], &fd)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream(fd) requires an integer file descriptor");
  }
  if (!is_tty_fd(fd)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream fd %d is not a TTY", fd);
  }

  if (fd == ANT_STDIN_FD) {
    ant_value_t stdin_obj = get_process_stream(js, "stdin");
    if (is_special_object(stdin_obj)) {
      ensure_stream_common_props(js, stdin_obj, fd);
      if (vtype(js_get(js, stdin_obj, "isRaw")) == T_UNDEF) js_set(js, stdin_obj, "isRaw", js_false);
      return stdin_obj;
    }
  }

  ant_value_t obj = stream_construct_readable(js, g_tty_readstream_proto, js_mkundef());
  if (is_err(obj)) return obj;

  ensure_stream_common_props(js, obj, fd);
  js_set(js, obj, "isRaw", js_false);
  return obj;
}

static ant_value_t tty_write_stream_constructor(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "WriteStream(fd) requires a file descriptor");

  int fd = 0;
  if (!parse_fd(args[0], &fd)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "WriteStream(fd) requires an integer file descriptor");
  }
  if (!is_tty_fd(fd)) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "WriteStream fd %d is not a TTY", fd);
  }

  if (fd == ANT_STDOUT_FD || fd == ANT_STDERR_FD) {
    ant_value_t stream = get_process_stream(js, fd == ANT_STDOUT_FD ? "stdout" : "stderr");
    if (is_special_object(stream)) {
      ensure_stream_common_props(js, stream, fd);
      return stream;
    }
  }

  ant_value_t obj = stream_construct_writable(js, g_tty_writestream_proto, js_mkundef());
  if (is_err(obj)) return obj;

  ensure_stream_common_props(js, obj, fd);
  return obj;
}

static void setup_readstream_proto(ant_t *js, ant_value_t proto) {
  if (!is_special_object(proto)) return;
  js_set(js, proto, "setRawMode", js_mkfun(tty_read_stream_set_raw_mode));
  js_set_sym(js, proto, get_toStringTag_sym(), js_mkstr(js, "ReadStream", 10));
}

static void setup_writestream_proto(ant_t *js, ant_value_t proto) {
  if (!is_special_object(proto)) return;

  js_set(js, proto, "write", js_mkfun(tty_stream_write));
  js_set(js, proto, "clearLine", js_mkfun(tty_write_stream_clear_line));
  js_set(js, proto, "clearScreenDown", js_mkfun(tty_write_stream_clear_screen_down));
  js_set(js, proto, "cursorTo", js_mkfun(tty_write_stream_cursor_to));
  js_set(js, proto, "moveCursor", js_mkfun(tty_write_stream_move_cursor));
  js_set(js, proto, "getWindowSize", js_mkfun(tty_write_stream_get_window_size));
  js_set(js, proto, "getColorDepth", js_mkfun(tty_write_stream_get_color_depth));
  js_set(js, proto, "hasColors", js_mkfun(tty_write_stream_has_colors));
  js_set_getter_desc(js, proto, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C);
  js_set_getter_desc(js, proto, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C);
  js_set_sym(js, proto, get_toStringTag_sym(), js_mkstr(js, "WriteStream", 11));
}

static void tty_init_stream_constructors(ant_t *js) {
  if (g_tty_readstream_ctor && g_tty_writestream_ctor) return;
  stream_init_constructors(js);

  g_tty_readstream_proto = js_mkobj(js);
  js_set_proto_init(g_tty_readstream_proto, stream_readable_prototype(js));
  setup_readstream_proto(js, g_tty_readstream_proto);
  
  g_tty_readstream_ctor = js_make_ctor(js, tty_read_stream_constructor, g_tty_readstream_proto, "ReadStream", 10);
  js_set_proto_init(g_tty_readstream_ctor, stream_readable_constructor(js));
  
  gc_register_root(&g_tty_readstream_proto);
  gc_register_root(&g_tty_readstream_ctor);

  g_tty_writestream_proto = js_mkobj(js);
  js_set_proto_init(g_tty_writestream_proto, stream_writable_prototype(js));
  setup_writestream_proto(js, g_tty_writestream_proto);
  
  g_tty_writestream_ctor = js_make_ctor(js, tty_write_stream_constructor, g_tty_writestream_proto, "WriteStream", 11);
  js_set_proto_init(g_tty_writestream_ctor, stream_writable_constructor(js));
  
  gc_register_root(&g_tty_writestream_proto);
  gc_register_root(&g_tty_writestream_ctor);
}

void init_tty_module(void) {
  ant_t *js = rt->js;
  if (!js) return;
  tty_init_stream_constructors(js);

  ant_value_t process_obj = js_get(js, js_glob(js), "process");
  if (!is_special_object(process_obj)) return;

  ant_value_t stdin_obj = js_get(js, process_obj, "stdin");
  if (is_special_object(stdin_obj)) {
    ensure_stream_common_props(js, stdin_obj, ANT_STDIN_FD);
    if (vtype(js_get(js, stdin_obj, "isRaw")) == T_UNDEF) js_set(js, stdin_obj, "isRaw", js_false);

    ant_value_t stdin_proto = js_get_proto(js, stdin_obj);
    if (is_special_object(stdin_proto)) {
      ant_value_t native_set_raw = js_get(js, stdin_proto, "setRawMode");
      if (is_callable(native_set_raw)) {
        js_set(js, stdin_proto, "__antNativeSetRawMode", native_set_raw);
      }
      js_set_proto_init(stdin_proto, stream_readable_prototype(js));
      setup_readstream_proto(js, stdin_proto);
    }
    stream_init_readable_object(js, stdin_obj, js_mkundef());
  }

  ant_value_t stdout_obj = js_get(js, process_obj, "stdout");
  if (is_special_object(stdout_obj)) {
    ensure_stream_common_props(js, stdout_obj, ANT_STDOUT_FD);
    js_set_getter_desc(js, stdout_obj, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C);
    js_set_getter_desc(js, stdout_obj, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C);

    ant_value_t stdout_proto = js_get_proto(js, stdout_obj);
    if (is_special_object(stdout_proto)) js_set_proto_init(stdout_proto, stream_writable_prototype(js));
    setup_writestream_proto(js, stdout_proto);
    stream_init_writable_object(js, stdout_obj, js_mkundef());
  }

  ant_value_t stderr_obj = js_get(js, process_obj, "stderr");
  if (is_special_object(stderr_obj)) {
    ensure_stream_common_props(js, stderr_obj, ANT_STDERR_FD);
    js_set_getter_desc(js, stderr_obj, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C);
    js_set_getter_desc(js, stderr_obj, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C);
    
    ant_value_t stderr_proto = js_get_proto(js, stderr_obj);
    if (is_special_object(stderr_proto)) js_set_proto_init(stderr_proto, stream_writable_prototype(js));
    setup_writestream_proto(js, stderr_proto);
    stream_init_writable_object(js, stderr_obj, js_mkundef());
  }
}

ant_value_t tty_library(ant_t *js) {
  ant_value_t lib = js_mkobj(js);
  tty_init_stream_constructors(js);

  js_set(js, lib, "isatty", js_mkfun(tty_isatty));
  js_set(js, lib, "ReadStream", g_tty_readstream_ctor);
  js_set(js, lib, "WriteStream", g_tty_writestream_ctor);
  js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "tty", 3));

  return lib;
}
