///|
/// 终端宿主:raw mode 会话、三条输入通道的派发时机、帧泵与还原。
///
/// 生命周期只有这一处状态机:`probe → attach → raw → modes → pump → shutdown`。
/// `tty.mbt` 只管 fd 上的事实,`decode.mbt` 只管字节到事实,
/// `input_channel.mbt` 只管事实到宿主事件;这里管「什么时候派发」。
/// 每条输入都立刻走完 dispatch→render_frame,所以「按下即响应」不需要应用
/// 自己找时机重画。
///
/// 残影与清屏:投影器每帧只写脏行(逐行绝对定位 + `ESC[2K`),resize 让整屏
/// 标脏并一次 `ESC[2J` 重建。宿主因此不能在任何两帧之间打印自己的文字——
/// 那会被下一次逐行定位覆盖成残影。所有宿主侧文本都攒在 `TerminalExit` 里,
/// 由调用方在离开 alt screen 之后打印。
priv struct TerminalState {
  mut driver : @runtime.HostRuntimeDriver?
  mut session : @render.RendererSession?
  mut input : TerminalInput?
  mut decoder : TerminalDecoder?
  mut tty : Tty?
  mut modes_active : Bool
  mut needs_render : Bool
  mut close_requested : Bool
  mut surface_generation : Int
  mut next_sequence : Int
  mut write_failure : String?
}

///|
/// 一次会话一份状态。进程仍只有一个控制终端、`run_*` 仍不支持并发重入
/// (两个会话抢同一颗 stdin),但那是 TTY 的物理事实,不需要把状态做成
/// 全局变量:每次 `run` 构造一份,生命周期与函数调用一致,嵌套宿主
/// (例如宿主测试里驱动两次会话)才成为可能。
fn TerminalState::new() -> TerminalState {
  {
    driver: None,
    session: None,
    input: None,
    decoder: None,
    tty: None,
    modes_active: false,
    needs_render: false,
    close_requested: false,
    surface_generation: 0,
    next_sequence: 1,
    write_failure: None,
  }
}

///|
/// 协议字节。C 桥不知道这些存在:全部在 MoonBit 侧拼写,才能用字节流单测。
///
/// - `?1049` alt screen:进入即清屏,离开即把主屏与原光标位置交还 shell。
/// - `?1000` 按钮事件、`?1003` 任意移动(拖动/悬停)、`?1006` SGR 坐标编码。
/// - `>3u` kitty 键盘协议:flag 1 消歧(方向键不再与 ESC 前缀混淆),
///   flag 2 上报事件类型(press/repeat/release 可区分)。不支持的终端忽略它并
///   退回 legacy 编码,解码器两条都认。
/// - `?25l` 隐藏光标:投影器逐行绝对定位,残留光标会闪。
const ENTER_ALT_SCREEN : String = "\u{1b}[?1049h"

///|
const LEAVE_ALT_SCREEN : String = "\u{1b}[?1049l"

///|
const ENABLE_MOUSE : String = "\u{1b}[?1000h\u{1b}[?1003h\u{1b}[?1006h"

///|
const DISABLE_MOUSE : String = "\u{1b}[?1006l\u{1b}[?1003l\u{1b}[?1000l"

///|
const PUSH_KEYBOARD : String = "\u{1b}[>3u"

///|
const POP_KEYBOARD : String = "\u{1b}[ String {
  match self {
    Closed => ""
    NotTerminal =>
      "标准输入与输出都必须是终端;管道与重定向下无法进入 raw mode。"
    NoGridSize => "终端没有报告行列数,无法建立 cell 栅格。"
    SetupFailed(reason) => "终端初始化失败:" + reason
    RendererRejected(reason) =>
      "TUI 渲染器拒绝接管这个 surface:" + reason
    WriteFailed(reason) => "写终端失败:" + reason
  }
}

