///|
priv struct TreeColors {
  codes : Map[String, String]
  suffixes : Array[(String, String)]
}

///|
priv struct TreeOutput {
  pending : @buffer.Buffer
  send : async (Bytes) -> Unit
}

///|
fn TreeOutput::new(send : async (Bytes) -> Unit) -> TreeOutput {
  { pending: @buffer.Buffer(), send, }
}

///|
async fn TreeOutput::flush(self : TreeOutput) -> Unit {
  if !self.pending.is_empty() {
    (self.send)(self.pending.to_bytes())
    self.pending.reset()
  }
}

///|
async fn TreeOutput::write(self : TreeOutput, text : String) -> Unit {
  self.pending.write_string_utf8(text)
  if self.pending.length() >= 65536 {
    self.flush()
  }
}

///|
fn tree_colors(force : Bool) -> TreeColors? {
  if !force || @env.get_env_var("TERM") is None {
    return None
  }
  let value = @env.get_env_var("TREE_COLORS").unwrap_or_else(() => {
    @env.get_env_var("LS_COLORS").unwrap_or("")
  })
  let value = if value == "" {
    "no=00:rs=0:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:ex=01;32"
  } else {
    value
  }
  let codes : Map[String, String] = Map([])
  let suffixes : Array[(String, String)] = []
  for field in value.split(":") {
    let parts = field.split("=").filter(part => !part.is_empty()).collect()
    if parts.length() < 2 {
      continue
    }
    let key = parts[0].to_owned()
    let sequence = parts[1].to_owned()
    if key.has_prefix("*") {
      suffixes.push((key[1:].to_owned(), sequence))
    } else {
      codes[key] = sequence
    }
  }
  suffixes.rev_in_place()
  if !codes.contains("lc") {
    codes["lc"] = "\u001b["
  }
  if !codes.contains("rc") {
    codes["rc"] = "m"
  }
  if !codes.contains("rs") {
    codes["rs"] = "0"
  }
  if !codes.contains("ec") {
    codes["ec"] = codes["lc"] + codes["rs"] + codes["rc"]
  }
  Some({ codes, suffixes, })
}

///|
fn require_color_kind(
  kind : @fs.FileKind,
  colors : TreeColors,
) -> Unit raise TreeError {
  let selectors = match kind {
    Regular => ["su", "sg", "ex"]
    Directory => ["tw", "ow", "st"]
    _ => []
  }
  let unknown = selectors.filter(selector => colors.codes.contains(selector))
  if !unknown.is_empty() {
    raise TreeError(
      "color selectors " +
      unknown.join(",") +
      " require unavailable raw mode bits; omit these TREE_COLORS selectors",
    )
  }
}

///|
fn tree_color(
  kind : @fs.FileKind,
  name : String,
  colors : TreeColors,
) -> String? {
  if kind == Regular {
    for (suffix, sequence) in colors.suffixes {
      if name.has_suffix(suffix) {
        return Some(sequence)
      }
    }
  }
  let code = match kind {
    Directory => "di"
    Regular => "fi"
    SymLink => "ln"
    Pipe => "pi"
    Socket => "so"
    BlockDevice => "bd"
    CharDevice => "cd"
    _ => "no"
  }
  colors.codes.get(code)
}

///|
fn c_name(value : String) -> String {
  let output = StringBuilder()
  for byte in @utf8.encode(value) {
    let simple = match byte {
      7 => Some('a')
      8 => Some('b')
      9 => Some('t')
      10 => Some('n')
      11 => Some('v')
      12 => Some('f')
      13 => Some('r')
      b'\\' => Some('\\')
      b' ' => Some(' ')
      _ => None
    }
    match simple {
      Some(c) => {
        output.write_char('\\')
        output.write_char(c)
      }
      None => {
        let code = byte.to_int()
        if byte >= 32 && byte < 127 {
          output.write_char(code.unsafe_to_char())
        } else {
          output.write_char('\\')
          output.write_char((48 + code / 64).unsafe_to_char())
          output.write_char((48 + code / 8 % 8).unsafe_to_char())
          output.write_char((48 + code % 8).unsafe_to_char())
        }
      }
    }
  }
  output.to_string()
}

///|
fn left_pad(value : String, width : Int32) -> String {
  " ".repeat((width - value.length()).max(0)) + value
}

///|
fn size_text(size : Int64, style : SizeStyle) -> String {
  if style == ExactSize {
    return " " + left_pad(size.to_string(), 11)
  }
  let base = if style == SiSize { 1000L } else { 1024L }
  let mut reduced = size
  let mut index = if size < base { 0 } else { 1 }
  while reduced >= base * base {
    reduced /= base
    index += 1
  }
  if index == 0 {
    return " " + left_pad(reduced.to_string(), 4)
  }
  let number = Float::from_int64(reduced) / Float::from_int64(base)
  let precision = if (reduced + 52L) / base >= 10L { 0 } else { 1 }
  let text = human_decimal(number, precision)
  let units = if style == SiSize { "dkMGTPEZY" } else { "BKMGTPEZY" }
  " " + left_pad(text, 3) + units[index:index + 1].to_owned()
}

///|
// psize only formats positive float32 quotients below 1024 to 0 or 1 decimals.
fn human_decimal(value : Float, precision : Int32) -> String {
  let bits = value.reinterpret_as_uint()
  let significant = ((bits & 0x7FFFFFU) | 0x800000U).to_uint64()
  let scaled = significant * (if precision == 1 { 10UL } else { 1UL })
  let divisor = 1UL << (150 - ((bits >> 23) & 0xFFU).reinterpret_as_int())
  let mut rounded = scaled / divisor
  let remainder = scaled % divisor
  if remainder * 2UL > divisor ||
    (remainder * 2UL == divisor && rounded % 2UL != 0UL) {
    rounded += 1UL
  }
  if precision == 0 {
    rounded.to_string()
  } else {
    (rounded / 10UL).to_string() + "." + (rounded % 10UL).to_string()
  }
}

///|
fn node_note(node : TreeNode, options : TreeOptions, root : Bool) -> String? {
  if root && (options.prune || options.disk_usage) && node.limited {
    Some("error opening dir")
  } else {
    node.note
  }
}

///|
fn write_text_label(
  node : TreeNode,
  options : TreeOptions,
  root : Bool,
  out : StringBuilder,
) -> Unit {
  if node.size is Some(size) {
    out.write_string("[" + size_text(size, options.size)[1:].to_owned() + "]  ")
  }
  let sequence = match options.colors {
    Some(colors) =>
      tree_color(node.kind, if root { "" } else { node.name }, colors)
    None => None
  }
  if sequence is Some(sequence) && options.colors is Some(colors) {
    out.write_string(colors.codes["lc"] + sequence + colors.codes["rc"])
  }
  out.write_string(c_name(node_label(node, options, root)))
  if sequence is Some(_) && options.colors is Some(colors) {
    out.write_string(colors.codes["ec"])
  }
  if node_note(node, options, root) is Some(note) {
    out.write_string("  [" + note + "]")
  }
}