///|
#cfg(not(platform="windows"))
extern "C" fn tui_is_tty(fd : Int) -> Int = "moonbit_tui_is_tty"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_enable_raw(fd : Int) -> Int = "moonbit_tui_enable_raw"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_restore(fd : Int) -> Int = "moonbit_tui_restore"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_width(fd : Int) -> Int = "moonbit_tui_width"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_height(fd : Int) -> Int = "moonbit_tui_height"

///|
#cfg(not(platform="windows"))
#borrow(bytes)
extern "C" fn tui_write(
  fd : Int,
  bytes : Bytes,
  offset : Int,
  len : Int,
) -> Int = "moonbit_tui_write"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_install_resize_handler() -> Unit = "moonbit_tui_install_resize_handler"

///|
#cfg(not(platform="windows"))
#borrow(bytes)
extern "C" fn tui_system(bytes : Bytes, offset : Int, len : Int) -> Int = "moonbit_tui_system"

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn is_tty(fd : Int) -> Bool {
  tui_is_tty(fd) == 1
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn is_tty(fd : Int) -> Bool {
  ignore(fd)
  false
}

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn terminal_size() -> Size raise TerminalError {
  let width = tui_width(1)
  let height = tui_height(1)
  guard width > 0 && height > 0 else {
    raise NativeError("failed to read terminal size")
  }
  { width, height }
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn terminal_size() -> Size raise TerminalError {
  raise UnsupportedPlatform
}

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn enter_raw_mode() -> Unit raise TerminalError {
  guard is_tty(0) && is_tty(1) else { raise NotATty("stdin/stdout") }
  guard tui_enable_raw(0) == 0 else {
    raise NativeError("failed to enable raw mode")
  }
  tui_install_resize_handler()
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn enter_raw_mode() -> Unit raise TerminalError {
  raise UnsupportedPlatform
}

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn restore_terminal() -> Unit {
  ignore(tui_restore(0))
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn restore_terminal() -> Unit {
  ()
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run(
  self : Program[Model, Msg],
) -> Unit raise TerminalError {
  ignore(self.run_with_options_returning_model(ProgramOptions::default()))
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_options(
  self : Program[Model, Msg],
  options : ProgramOptions,
) -> Unit raise TerminalError {
  ignore(self.run_with_options_returning_model(options))
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_returning_model(
  self : Program[Model, Msg],
) -> Model raise TerminalError {
  self.run_with_options_returning_model(ProgramOptions::default())
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_options_returning_model(
  self : Program[Model, Msg],
  options : ProgramOptions,
) -> Model raise TerminalError {
  self.run_with_runtime_control(options, None, 0)
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_cancel_token(
  self : Program[Model, Msg],
  options : ProgramOptions,
  token : CancelToken,
) -> Model raise TerminalError {
  self.run_with_runtime_control(options, Some(token), 0)
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_timeout(
  self : Program[Model, Msg],
  options : ProgramOptions,
  milliseconds : Int,
) -> Model raise TerminalError {
  self.run_with_runtime_control(options, None, milliseconds)
}

///|
#coverage.skip
async fn[Model, Msg] Program::run_with_runtime_control(
  self : Program[Model, Msg],
  options : ProgramOptions,
  cancel_token : CancelToken?,
  timeout_millis : Int,
) -> Model raise TerminalError {
  enter_raw_mode()
  defer restore_terminal()
  let mut current_size = terminal_size()
  let queue = @aqueue.Queue::new(kind=Unbounded)
  let emit = Emit::new(msg => {
    ignore(queue.try_put(RuntimeMsg(msg)) catch { _ => false })
  })
  for cmd in self.handle_event(emit, Resize(current_size)) {
    cmd.run_with_terminal(command => {
      ignore(queue.try_put(RuntimeTerminal(command)) catch { _ => false })
    }) catch {
      err => raise NativeError(err.to_string())
    }
  }
  self
  .init(emit)
  .run_with_terminal(command => {
    ignore(queue.try_put(RuntimeTerminal(command)) catch { _ => false })
  }) catch {
    err => raise NativeError(err.to_string())
  }
  let mut previous : StyledFrame? = None
  if options.renderer_enabled {
    let initial_view = self.view()
    let initial_frame = StyledFrame::from_node(initial_view, current_size)
    write_terminal(
      start_program_output(options, initial_view, current_size, initial_frame),
    )
    previous = Some(initial_frame)
  } else {
    write_terminal(start_program_control_output(options))
  }
  self.mark_clean()
  @async.with_task_group(group => {
    self.subscriptions().start(emit, group)
    group.spawn_bg(no_wait=true, allow_failure=true, () => {
      read_terminal_input(self, queue, group)
    })
    group.spawn_bg(no_wait=true, allow_failure=true, () => {
      watch_terminal_resize(
        self,
        queue,
        current_size,
        options.resize_poll_millis,
      )
    })
    if options.renderer_enabled && options.renderer_fps > 0 {
      let interval = Int::max(1, 1000 / options.renderer_fps)
      group.spawn_bg(no_wait=true, allow_failure=true, () => {
        while true {
          @async.sleep(interval)
          queue.put(RuntimeRender)
        }
      })
    }
    match cancel_token {
      Some(token) =>
        group.spawn_bg(no_wait=true, allow_failure=true, () => {
          while !token.is_cancelled() {
            @async.sleep(16)
          }
          group.return_immediately(())
        })
      None => ()
    }
    if timeout_millis > 0 {
      group.spawn_bg(no_wait=true, allow_failure=true, () => {
        @async.sleep(timeout_millis)
        group.return_immediately(())
      })
    }
    while true {
      let item = queue.get()
      let mut render_due = options.renderer_fps <= 0 || item is RuntimeRender
      if handle_runtime_item(self, emit, queue, group, item, options, previous) {
        group.return_immediately(())
      }
      let mut drained = 0
      while drained < options.max_messages_per_frame {
        match (queue.try_get() catch { _ => None }) {
          Some(item) => {
            if item is RuntimeRender {
              render_due = true
            }
            if handle_runtime_item(
                self, emit, queue, group, item, options, previous,
              ) {
              group.return_immediately(())
            }
            drained += 1
          }
          None => break
        }
      }
      if self.is_dirty() && render_due {
        if options.renderer_enabled {
          let next_size = terminal_size()
          let resized = next_size != current_size
          current_size = next_size
          let next = StyledFrame::from_node(self.view(), current_size)
          let output = if resized {
            repaint_resized_frame(next)
          } else {
            frame_update_output(options, previous, next)
          }
          write_terminal(output)
          previous = Some(next)
        } else {
          current_size = terminal_size() catch { _ => current_size }
        }
        self.mark_clean()
      }
    }
  }) catch {
    err if @async.is_cancellation_error(err) => ()
    err => raise NativeError(err.to_string())
  }
  write_terminal(stop_program_output(options))
  self.model()
}

///|
#coverage.skip
priv enum RuntimeItem[Msg] {
  RuntimeMsg(Msg)
  RuntimeTerminal(TerminalCommand)
  RuntimeRender
}

///|
#coverage.skip
fn start_program_output(
  options : ProgramOptions,
  node : Node,
  size : Size,
  frame : StyledFrame,
) -> String {
  if !options.renderer_enabled {
    return start_program_control_output(options)
  }
  if options.alternate_screen {
    ansi_start_program(options, node, size)
  } else {
    let out = StringBuilder::new()
    out.write_string(ansi_disable_autowrap())
    if options.hide_cursor {
      out.write_string(ansi_hide_cursor())
    }
    if options.mouse_mode != MouseOff {
      out.write_string(ansi_enable_mouse(options.mouse_mode))
    }
    if options.bracketed_paste {
      out.write_string(ansi_enable_bracketed_paste())
    }
    if options.focus_events {
      out.write_string(ansi_enable_focus_events())
    }
    out.write_string(paint_styled_frame_inline(frame))
    out.to_string()
  }
}

///|
#coverage.skip
fn start_program_control_output(options : ProgramOptions) -> String {
  let out = StringBuilder::new()
  if options.alternate_screen {
    out.write_string(ansi_enter_alternate_screen())
  }
  out.write_string(ansi_disable_autowrap())
  if options.hide_cursor {
    out.write_string(ansi_hide_cursor())
  }
  if options.mouse_mode != MouseOff {
    out.write_string(ansi_enable_mouse(options.mouse_mode))
  }
  if options.bracketed_paste {
    out.write_string(ansi_enable_bracketed_paste())
  }
  if options.focus_events {
    out.write_string(ansi_enable_focus_events())
  }
  out.to_string()
}

///|
#coverage.skip
fn frame_update_output(
  options : ProgramOptions,
  previous : StyledFrame?,
  next : StyledFrame,
) -> String {
  if options.alternate_screen {
    match previous {
      None => paint_styled_frame_ansi(next)
      Some(prev) => diff_styled_frame(prev, next)
    }
  } else {
    match previous {
      None => repaint_styled_frame_inline(next.lines.length(), next)
      Some(prev) => diff_styled_frame_inline(prev, next)
    }
  }
}

///|
#coverage.skip
fn stop_program_output(options : ProgramOptions) -> String {
  let out = StringBuilder::new()
  out.write_string(ansi_stop_program(options))
  if !options.alternate_screen {
    out.write_string("\r\n")
  }
  out.to_string()
}

///|
#coverage.skip
fn repaint_resized_frame(frame : StyledFrame) -> String {
  let out = StringBuilder::new()
  out.write_string(ansi_clear_screen())
  out.write_string(ansi_move_cursor(1, 1))
  out.write_string(paint_styled_frame_ansi(frame))
  out.to_string()
}

///|
#coverage.skip
fn safe_terminal_command_output(
  command : TerminalCommand,
  options : ProgramOptions,
  previous : StyledFrame?,
) -> String {
  match command {
    Print(value) => safe_print_output(value, newline=false, options, previous)
    PrintLine(value) =>
      safe_print_output(value, newline=true, options, previous)
    PrintErr(value) =>
      safe_print_output(value, newline=false, options, previous)
    PrintErrLine(value) =>
      safe_print_output(value, newline=true, options, previous)
    _ => ansi_terminal_command(command)
  }
}

///|
#coverage.skip
fn terminal_command_fd(command : TerminalCommand) -> Int {
  match command {
    PrintErr(_) | PrintErrLine(_) => 2
    _ => 1
  }
}

///|
#coverage.skip
fn safe_print_output(
  value : String,
  newline~ : Bool,
  options : ProgramOptions,
  previous : StyledFrame?,
) -> String {
  if !options.renderer_enabled {
    return if newline { "\{value}\r\n" } else { value }
  }
  match previous {
    None => if newline { "\{value}\r\n" } else { value }
    Some(frame) => {
      let out = StringBuilder::new()
      if options.alternate_screen {
        out.write_string(ansi_move_cursor(1, 1))
      } else if frame.lines.length() > 0 {
        out.write_string(ansi_move_cursor_up(frame.lines.length() - 1))
      }
      out.write_string("\r")
      write_safe_print_text(out, value, newline)
      if options.alternate_screen {
        out.write_string(paint_styled_frame_ansi(frame))
      } else {
        out.write_string(paint_styled_frame_inline(frame))
      }
      out.to_string()
    }
  }
}

///|
#coverage.skip
fn write_safe_print_text(
  out : StringBuilder,
  value : String,
  newline : Bool,
) -> Unit {
  let lines = split_safe_print(value)
  if lines.is_empty() {
    if newline {
      out.write_string(ansi_clear_line())
      out.write_string("\r\n")
    }
    return
  }
  for index, line in lines {
    out.write_string(line)
    out.write_string(ansi_clear_line())
    if index + 1 < lines.length() || newline {
      out.write_string("\r\n")
    }
  }
}

///|
#coverage.skip
fn split_safe_print(value : String) -> Array[String] {
  let lines : Array[String] = []
  let mut start = 0
  let mut index = 0
  while index < value.length() {
    if value.code_unit_at(index) == Int::to_uint16('\n'.to_int()) {
      lines.push(value[start:index].to_owned())
      start = index + 1
    }
    index += 1
  }
  if start < value.length() {
    lines.push(value[start:].to_owned())
  }
  lines
}

///|
#coverage.skip
async fn[Model, Msg] handle_runtime_item(
  program : Program[Model, Msg],
  emit : Emit[Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  item : RuntimeItem[Msg],
  options : ProgramOptions,
  previous : StyledFrame?,
) -> Bool {
  match item {
    RuntimeMsg(msg) => {
      handle_runtime_msg(program, emit, queue, group, msg, options, previous)
      false
    }
    RuntimeTerminal(command) =>
      handle_terminal_command(command, program, options, previous)
    RuntimeRender => false
  }
}

///|
#coverage.skip
async fn[Model, Msg] handle_runtime_msg(
  program : Program[Model, Msg],
  emit : Emit[Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  msg : Msg,
  options : ProgramOptions,
  previous : StyledFrame?,
) -> Unit {
  let cmd = program.step(emit, msg)
  if cmd_needs_suspension(cmd) {
    run_runtime_cmd(program, cmd, queue, group, options, previous)
  } else {
    group.spawn_bg(no_wait=true, allow_failure=true, () => {
      run_runtime_cmd(program, cmd, queue, group, options, previous)
    })
  }
}

///|
#coverage.skip
fn cmd_needs_suspension(cmd : Cmd) -> Bool {
  match cmd {
    CmdSuspend(_) | CmdExecProcess(_) => true
    CmdBatch(cmds) | CmdSequence(cmds) => {
      for child in cmds {
        if cmd_needs_suspension(child) {
          return true
        }
      }
      false
    }
    _ => false
  }
}

///|
#coverage.skip
async fn[Model, Msg] run_runtime_cmd(
  program : Program[Model, Msg],
  cmd : Cmd,
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  options : ProgramOptions,
  previous : StyledFrame?,
) -> Unit {
  match cmd {
    NoCmd => ()
    CmdMessage(send) => send()
    CmdBatch(cmds) =>
      @async.with_task_group(batch => {
        for child in cmds {
          batch.spawn_bg(() => {
            run_runtime_cmd(program, child, queue, group, options, previous)
          })
        }
      })
    CmdSequence(cmds) =>
      for child in cmds {
        run_runtime_cmd(program, child, queue, group, options, previous)
      }
    CmdDelay(milliseconds, cmd) => {
      @async.sleep(milliseconds)
      run_runtime_cmd(program, cmd, queue, group, options, previous)
    }
    CmdTask(task) => {
      let next = task()
      run_runtime_cmd(program, next, queue, group, options, previous)
    }
    CmdSuspend(task) => {
      let next = run_suspended_task(program, options, previous, task)
      run_runtime_cmd(program, next, queue, group, options, previous)
    }
    CmdExecProcess(command, done) => {
      let status = run_suspended_process(program, options, previous, command)
      run_runtime_cmd(program, done(status), queue, group, options, previous)
    }
    CmdTerminal(command) => queue.put(RuntimeTerminal(command))
  }
}

///|
#coverage.skip
async fn[Model, Msg] run_suspended_task(
  program : Program[Model, Msg],
  options : ProgramOptions,
  previous : StyledFrame?,
  task : async () -> Cmd,
) -> Cmd raise TerminalError {
  suspend_terminal(options, previous)
  let msg = task() catch {
    err => {
      resume_terminal(program, options)
      raise NativeError(err.to_string())
    }
  }
  resume_terminal(program, options)
  msg
}

///|
#coverage.skip
fn[Model, Msg] run_suspended_process(
  program : Program[Model, Msg],
  options : ProgramOptions,
  previous : StyledFrame?,
  command : String,
) -> Int raise TerminalError {
  suspend_terminal(options, previous)
  let status = run_system(command) catch {
    err => {
      resume_terminal(program, options)
      raise err
    }
  }
  resume_terminal(program, options)
  status
}

///|
#coverage.skip
fn suspend_terminal(
  options : ProgramOptions,
  previous : StyledFrame?,
) -> Unit raise TerminalError {
  match previous {
    Some(frame) if options.renderer_enabled =>
      if !options.alternate_screen && frame.lines.length() > 0 {
        write_terminal("\{ansi_move_cursor_up(frame.lines.length() - 1)}\r")
      }
    _ => ()
  }
  write_terminal(stop_program_output(options))
  restore_terminal()
}

///|
#coverage.skip
fn[Model, Msg] resume_terminal(
  program : Program[Model, Msg],
  options : ProgramOptions,
) -> Unit raise TerminalError {
  enter_raw_mode()
  if options.renderer_enabled {
    let size = terminal_size()
    let view = program.view()
    let frame = StyledFrame::from_node(view, size)
    write_terminal(start_program_output(options, view, size, frame))
  } else {
    write_terminal(start_program_control_output(options))
  }
}

///|
#coverage.skip
#cfg(not(platform="windows"))
fn run_system(command : String) -> Int raise TerminalError {
  let bytes = @encoding/utf8.encode(command)
  let status = tui_system(bytes, 0, bytes.length())
  if status < 0 {
    raise NativeError("failed to execute process: \{command}")
  }
  status
}

///|
#coverage.skip
#cfg(platform="windows")
fn run_system(command : String) -> Int raise TerminalError {
  ignore(command)
  raise UnsupportedPlatform
}

///|
#coverage.skip
fn[Model, Msg] handle_terminal_command(
  command : TerminalCommand,
  program : Program[Model, Msg],
  options : ProgramOptions,
  previous : StyledFrame?,
) -> Bool raise TerminalError {
  match command {
    QuitProgram => true
    Repaint => {
      program.mark_dirty()
      false
    }
    _ => {
      write_terminal_fd(
        terminal_command_fd(command),
        safe_terminal_command_output(command, options, previous),
      )
      if command is ClearScreen {
        program.mark_dirty()
      }
      false
    }
  }
}

///|
#coverage.skip
async fn[Model, Msg] watch_terminal_resize(
  program : Program[Model, Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  initial_size : Size,
  poll_millis : Int,
) -> Unit {
  let mut last = initial_size
  while true {
    @async.sleep(poll_millis)
    let next = terminal_size() catch { _ => last }
    if next != last {
      last = next
      for msg in program.subscriptions().map_event(Resize(next)) {
        queue.put(RuntimeMsg(msg))
      }
    }
  }
}

///|
#coverage.skip
async fn[Model, Msg] read_terminal_input(
  program : Program[Model, Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
) -> Unit {
  let mut decoder = InputDecoder::new()
  let mut pending_idle = 0
  while true {
    match @stdio.stdin.read_some(max_len=32) {
      None => {
        if decoder.has_pending() && decoder.pending_is_lone_escape() {
          pending_idle += 1
          if pending_idle >= 2 {
            let (events, next) = decoder.flush()
            decoder = next
            pending_idle = 0
            dispatch_input_events(program, queue, group, events)
          }
        }
        @async.sleep(16)
      }
      Some(bytes) => {
        pending_idle = 0
        let (events, next) = decoder.feed(bytes)
        decoder = next
        dispatch_input_events(program, queue, group, events)
      }
    }
  }
}

///|
#coverage.skip
async fn[Model, Msg] dispatch_input_events(
  program : Program[Model, Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  events : Array[Event],
) -> Unit {
  for event in events {
    match event {
      Key(Ctrl("c")) | Quit => group.return_immediately(())
      _ =>
        for msg in program.subscriptions().map_event(event) {
          queue.put(RuntimeMsg(msg))
        }
    }
  }
}

///|
#coverage.skip
fn[Msg, X] Sub::start(
  self : Sub[Msg],
  emit : Emit[Msg],
  group : @async.TaskGroup[X],
) -> Unit {
  match self {
    NoSub => ()
    SubBatch(subs) =>
      for sub in subs {
        sub.start(emit, group)
      }
    TickSub(milliseconds, msg) =>
      group.spawn_bg(no_wait=true, allow_failure=true, () => {
        while true {
          @async.sleep(milliseconds)
          emit.send(msg)
        }
      })
    EventSub(_)
    | Keyboard(_)
    | KeySub(_)
    | MouseSub(_)
    | MouseEventSub(_)
    | ResizeSub(_)
    | PasteSub(_)
    | FocusSub(_)
    | FocusChangesSub(_) => ()
  }
}

///|
#coverage.skip
#cfg(not(platform="windows"))
fn write_terminal(output : String) -> Unit raise TerminalError {
  write_terminal_fd(1, output)
}

///|
#coverage.skip
#cfg(not(platform="windows"))
fn write_terminal_fd(fd : Int, output : String) -> Unit raise TerminalError {
  let bytes = @encoding/utf8.encode(output)
  let mut offset = 0
  while offset < bytes.length() {
    let written = tui_write(fd, bytes, offset, bytes.length() - offset)
    if written <= 0 {
      raise NativeError("failed to write terminal output")
    } else {
      offset += written
    }
  }
}

///|
#coverage.skip
#cfg(platform="windows")
fn write_terminal(output : String) -> Unit raise TerminalError {
  ignore(output)
  raise UnsupportedPlatform
}

///|
#coverage.skip
#cfg(platform="windows")
fn write_terminal_fd(fd : Int, output : String) -> Unit raise TerminalError {
  ignore(fd)
  ignore(output)
  raise UnsupportedPlatform
}