///|
/// 跑一个 Program:进入 alt screen 与 raw mode,直到应用请求关闭或终端不可用。
/// 返回前终端一定已经还原。
///
/// 渲染器在此显式声明为 `tui-ansi`,不走多渲染器择优:终端没有 GPU surface
/// 可回退,择优失败只会把原因藏起来。
pub fn[Model, Msg] run_program(
  program~ : @core.Program[Model, Msg],
  cell? : @tui_renderer.CellBox = @tui_renderer.CellBox::default(),
  idle_timeout_ms? : Int = DEFAULT_IDLE_TIMEOUT_MS,
  theme? : @core.Theme? = None,
) -> TerminalExit {
  match probe_tty() {
    Err(exit) => exit
    Ok((tty, cols, rows)) => {
      let input = TerminalInput::new(cols~, rows~, cell~)
      let runtime = @runtime.new_program(
        program~,
        size=input.metrics().logical_size,
      )
      run(tty, runtime, input, idle_timeout_ms, palette_for(theme))
    }
  }
}

///|
/// 跑一个根 View,给不需要 Program 的薄入口用。
pub fn[Msg] run_view(
  root~ : @core.View[Msg],
  cell? : @tui_renderer.CellBox = @tui_renderer.CellBox::default(),
  idle_timeout_ms? : Int = DEFAULT_IDLE_TIMEOUT_MS,
  on_message? : ((Msg) -> Unit)? = None,
  theme? : @core.Theme? = None,
) -> TerminalExit {
  match probe_tty() {
    Err(exit) => exit
    Ok((tty, cols, rows)) => {
      let input = TerminalInput::new(cols~, rows~, cell~)
      let runtime = @runtime.new_view(
        root~,
        size=input.metrics().logical_size,
        on_message~,
      )
      run(tty, runtime, input, idle_timeout_ms, palette_for(theme))
    }
  }
}

///|
/// 主题只在建会话时用一次:cell 前景色是渲染策略,由 `TuiPalette`
/// 量化到 xterm-256;不给主题就是 S2–S7 的默认调色板。
/// 运行中热切换记在 tui-hardening 的 debt 里。
fn palette_for(theme : @core.Theme?) -> @tui_renderer.TuiPalette {
  match theme {
    Some(t) => @tui_renderer.TuiPalette::from_theme(t)
    None => @tui_renderer.TuiPalette::default()
  }
}

///|
fn probe_tty() -> Result[(Tty, Int, Int), TerminalExit] {
  let tty = Tty::current()
  if !tty.is_terminal() {
    Err(NotTerminal)
  } else {
    match tty.grid() {
      None => Err(NoGridSize)
      Some((cols, rows)) => Ok((tty, cols, rows))
    }
  }
}

///|
/// 装配 → 开协议 → 泵 → 还原。`attach` 在 raw mode 之前:绑定失败时终端
/// 从未被改过,还原路径因此是空操作而不是猜。
fn run(
  tty : Tty,
  runtime : @runtime.AppRuntime,
  input : TerminalInput,
  idle_timeout_ms : Int,
  palette : @tui_renderer.TuiPalette,
) -> TerminalExit {
  let state = TerminalState::new()
  state.tty = Some(tty)
  state.input = Some(input)
  state.decoder = Some(TerminalDecoder::new())
  // 终端投影只读语义帧,而语义负载默认关闭:不开就是拿一个空帧。
  runtime.set_semantics_frames_enabled(true)
  state.driver = Some(
    @runtime.HostRuntimeDriver::new(runtime~, schedule_redraw=() => {
      state.needs_render = true
    }),
  )
  let exit = match state.attach(input, tty, palette) {
    Some(reason) => RendererRejected(reason)
    None =>
      match state.open_session(tty) {
        Some(reason) => SetupFailed(reason)
        None => state.pump(idle_timeout_ms~)
      }
  }
  state.shutdown(tty)
  exit
}

