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

///|
let open_end : Int = 0x7FFFFFFF

///|
fn cut_command() -> @argparse.Command {
  Command(
    "cut",
    about="Print selected fields or characters of each line.",
    flags=[
      FlagArg(
        "only-delimited",
        short='s',
        about="Skip lines without the delimiter (field mode).",
      ),
    ],
    options=[
      OptionArg(
        "delimiter",
        short='d',
        about="Field delimiter character (default TAB).",
      ),
      OptionArg(
        "fields",
        short='f',
        about="Comma-separated field list, e.g. 1,3-5,7-.",
        allow_hyphen_values=true,
      ),
      OptionArg(
        "characters",
        short='c',
        about="Comma-separated character position list.",
        conflicts_with=["fields"],
        allow_hyphen_values=true,
      ),
    ],
    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 parse_positive(text : String, spec : String) -> Int raise CliError {
  let n = @string.parse_int(text) catch {
    _ => raise CliError("cut: invalid list: '\{spec}'")
  }
  if n < 1 {
    raise CliError("cut: list positions are numbered from 1: '\{spec}'")
  }
  n
}

///|
fn parse_list(spec : String) -> Array[(Int, Int)] raise CliError {
  let ranges : Array[(Int, Int)] = []
  for part in spec.split(",") {
    let item = part.to_owned()
    if item is "" {
      raise CliError("cut: invalid list: '\{spec}'")
    }
    let pieces : Array[String] = item
      .split("-")
      .map(v => v.to_owned())
      .collect()
    match pieces {
      [n] => {
        let v = parse_positive(n, spec)
        ranges.push((v, v))
      }
      ["", ""] => raise CliError("cut: invalid list: '\{spec}'")
      [a, b] => {
        let lo = if a is "" { 1 } else { parse_positive(a, spec) }
        let hi = if b is "" { open_end } else { parse_positive(b, spec) }
        if lo > hi {
          raise CliError("cut: invalid decreasing range: '\{spec}'")
        }
        ranges.push((lo, hi))
      }
      _ => raise CliError("cut: invalid list: '\{spec}'")
    }
  }
  if ranges.is_empty() {
    raise CliError("cut: invalid list: '\{spec}'")
  }
  ranges
}

///|
fn selected(ranges : Array[(Int, Int)], index : Int) -> Bool {
  for r in ranges {
    let (lo, hi) = r
    if index >= lo && index <= hi {
      return true
    }
  }
  false
}

///|
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 cut_fields(
  line : String,
  ranges : Array[(Int, Int)],
  delimiter : String,
  only_delimited : Bool,
) -> String? {
  let parts : Array[String] = line
    .split(delimiter)
    .map(v => v.to_owned())
    .collect()
  if parts.length() == 1 {
    // No delimiter in this line.
    if only_delimited {
      return None
    }
    return Some(line)
  }
  let kept : Array[String] = []
  for index, part in parts {
    if selected(ranges, index + 1) {
      kept.push(part)
    }
  }
  Some(kept.join(delimiter))
}

///|
fn cut_characters(line : String, ranges : Array[(Int, Int)]) -> String {
  let sb = StringBuilder()
  let mut index = 1
  for c in line {
    if selected(ranges, index) {
      sb.write_char(c)
    }
    index += 1
  }
  sb.to_string()
}

///|
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 = cut_command()
  let matches = command.parse(argv=args, env=Map([])) catch {
    err => {
      @stdio.stderr.write("\{err}\n")
      @sys.exit(2)
      return
    }
  }
  let only_delimited = matches.flags.get_or_default("only-delimited", false)
  let delimiter = match option_value(matches, "delimiter") {
    Some(d) => {
      if d.char_length() != 1 {
        @stdio.stderr.write("cut: the delimiter must be a single character\n")
        @sys.exit(2)
        return
      }
      d
    }
    None => "\t"
  }
  let fields_spec = option_value(matches, "fields")
  let chars_spec = option_value(matches, "characters")
  if fields_spec is None && chars_spec is None {
    @stdio.stderr.write(
      "cut: you must specify a list of fields (-f) or characters (-c)\n",
    )
    @sys.exit(2)
    return
  }
  let (field_ranges, char_ranges) = try {
    let field_ranges = match fields_spec {
      Some(spec) => Some(parse_list(spec))
      None => None
    }
    let char_ranges = match chars_spec {
      Some(spec) => Some(parse_list(spec))
      None => None
    }
    (field_ranges, char_ranges)
  } catch {
    CliError(msg) => {
      @stdio.stderr.write("\{msg}\n")
      @sys.exit(2)
      return
    }
  }
  let files = matches.values.get("files").unwrap_or([])
  let sources = if files.is_empty() { ["-"] } else { files }
  let mut failed = false
  for path in sources {
    let text = read_source_text(path) catch {
      err => {
        @stdio.stderr.write("cut: \{err}\n")
        failed = true
        continue
      }
    }
    for line in split_lines(text) {
      match field_ranges {
        Some(ranges) =>
          match cut_fields(line, ranges, delimiter, only_delimited) {
            Some(out) => @stdio.stdout.write(out + "\n")
            None => ()
          }
        None =>
          match char_ranges {
            Some(ranges) =>
              @stdio.stdout.write(cut_characters(line, ranges) + "\n")
            None => ()
          }
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}