///|
priv suberror CliError {
  CliError(String)
}

///|
priv struct Entry {
  line : String
  key : String
  numeric : Double
  index : Int
}

///|
fn sort_command() -> @argparse.Command {
  Command(
    "sort",
    about="Sort lines of all inputs together.",
    flags=[
      FlagArg("reverse", short='r', about="Reverse the comparison."),
      FlagArg(
        "numeric-sort",
        short='n',
        about="Compare by the numeric prefix of the key.",
      ),
      FlagArg(
        "unique",
        short='u',
        about="Output only the first line of each group of equal keys.",
      ),
      FlagArg(
        "ignore-case",
        short='f',
        about="Fold lower case to upper case when comparing.",
      ),
    ],
    options=[
      OptionArg(
        "key",
        short='k',
        about="Sort by fields START[,END] (1-based, whole fields only).",
      ),
      OptionArg(
        "field-separator",
        short='t',
        about="Field separator for -k (default: runs of blanks).",
      ),
    ],
    positionals=[
      PositionArg(
        "files",
        about="[file...] ('-' or no file reads stdin)",
        num_args=ValueRange(lower=0),
      ),
    ],
    disable_help_subcommand=true,
  )
}

///|
fn option_value(matches : @argparse.Matches, name : String) -> String? {
  match matches.values.get(name) {
    Some(vals) =>
      if vals.is_empty() {
        None
      } else {
        Some(vals[vals.length() - 1])
      }
    None => None
  }
}

///|
fn split_lines(text : String) -> Array[String] {
  if text is "" {
    return []
  }
  let lines : Array[String] = text.split("\n").map(v => v.to_owned()).collect()
  if text.has_suffix("\n") {
    ignore(lines.pop())
  }
  lines
}

///|
fn split_fields(line : String, separator : String?) -> Array[String] {
  match separator {
    Some(sep) => line.split(sep).map(v => v.to_owned()).collect()
    None => {
      let fields : Array[String] = []
      let sb = StringBuilder()
      let mut in_field = false
      for c in line {
        if c == ' ' || c == '\t' {
          if in_field {
            fields.push(sb.to_string())
            sb.reset()
            in_field = false
          }
        } else {
          sb.write_char(c)
          in_field = true
        }
      }
      if in_field {
        fields.push(sb.to_string())
      }
      fields
    }
  }
}

///|
fn parse_positive(text : StringView, spec : String) -> Int raise CliError {
  let n = @string.parse_int(text) catch {
    _ => raise CliError("sort: invalid key specification: '\{spec}'")
  }
  if n < 1 {
    raise CliError("sort: invalid key specification: '\{spec}'")
  }
  n
}

///|
fn parse_key(spec : String) -> (Int, Int) raise CliError {
  let pieces : Array[String] = spec.split(",").map(v => v.to_owned()).collect()
  match pieces {
    [a] => {
      let start = parse_positive(a, spec)
      (start, 0x7FFFFFFF)
    }
    [a, b] => {
      let start = parse_positive(a, spec)
      let end = parse_positive(b, spec)
      if start > end {
        raise CliError("sort: invalid key specification: '\{spec}'")
      }
      (start, end)
    }
    _ => raise CliError("sort: invalid key specification: '\{spec}'")
  }
}

///|
fn extract_key(
  line : String,
  key : (Int, Int)?,
  separator : String?,
  out_sep : String,
) -> String {
  match key {
    None => line
    Some((start, end)) => {
      let fields = split_fields(line, separator)
      let kept : Array[String] = []
      for index, field in fields {
        let position = index + 1
        if position >= start && position <= end {
          kept.push(field)
        }
      }
      kept.join(out_sep)
    }
  }
}

