///|
priv struct LsOptions {
  all : Bool
  almost_all : Bool
  directory : Bool
  classify : Bool
  recursive : Bool
}

///|
fn join_path(parent : String, name : String) -> String {
  if parent == "/" {
    "/" + name
  } else {
    parent + "/" + name
  }
}

///|
fn indicator(kind : @fs.FileKind) -> String {
  match kind {
    Directory => "/"
    SymLink => "@"
    Pipe => "|"
    Socket => "="
    _ => ""
  }
}

///|
async fn print_operand(path : String, options : LsOptions) -> Unit {
  let suffix = if options.classify {
    indicator(@fs.kind(path, follow_symlink=false))
  } else {
    ""
  }
  @stdio.stdout.write(path + suffix + "\n")
}

///|
async fn directory_entries(path : String, options : LsOptions) -> Array[String] {
  let entries = @fs.readdir(
    path,
    include_hidden=options.all || options.almost_all,
    include_special=options.all,
    sort=false,
  )
  entries.sort_by((left, right) => left.lexical_compare(right))
  entries
}

///|
async fn print_directory(
  path : String,
  options : LsOptions,
  show_header : Bool,
  printed_section : Ref[Bool],
) -> Array[String] {
  if show_header {
    if printed_section.val {
      @stdio.stdout.write("\n")
    }
    @stdio.stdout.write(path + ":\n")
  }
  printed_section.val = true
  let entries = directory_entries(path, options)
  let child_dirs : Array[String] = []
  for name in entries {
    let child = join_path(path, name)
    let kind = @fs.kind(child, follow_symlink=false)
    let suffix = if options.classify { indicator(kind) } else { "" }
    @stdio.stdout.write(name + suffix + "\n")
    if options.recursive && kind == Directory && name != "." && name != ".." {
      child_dirs.push(child)
    }
  }
  child_dirs
}

///|

///|
async fn main {
  let args = @env.args()[1:]
  let parsed = @cli.parse(args, [
    @cli.flag("all", short='a'),
    @cli.flag("almost-all", short='A'),
    @cli.flag("directory", short='d'),
    @cli.flag("classify", short='F'),
    @cli.flag("recursive", short='R'),
    @cli.flag("one", short='1'),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("ls: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write("Usage: ls [-aAdF1R] [FILE...]\n")
    return
  }
  let options : LsOptions = {
    all: parsed.contains("all"),
    almost_all: parsed.contains("almost-all"),
    directory: parsed.contains("directory"),
    classify: parsed.contains("classify"),
    recursive: parsed.contains("recursive"),
  }
  let operands = parsed.operands
  let paths = if operands.is_empty() { ["."] } else { operands }
  let mut failed = false
  let directories : Array[String] = []
  for path in paths {
    let kind = @fs.kind(path, follow_symlink=false) catch {
      err => {
        @stdio.stderr.write("ls: cannot access '\{path}': \{err}\n")
        failed = true
        continue
      }
    }
    if kind == Directory && !options.directory {
      directories.push(path)
    } else {
      print_operand(path, options) catch {
        err => {
          @stdio.stderr.write("ls: cannot access '\{path}': \{err}\n")
          failed = true
        }
      }
    }
  }
  let printed_section = Ref(false)
  let show_top_header = paths.length() > 1 || options.recursive
  for directory in directories {
    let pending : Array[String] = [directory]
    let mut first = true
    while pending.pop() is Some(path) {
      let children = print_directory(
        path,
        options,
        show_top_header || !first,
        printed_section,
      ) catch {
        err => {
          @stdio.stderr.write("ls: cannot open '\{path}': \{err}\n")
          failed = true
          first = false
          continue
        }
      }
      let mut index = children.length()
      while index > 0 {
        index -= 1
        pending.push(children[index])
      }
      first = false
      if !options.recursive {
        pending.clear()
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}