///|
priv enum SortMode {
  SortByName
  SortByTime
  SortBySize
} derive(Eq)

///|
priv struct LsOptions {
  all : Bool
  almost_all : Bool
  directory : Bool
  classify : Bool
  recursive : Bool
  reverse : Bool
  sort : SortMode
  time : @fsops.TimestampKind
  links : @fsops.LinkTraversal
}

///|
priv struct LsEntry {
  name : String
  path : String
  kind : @fs.FileKind
  timestamp : @fsops.FileTimestamp?
  size : Int64?
}

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

///|
fn c_locale_compare(left : String, right : String) -> Int {
  let left = @utf8.encode(left)
  let right = @utf8.encode(right)
  let shared = left.length().min(right.length())
  for index = 0; index < shared; index = index + 1 {
    if left[index] < right[index] {
      return -1
    }
    if left[index] > right[index] {
      return 1
    }
  }
  if left.length() < right.length() {
    -1
  } else if left.length() > right.length() {
    1
  } else {
    0
  }
}

///|
async fn displayed_kind(
  path : String,
  links : @fsops.LinkTraversal,
  command_line : Bool,
) -> @fs.FileKind {
  let kind = @fs.kind(path, follow_symlink=false)
  if kind == SymLink &&
    (
      links == FollowAllLinks ||
      (links == FollowCommandLineLinks && command_line)
    ) {
    @fs.kind(path, follow_symlink=true) catch {
      _ => SymLink
    }
  } else {
    kind
  }
}

///|
async fn indicator(path : String, kind : @fs.FileKind) -> String {
  match kind {
    Directory => "/"
    SymLink => "@"
    Pipe => "|"
    Socket => "="
    Regular => if @fs.can_execute(path) { "*" } else { "" }
    _ => ""
  }
}

///|
async fn read_entry(
  name : String,
  path : String,
  options : LsOptions,
  command_line : Bool,
) -> LsEntry {
  let kind = displayed_kind(path, options.links, command_line)
  let follow = kind != SymLink
  let timestamp = if options.sort == SortByTime {
    Some(@fsops.read_file_timestamp(path, options.time, follow_symlink=follow))
  } else {
    None
  }
  let size = if options.sort == SortBySize {
    if kind != Regular {
      raise @fsops.FsOpError(
        "-S is limited to sets of regular files: '\{path}'",
      )
    }
    let file = @fs.open(path, mode=ReadOnly)
    defer file.close()
    Some(file.size())
  } else {
    None
  }
  { name, path, kind, timestamp, size, }
}

///|
fn compare_entries(left : LsEntry, right : LsEntry, options : LsOptions) -> Int {
  let primary = match options.sort {
    SortByName => c_locale_compare(left.name, right.name)
    SortByTime =>
      -@fsops.compare_timestamps(
        left.timestamp.unwrap(),
        right.timestamp.unwrap(),
      )
    SortBySize =>
      if left.size.unwrap() > right.size.unwrap() {
        -1
      } else if left.size.unwrap() < right.size.unwrap() {
        1
      } else {
        0
      }
  }
  let compared = if primary == 0 {
    c_locale_compare(left.name, right.name)
  } else {
    primary
  }
  if options.reverse {
    -compared
  } else {
    compared
  }
}

///|
async fn sorted_entries(path : String, options : LsOptions) -> Array[LsEntry] {
  let names = @fs.readdir(
    path,
    include_hidden=options.all || options.almost_all,
    include_special=options.all,
    sort=false,
  )
  let entries : Array[LsEntry] = []
  for name in names {
    entries.push(read_entry(name, join_path(path, name), options, false))
  }
  entries.sort_by((left, right) => compare_entries(left, right, options))
  entries
}

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

///|
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 = sorted_entries(path, options)
  let child_dirs : Array[String] = []
  for entry in entries {
    let suffix = if options.classify {
      indicator(entry.path, entry.kind)
    } else {
      ""
    }
    @stdio.stdout.write(entry.name + suffix + "\n")
    if options.recursive &&
      entry.kind == Directory &&
      entry.name != "." &&
      entry.name != ".." {
      child_dirs.push(entry.path)
    }
  }
  child_dirs
}

///|
fn parse_time(value : String) -> @fsops.TimestampKind raise @cli.CliError {
  match value {
    "atime" | "access" | "use" => AccessTime
    "ctime" | "status" => StatusChangeTime
    "mtime" | "modification" => ModificationTime
    _ =>
      raise @cli.CliError(
        kind=InvalidValue,
        option="time",
        message="invalid time selector",
      )
  }
}

///|
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("sort-time", short='t'),
    @cli.flag("access-time", short='u'),
    @cli.flag("change-time", short='c'),
    @cli.flag("reverse", short='r'),
    @cli.flag("sort-size", short='S'),
    @cli.flag("dereference-command-line", short='H'),
    @cli.flag("dereference", short='L'),
    @cli.flag("no-dereference", short='P'),
    @cli.option("time"),
    @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 [-aAdF1RrtucS] [-H|-L|-P] [--time=WORD] [FILE...]\n",
    )
    return
  }
  let selected_time = match
    parsed.last_occurrence(["access-time", "change-time", "time"]) {
    Some("access-time") => @fsops.TimestampKind::AccessTime
    Some("change-time") => @fsops.TimestampKind::StatusChangeTime
    Some("time") =>
      parse_time(parsed.last_value("time").unwrap()) catch {
        @cli.CliError(option~, message~, ..) => {
          @stdio.stderr.write("ls: \{message}: '\{option}'\n")
          @sys.exit(2)
          return
        }
      }
    _ => @fsops.TimestampKind::ModificationTime
  }
  let links = match
    parsed.last_occurrence([
      "dereference-command-line", "dereference", "no-dereference",
    ]) {
    Some("dereference") => @fsops.LinkTraversal::FollowAllLinks
    Some("no-dereference") => @fsops.LinkTraversal::NeverFollowLinks
    _ => @fsops.LinkTraversal::NeverFollowLinks
  }
  let sort = match parsed.last_occurrence(["sort-time", "sort-size"]) {
    Some("sort-time") => SortByTime
    Some("sort-size") => SortBySize
    _ => SortByName
  }
  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"),
    reverse: parsed.contains("reverse"),
    sort,
    time: selected_time,
    links,
  }
  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 = displayed_kind(path, options.links, true) 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, Array[String])] = [(directory, [])]
    let mut first = true
    while pending.pop() is Some((path, ancestors)) {
      let real = @fs.realpath(path) catch {
        err => {
          @stdio.stderr.write("ls: cannot resolve '\{path}': \{err}\n")
          failed = true
          continue
        }
      }
      if ancestors.contains(real) {
        @stdio.stderr.write("ls: recursive symbolic-link cycle at '\{path}'\n")
        failed = true
        continue
      }
      let next_ancestors = ancestors.copy()
      next_ancestors.push(real)
      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], next_ancestors.copy()))
      }
      first = false
      if !options.recursive {
        pending.clear()
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}