///|
fn help_message() -> String {
  let message =
    #|Usage: join [options] FILE1 FILE2
    #|
    #|Join lines of two files sorted on their join fields.
    #|Only pairable lines are printed (no -a support yet).
    #|
    #|Options:
    #|  -1 FIELD    Join on this field of FILE1 (default 1).
    #|  -2 FIELD    Join on this field of FILE2 (default 1).
    #|  -t CHAR     Use CHAR as the field separator instead of runs of
    #|              blanks; the output separator becomes CHAR as well.
    #|  -h, --help  Show this help message.
    #|
    #|Use '-' as a file name to read stdin.
  message
}

///|
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 key_of(fields : Array[String], field : Int) -> String {
  if field - 1 < fields.length() {
    fields[field - 1]
  } else {
    ""
  }
}

///|
fn rest_of(fields : Array[String], field : Int) -> Array[String] {
  let rest : Array[String] = []
  for index, value in fields {
    if index != field - 1 {
      rest.push(value)
    }
  }
  rest
}

///|
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 mut field1 = 1
  let mut field2 = 1
  let mut separator : String? = None
  let files : Array[String] = []
  let mut index = 0
  while index < args.length() {
    let arg = args[index]
    if arg == "-h" || arg == "--help" {
      @stdio.stdout.write(help_message() + "\n")
      return
    }
    if arg.has_prefix("-1") || arg.has_prefix("-2") || arg.has_prefix("-t") {
      let is_separator = arg.has_prefix("-t")
      // Accept both `-t,` (attached value) and `-t ,` (separate argument).
      let (value, consumed) = if arg.length() > 2 {
        (arg[2:].to_owned(), 1)
      } else if index + 1 < args.length() {
        (args[index + 1], 2)
      } else {
        @stdio.stderr.write("join: missing value after '\{arg}'\n")
        @sys.exit(2)
        return
      }
      if is_separator {
        if value.char_length() != 1 {
          @stdio.stderr.write(
            "join: the separator must be a single character\n",
          )
          @sys.exit(2)
          return
        }
        separator = Some(value)
      } else {
        let n = @string.parse_int(value) catch {
          _ => {
            @stdio.stderr.write("join: invalid field number: '\{value}'\n")
            @sys.exit(2)
            return
          }
        }
        if n < 1 {
          @stdio.stderr.write("join: invalid field number: '\{value}'\n")
          @sys.exit(2)
          return
        }
        if arg.has_prefix("-1") {
          field1 = n
        } else {
          field2 = n
        }
      }
      index += consumed
      continue
    }
    if arg.has_prefix("-") && arg != "-" {
      @stdio.stderr.write("join: unknown option: '\{arg}'\n")
      @sys.exit(2)
      return
    }
    files.push(arg)
    index += 1
  }
  if files.length() != 2 {
    @stdio.stderr.write(
      "join: expected exactly two files\n\n" + help_message() + "\n",
    )
    @sys.exit(2)
    return
  }
  let (lines1, lines2) = (
    split_lines(read_source_text(files[0])),
    split_lines(read_source_text(files[1])),
  ) catch {
    err => {
      @stdio.stderr.write("join: \{err}\n")
      @sys.exit(1)
      return
    }
  }
  let fields1 : Array[Array[String]] = lines1.map(l => {
    split_fields(l, separator)
  })
  let fields2 : Array[Array[String]] = lines2.map(l => {
    split_fields(l, separator)
  })
  let out_sep = match separator {
    Some(sep) => sep
    None => " "
  }
  let mut i = 0
  let mut j = 0
  while i < fields1.length() && j < fields2.length() {
    let key1 = key_of(fields1[i], field1)
    let key2 = key_of(fields2[j], field2)
    let cmp = key1.compare(key2)
    if cmp < 0 {
      i += 1
    } else if cmp > 0 {
      j += 1
    } else {
      // Find the runs of equal keys in both inputs and emit their product.
      let mut end1 = i
      while end1 < fields1.length() && key_of(fields1[end1], field1) == key1 {
        end1 += 1
      }
      let mut end2 = j
      while end2 < fields2.length() && key_of(fields2[end2], field2) == key2 {
        end2 += 1
      }
      for a in i..