///|
/// 终端 fd 层:raw mode、尺寸、读写字节与信号。
///
/// 这里只有「fd 上发生了什么」,没有协议:不拼 ESC,不认键,不算 cell 尺寸。
/// 分成两层是为了让 `moon test` 能对着字节流测解码,也为了让宿主在退出时
/// 只依赖一处还原逻辑。
#borrow(bytes)
extern "C" fn c_write(fd : Int, bytes : Bytes, len : Int) -> Int64 = "mootty_write"

///|
extern "C" fn c_isatty(fd : Int) -> Int64 = "mootty_isatty"

///|
extern "C" fn c_enter_raw(fd : Int) -> Int64 = "mootty_enter_raw"

///|
extern "C" fn c_leave_raw(fd : Int) -> Int64 = "mootty_leave_raw"

///|
extern "C" fn c_winsize(fd : Int) -> Int64 = "mootty_winsize"

///|
extern "C" fn c_poll(fd : Int, timeout_ms : Int) -> Int64 = "mootty_poll"

///|
extern "C" fn c_read(fd : Int) -> Bytes = "mootty_read"

///|
extern "C" fn c_install_signals() -> Int64 = "mootty_install_signals"

///|
extern "C" fn c_take_winch() -> Int64 = "mootty_take_winch"

///|
extern "C" fn c_terminate_pending() -> Int64 = "mootty_terminate_pending"

///|
fn fd_is_a_terminal(fd : Int) -> Bool {
  c_isatty(fd) == 1L
}

///|
/// `wait` 的结果。`Timeout` 不是错误:它就是「这一轮没有更多字节」的事实,
/// 解码器据此把裸 ESC 结算成 Escape 键。
pub enum PollState {
  Ready
  Timeout
  Unavailable
} derive(Eq, Debug)

///|
pub extend PollState with Eq::{not_equal, equal}

///|
pub extend PollState with Debug::{to_repr}

///|
/// 控制终端:输入固定在 fd 0,输出固定在 fd 1(终端会话的约定)。
///
/// 两个 fd 分开存,是因为「stdin 是管道」(测试里常见)和「stdout 被重定向到
/// 文件」是两种不同的失败,合在一起报会让用户不知道该改哪一边。
pub struct Tty {
  in_fd : Int
  out_fd : Int
}

///|
/// 当前进程的控制终端。这里不检查它是不是 tty:调用方先用 `is_terminal`
/// 判断,才能在非 tty 环境(管道、CI)里给出人话错误而不是 raw mode 失败码。
pub fn Tty::current() -> Tty {
  { in_fd: 0, out_fd: 1, }
}

///|
pub fn Tty::is_terminal(self : Tty) -> Bool {
  fd_is_a_terminal(self.in_fd) && fd_is_a_terminal(self.out_fd)
}

///|
/// 保存终端设置并进入 raw mode。进入后必须成对调用 `restore`,
/// 否则 shell 会被留在 raw mode。
///
/// 失败用返回值而不是 `raise`:`Unit raise` 的错误类型是擦除的,宿主拿回的
/// 是一个 `Error` 而不是这里写的那句话,而终端宿主必须把 raw mode 失败、信号
/// 安装失败、写失败分别映射成 `TerminalExit` 的不同格子。返回值也让
/// `TuiSink::write`(要求纯函数)那条出口路径不用 try/catch。
pub fn Tty::enter_raw(self : Tty) -> String? {
  let rc = c_enter_raw(self.in_fd)
  if rc == 0L {
    None
  } else {
    Some(
      "无法进入 raw mode(终端不可用或被其它程序占用),code=" +
      rc.to_string(),
    )
  }
}

///|
/// 还原进入 raw mode 之前的终端设置。没进过 raw mode 时是空操作,
/// 因此可以放在清理路径上无条件调用。
pub fn Tty::restore(self : Tty) -> Unit {
  let _ = c_leave_raw(self.in_fd)
}

///|
/// 终端网格尺寸(列, 行)。非 tty、或尺寸报 0 时返回 None。
pub fn Tty::grid(self : Tty) -> (Int, Int)? {
  let packed = c_winsize(self.in_fd)
  if packed < 0L {
    None
  } else {
    let cols = (packed & 0xffffffffL).to_int()
    let rows = (packed >> 32).to_int()
    Some((cols, rows))
  }
}

///|
/// 等待输入,最多 `timeout_ms` 毫秒。SIGWINCH 会打断等待并返回 `Timeout`,
/// 让宿主立刻去查新尺寸,而不是等下一个超时。
pub fn Tty::wait(self : Tty, timeout_ms~ : Int) -> PollState {
  let rc = c_poll(self.in_fd, timeout_ms)
  if rc > 0L {
    Ready
  } else if rc == 0L {
    Timeout
  } else {
    Unavailable
  }
}

///|
/// 取走当前可读字节。空 `Bytes` 表示没有数据,或对端已关闭(EOF)。
pub fn Tty::take_input(self : Tty) -> Bytes {
  c_read(self.in_fd)
}

///|
/// 把字节写到终端。短写在 C 侧循环补全;写不进就返回原因,
/// 让宿主有机会先还原终端再退出。
pub fn Tty::emit(self : Tty, bytes : Bytes) -> String? {
  let rc = c_write(self.out_fd, bytes, bytes.length())
  if rc < 0L {
    Some("写入终端失败")
  } else {
    None
  }
}

///|
/// 把一段文本按 UTF-8 写到终端。协议字节和投影输出都走这一条,编码只在此处
/// 发生:`String::to_bytes` 已被标 deprecated,而 CJK 内容必须按 UTF-8 出去。
pub fn Tty::write_text(self : Tty, text : String) -> String? {
  self.emit(@utf8.encode(text[:], bom=false))
}

///|
/// 装 SIGWINCH(resize 标志)与 SIGINT/SIGTERM/SIGHUP(退出标志)。
/// 两类信号都只置位,处置权留在主循环:协议字节只能在正常退出路径上关闭。
pub fn Tty::install_signal_handlers(_self : Tty) -> String? {
  let rc = c_install_signals()
  if rc == 0L {
    None
  } else {
    Some("无法安装终端信号处理,code=" + rc.to_string())
  }
}

///|
/// 读取并清零 resize 信号:每次只报一次,宿主据此把 resize 当成一次完整重排。
pub fn Tty::take_resize_signal(_self : Tty) -> Bool {
  c_take_winch() != 0L
}

///|
/// 是否收到过退出信号。不清零:标志一旦置位,主循环每一轮都要继续看到它,
/// 直到走完还原。raw mode 有意保留 `ISIG`,所以 Ctrl-C 到不了按键通道,
/// 只能从这条路结算。
pub fn Tty::terminate_requested(_self : Tty) -> Bool {
  c_terminate_pending() != 0L
}