///|
/// Parse the leading numeric prefix (optional sign, digits, optional decimal
/// part) of a key, ignoring leading blanks. Lines without one sort as 0.
fn numeric_prefix(key : String) -> Double {
  let chars : Array[Char] = key.iter().collect()
  let mut start = 0
  while start < chars.length() && (chars[start] == ' ' || chars[start] == '\t') {
    start += 1
  }
  let sb = StringBuilder()
  let mut i = start
  if i < chars.length() && (chars[i] == '-' || chars[i] == '+') {
    sb.write_char(chars[i])
    i += 1
  }
  let mut saw_digit = false
  while i < chars.length() && chars[i] is ('0'..='9') {
    sb.write_char(chars[i])
    saw_digit = true
    i += 1
  }
  if i < chars.length() && chars[i] == '.' {
    sb.write_char('.')
    i += 1
    while i < chars.length() && chars[i] is ('0'..='9') {
      sb.write_char(chars[i])
      saw_digit = true
      i += 1
    }
  }
  if !saw_digit {
    return 0.0
  }
  @string.parse_double(sb.to_string()) catch {
    _ => 0.0
  }
}

///|
async fn read_source_text(path : String) -> String {
  if path == "-" {
    @stdio.stdin.read_all().text()
  } else {
    @fs.read_file_to_string(path)
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let command = sort_command()
  let matches = command.parse(argv=args, env=Map([])) catch {
    err => {
      @stdio.stderr.write("\{err}\n")
      @sys.exit(2)
      return
    }
  }
  let reverse = matches.flags.get_or_default("reverse", false)
  let numeric = matches.flags.get_or_default("numeric-sort", false)
  let unique = matches.flags.get_or_default("unique", false)
  let fold_case = matches.flags.get_or_default("ignore-case", false)
  let separator = match option_value(matches, "field-separator") {
    Some(sep) => {
      if sep.char_length() != 1 {
        @stdio.stderr.write(
          "sort: the field separator must be a single character\n",
        )
        @sys.exit(2)
        return
      }
      Some(sep)
    }
    None => None
  }
  let key_spec = try {
    match option_value(matches, "key") {
      Some(spec) => Some(parse_key(spec))
      None => None
    }
  } catch {
    CliError(msg) => {
      @stdio.stderr.write("\{msg}\n")
      @sys.exit(2)
      return
    }
  }
  let out_sep = match separator {
    Some(sep) => sep
    None => " "
  }
  let files = matches.values.get("files").unwrap_or([])
  let sources = if files.is_empty() { ["-"] } else { files }
  let lines : Array[String] = []
  for path in sources {
    let text = read_source_text(path) catch {
      err => {
        @stdio.stderr.write("sort: \{err}\n")
        @sys.exit(1)
        return
      }
    }
    lines.append(split_lines(text))
  }
  let entries : Array[Entry] = []
  for index, line in lines {
    let raw_key = extract_key(line, key_spec, separator, out_sep)
    let key = if fold_case { raw_key.to_lower() } else { raw_key }
    entries.push({
      line,
      key,
      numeric: if numeric {
        numeric_prefix(key)
      } else {
        0.0
      },
      index,
    })
  }
  fn key_compare(a : Entry, b : Entry) -> Int {
    if numeric {
      a.numeric.compare(b.numeric)
    } else {
      a.key.compare(b.key)
    }
  }

  entries.sort_by(fn(a, b) {
    let mut cmp = key_compare(a, b)
    if cmp == 0 {
      // Last-resort comparison on the whole line, like GNU sort.
      cmp = a.line.compare(b.line)
    }
    if reverse {
      cmp = -cmp
    }
    if cmp == 0 {
      // Stability tiebreak: sort_by itself is unstable.
      a.index - b.index
    } else {
      cmp
    }
  })
  let mut previous : Entry? = None
  for entry in entries {
    let emit = match previous {
      Some(prev) => if unique { key_compare(prev, entry) != 0 } else { true }
      None => true
    }
    if emit {
      @stdio.stdout.write(entry.line + "\n")
      previous = Some(entry)
    }
  }
}