///|
/// 打开会话:raw mode → 信号处理 → 协议字节。任一步失败就带着那一步的原因返回,
/// 后面的步骤不再做(尤其不要在没进 raw mode 的终端上写 alt screen)。
fn TerminalState::open_session(self : TerminalState, tty : Tty) -> String? {
  match tty.enter_raw() {
    Some(reason) => return Some(reason)
    None => ()
  }
  match tty.install_signal_handlers() {
    Some(reason) => return Some(reason)
    None => ()
  }
  // `modes_active` 只在写成功之后置位:退出时不对没开过的模式发关闭序列,
  // 也不对着没切过去的 alt screen 发离开序列。
  self.enter_modes(tty)
}

///|
/// 绑渲染会话。返回 `String?`:None 是成功,Some 是可直接展示的拒绝原因。
fn TerminalState::attach(
  self : TerminalState,
  input : TerminalInput,
  tty : Tty,
  palette : @tui_renderer.TuiPalette,
) -> String? {
  let sink = @tui_renderer.TuiSink::new(description="terminal stdout", write=text => {
    // 出口必须是纯函数,所以失败不抛出而是登记:主循环在帧末取出它,
    // 先还原终端再退出。丢在这里不报的原因,用户看到的是画面静止。
    match tty.write_text(text) {
      Some(reason) => self.write_failure = Some(reason)
      None => ()
    }
  })
  let surface = @render.HostSurface::new(
    surface_id="terminal",
    metrics=input.metrics(),
  )
  match
    @tui_renderer.ansi(sink~, cell=input.cell, palette~).bind_surface(surface) {
    Bound(session) => {
      match self.session {
        Some(previous) => previous.dispose()
        None => ()
      }
      self.session = Some(session)
      match self.driver {
        Some(driver) => driver.set_text_system(session.text_system())
        None => ()
      }
      self.needs_render = true
      self.surface_generation = self.surface_generation + 1
      self.next_sequence = 1
      None
    }
    Rejected(reason) => Some(reason)
  }
}

///|
/// 开启协议字节。`modes_active` 只在写成功之后置位:退出时不对没开过的模式
/// 发关闭序列,也不对着没切过去的 alt screen 发离开序列。
fn TerminalState::enter_modes(self : TerminalState, tty : Tty) -> String? {
  let failure = tty.write_text(ENTER_MODES)
  if failure is None {
    self.modes_active = true
  }
  failure
}

///|
/// 输入泵:等字节 → 解码 → 派发 → (resize?) → 重画,直到应用请求关闭、收到
/// 退出信号或终端不可用。三条都走同一个 `outcome`/`shutdown`,所以 Ctrl-C 与
/// 应用自关闭在「把终端交回去」这件事上没有区别。
fn TerminalState::pump(
  self : TerminalState,
  idle_timeout_ms~ : Int,
) -> TerminalExit {
  let tty = match self.tty {
    Some(tty) => tty
    None => return SetupFailed("终端宿主状态未初始化")
  }
  let decoder = match self.decoder {
    Some(decoder) => decoder
    None => return SetupFailed("输入解码器未初始化")
  }
  // 首帧在进入等待之前画:否则界面要等一个 idle 周期才出现。
  self.render_frame()
  let mut outcome : TerminalExit = Closed
  let mut running = true
  while running {
    match tty.wait(timeout_ms=idle_timeout_ms) {
      Ready => {
        let bytes = tty.take_input()
        if bytes.is_empty() {
          // poll 说可读而 read 给 0 字节:输入端已关闭(管道结束 / hangup)。
          running = false
        } else {
          for event in decoder.feed_bytes(bytes) {
            self.deliver(event)
          }
        }
      }
      Timeout =>
        // 数据流停了:把只能靠超时消歧的裸 ESC 结算成 Escape 键。
        match decoder.flush() {
          Some(event) => self.deliver(event)
          None => ()
        }
      Unavailable => running = false
    }
    // SIGWINCH 会在 poll 里把等待打断成 Timeout,也会在处理输入期间到达:
    // 两条都在这一次重排里生效,重画永远用新网格而不是旧布局。
    if tty.take_resize_signal() {
      self.reflow(tty)
    }
    self.render_frame()
    match self.write_failure {
      Some(reason) => {
        outcome = WriteFailed(reason)
        running = false
      }
      None => ()
    }
    // 退出信号走的是标志而不是按键:raw mode 有意保留 ISIG,所以 Ctrl-C 到不了
    // 解码器。放在写帧之后,保证最后一帧不会被跳过;放在 close_requested 之前,
    // 让还原路径始终是同一条 shutdown。
    if tty.terminate_requested() {
      running = false
    }
    if self.close_requested {
      running = false
    }
  }
  outcome
}

