///|
fn split_fields(line : String, separator : String?) -> Array[String] {
  let fields : Array[String] = []
  let delimiters = separator.unwrap_or(" \t")
  let mut start = 0
  for index, char in line {
    if delimiters.contains(char.to_string()) {
      if separator is Some(_) || index > start {
        fields.push(line[start:index].to_owned())
      }
      start = index + 1
    }
  }
  if separator is Some(_) || start < line.length() {
    fields.push(line[start:].to_owned())
  }
  fields
}

///|
// Input is converted to the GNU/Linux C-locale byte representation before
// layout. Control bytes are preserved but occupy no printable cells.
fn display_width(field : String, table? : Bool = false) -> Int {
  let mut width = 0
  let mut i = 0
  while i < field.length() {
    let c = field[i]
    i += 1
    if c >= ' ' && c <= '~' {
      width += 1
    } else if table && i < field.length() && field[i] == '[' {
      // util-linux 2.40's table width skips control + '[' ... 'm'.
      // Its default grid uses the unfiltered printable-character count.
      let mut end = i + 1
      while end < field.length() && field[end] != 'm' {
        end += 1
      }
      if end < field.length() {
        i = end + 1
      }
    }
  }
  width
}

///|
fn table(
  lines : Array[String],
  separator : String?,
  output_separator : String,
) -> String {
  let rows = lines.map(line => split_fields(line, separator))
  let widths : Array[Int] = []
  for row in rows {
    for index, field in row {
      while widths.length() <= index {
        widths.push(0)
      }
      widths[index] = widths[index].max(display_width(field, table=true))
    }
  }
  let output = StringBuilder()
  for row in rows {
    for index in 0.. String {
  if lines.is_empty() {
    return ""
  }
  let longest = lines
    .iter()
    .fold(init=0, (n, line) => n.max(display_width(line)))
  if longest >= width {
    return lines.join("\n") + "\n"
  }
  let stride = (longest / 8 + 1) * 8
  let count = (width / stride).max(1).min(lines.length())
  let rows = (lines.length() + count - 1) / count
  let output = StringBuilder()
  for row in 0..= lines.length() {
        break
      }
      if col > 0 {
        while (position / 8 + 1) * 8 <= end_column {
          output.write_char('\t')
          position = (position / 8 + 1) * 8
        }
        end_column += stride
      }
      output.write_string(lines[index])
      position += display_width(lines[index])
    }
    output.write_char('\n')
  }
  output.to_string()
}

///|
fn input_lines(data : Bytes) -> Array[String] {
  let result : Array[String] = []
  let mut start = 0
  for end in 0..<=data.length() {
    if end < data.length() && data[end] != b'\n' {
      continue
    }
    let line = StringBuilder()
    for byte in data[start:end] {
      if byte == 0 {
        break
      }
      if byte >= 128 {
        let hex = "0123456789abcdef"
        line.write_string("\\x")
        line.write_char(hex[byte.to_int() / 16].to_char().unwrap())
        line.write_char(hex[byte.to_int() % 16].to_char().unwrap())
      } else {
        line.write_char(byte.to_int().to_char().unwrap())
      }
    }
    let value = line.to_string()
    if !value
      .iter()
      .all(c => c is (' ' | '\t' | '\r' | '\n' | '\u000b' | '\u000c')) {
      result.push(value)
    }
    start = end + 1
  }
  result
}

///|
fn help_message() -> String {
  let help =
    #|Usage: column [OPTION]... [FILE]...
    #|Columnate input in columns (default), or align a table with -t.
    #|  -t, --table                 create a table
    #|  -s, --separator=CHARS       table delimiters (default: space and TAB)
    #|  -o, --output-separator=SEP  table separator (default: two spaces)
    #|  -c, --output-width=WIDTH    output width (default: COLUMNS or 80)
    #|  -x, --fillrows              fill rows before columns
    #|      --help                 display this help and exit
    #|      --version              display version and exit
    #|Layout uses the C locale. Bytes above ASCII are printed as \xHH.
    #|
  help
}

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("column 0.1.2\n")
    return
  }
  let parsed = @cli.parse(@env.args()[1:], [
    @cli.flag("table", short='t'),
    @cli.option("separator", short='s'),
    @cli.option("output-separator", short='o'),
    @cli.option("output-width", short='c'),
    @cli.flag("fillrows", short='x'),
    @cli.flag("help"),
    @cli.flag("version"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("column: \{message}: '\{option}'\n")
      @sys.exit(1)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(help_message())
    return
  }
  if parsed.contains("version") {
    @stdio.stdout.write("column \{command_version}\n")
    return
  }
  let separator = parsed.last_value("separator")
  if separator is Some(value) && value.iter().any(c => c.to_int() >= 128) {
    @stdio.stderr.write("column: unsupported non-ASCII input separator\n")
    @sys.exit(2)
    return
  }
  let default_width = @string.parse_int(
    @env.get_env_var("COLUMNS").unwrap_or("80"),
  ) catch {
    _ => 80
  }
  let width = match parsed.last_value("output-width") {
    None => if default_width > 0 { default_width } else { 80 }
    Some("unlimited") => 0
    Some(value) => @string.parse_int(value) catch { _ => -1 }
  }
  if width < 0 {
    @stdio.stderr.write("column: invalid output width\n")
    @sys.exit(1)
    return
  }
  let lines : Array[String] = []
  let mut failed = false
  if parsed.operands.is_empty() {
    lines.append(input_lines(@stdio.stdin.read_all().binary()))
  } else {
    for path in parsed.operands {
      // util-linux treats '-' as a filename; omitted operands select stdin.
      let data = @fs.read_file(path).binary() catch {
        err => {
          @stdio.stderr.write("column: \{path}: \{err}\n")
          failed = true
          continue
        }
      }
      lines.append(input_lines(data))
    }
  }
  @stdio.stdout.write(
    if parsed.contains("table") {
      table(
        lines,
        separator,
        parsed.last_value("output-separator").unwrap_or("  "),
      )
    } else {
      columns(lines, width, parsed.contains("fillrows"))
    },
  )
  if failed {
    @sys.exit(1)
  }
}