///|
/// Interactive terminal helpers for `mb tool install -i`.
///
/// The prompt reads single keypresses from a raw terminal and renders a
/// checkbox list with ANSI escapes. ESC cancels.

///|
/// Whether a prompt can be shown (stdin and stdout are terminals).
fn can_prompt() -> Bool {
  mb_isatty_stdin() != 0 && is_tty()
}

///|
/// Write raw text to stdout without a trailing newline.
fn write_raw(text : String) -> Unit {
  ignore(mb_write(cstring(text)))
}

///|
fn in_array(items : Array[String], s : String) -> Bool {
  for item in items {
    if item == s {
      return true
    }
  }
  false
}

///|
fn count_selected(selected : Array[Bool]) -> Int32 {
  let mut n = 0
  for s in selected {
    if s {
      n = n + 1
    }
  }
  n
}

///|
fn render_select(
  names : Array[String],
  selected : Array[Bool],
  cursor : Int32,
  redraw : Bool,
) -> Unit {
  let mut out = ""
  if redraw {
    out = out + "\u{1B}[" + (names.length() + 1).to_string() + "A"
  }
  out = out + "\r\u{1B}[J"
  out = out +
    "Select executables to install  (\u{2191}/\u{2193} move, space toggle, a all, enter confirm, esc cancel)\n"
  for i, name in names {
    let arrow = if i == cursor { "\u{276F}" } else { " " }
    let mark = if selected[i] { "x" } else { " " }
    out = out + "  " + arrow + " [" + mark + "] " + name + "\n"
  }
  write_raw(out)
}

///|
fn clear_select(lines : Int32) -> Unit {
  write_raw("\u{1B}[" + (lines + 1).to_string() + "A\r\u{1B}[J")
}

///|
/// Ask the user which executables to install. Returns the selected names, or
/// `None` when the user cancels with ESC (or EOF).
fn select_executables(names : Array[String]) -> Array[String]? {
  let n = names.length()
  if n == 0 {
    return Some([])
  }
  let selected : Array[Bool] = Array::make(n, true)
  let mut cursor = 0
  if mb_tty_raw() != 0 {
    return None
  }
  write_raw("\u{1B}[?25l")
  render_select(names, selected, cursor, false)
  let mut result : Array[String]? = None
  while true {
    let key = mb_read_byte(0)
    if key == -1 {
      result = None
      break
    } else if key == 27 {
      // ESC: either a lone cancel or the start of an arrow-key sequence.
      let next = mb_read_byte(50)
      if next == 91 {
        let code = mb_read_byte(50)
        if code == 65 {
          cursor = (cursor + n - 1) % n
        } else if code == 66 {
          cursor = (cursor + 1) % n
        }
      } else {
        result = None
        break
      }
    } else if key == 32 {
      selected[cursor] = !selected[cursor]
    } else if key == 13 || key == 10 {
      if count_selected(selected) > 0 {
        let picked : Array[String] = []
        for i, name in names {
          if selected[i] {
            picked.push(name)
          }
        }
        result = Some(picked)
        break
      }
    } else if key == 97 || key == 65 {
      // 'a' / 'A': toggle everything.
      let all = count_selected(selected) != n
      for i = 0; i < n; i = i + 1 {
        selected[i] = all
      }
    } else if key == 106 {
      cursor = (cursor + 1) % n
    } else if key == 107 {
      cursor = (cursor + n - 1) % n
    }
    render_select(names, selected, cursor, true)
  }
  clear_select(n)
  write_raw("\u{1B}[?25h")
  ignore(mb_tty_restore())
  result
}

///|
/// Return the names to install: with `interactive` and more than one candidate,
/// ask the user; ESC cancels the install.
fn choose_names(
  names : Array[String],
  interactive : Bool,
) -> Array[String] raise MbError {
  if !interactive || names.length() <= 1 {
    return names
  }
  if !can_prompt() {
    raise MbError(
      "cannot choose executables on a non-interactive terminal; candidates: " +
      names.join(", "),
    )
  }
  match select_executables(names) {
    None => {
      println("Installation cancelled.")
      raise Coded("installation cancelled", 0)
    }
    Some(chosen) => chosen
  }
}