///|
/// 一条终端事实立刻走完派发:不等下一批输入,所以「按下即响应」不依赖
/// Enter 或文本提交。`to_host` 给 None 表示这条事实不该进入应用(按钮号与
/// 相位矛盾、能力表关掉了某条通道)。
fn TerminalState::deliver(self : TerminalState, event : TerminalEvent) -> Unit {
  let host_event = match self.input {
    Some(input) => input.to_host(event)
    None => None
  }
  match (self.driver, host_event) {
    (Some(driver), Some(event)) => {
      let result = driver.dispatch(event)
      if result.needs_redraw {
        self.needs_render = true
      }
      if result.close_requested {
        self.close_requested = true
      }
    }
    _ => ()
  }
}

///|
/// 尺寸通道:一次 resize 做两件事——把新度量交给渲染会话重建栅格,把
/// `Resized` 交给运行时走完整 update→layout→present。只重画旧布局会留下
/// 错格的分栏边界,那只差一次重排而不是差一次渲染。
fn TerminalState::reflow(self : TerminalState, tty : Tty) -> Unit {
  match (self.input, self.session, self.driver, tty.grid()) {
    (Some(input), Some(session), Some(driver), Some((cols, rows))) => {
      let event = input.resize(cols, rows)
      session.resize(input.metrics())
      let result = driver.dispatch(event)
      if result.close_requested {
        self.close_requested = true
      }
      // 栅格换了行列,可视区之外的旧字符不会被任何增量写覆盖:整屏重画一次,
      // 清屏由 damage 的 full 分支(`ESC[2J` + 归位)做。
      self.needs_render = true
    }
    _ => ()
  }
}

///|
/// 帧泵:一次 redraw 一个 token,会话按 token 判断该画还是该跳过。
/// 零增量帧不写一个字节,这是增量通路生效的证据而不是失败。
fn TerminalState::render_frame(self : TerminalState) -> Unit {
  if !self.needs_render {
    return
  }
  self.needs_render = false
  match (self.driver, self.session) {
    (Some(driver), Some(session)) => {
      driver.begin_redraw()
      let frame = driver.draw_frame()
      let token = @render.FrameToken::new(
        surface_generation=self.surface_generation,
        sequence=self.next_sequence,
      )
      self.next_sequence = self.next_sequence + 1
      let _ = session.render_frame(
        @render.RenderFrameSubmission::new(token~, frame~),
      )
      driver.finish_redraw()
    }
    _ => ()
  }
}

///|
/// 还原:先关鼠标与键盘协议,再显示光标、退 alt screen,最后还原 termios。
/// 顺序反了会让 alt screen 恢复期间终端的回复(光标位置报告等)当成按键
/// 派发给已经不存在的应用。写失败在这里忽略:此刻唯一要紧的是把终端交回去,
/// stdout 已经关掉时也必须继续还原 termios,否则 shell 会被留在 raw mode。
fn TerminalState::shutdown(self : TerminalState, tty : Tty) -> Unit {
  if self.modes_active {
    self.modes_active = false
    let _ = tty.write_text(EXIT_MODES)
  }
  tty.restore()
  match self.session {
    Some(session) => session.dispose()
    None => ()
  }
  self.session = None
  self.driver = None
  self.input = None
  self.decoder = None
  self.tty = None
  self.needs_render = false
  self.close_requested = false
  self.write_failure = None
}