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

///|
let open_end : Int = 0x7FFFFFFF

///|
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 cut_fields(
  line : Bytes,
  ranges : Array[(Int, Int)],
  delimiter : Byte,
  only_delimited : Bool,
) -> Bytes? {
  let fields : Array[Bytes] = []
  let mut start = 0
  for index = 0; index < line.length(); index = index + 1 {
    if line[index] == delimiter {
      fields.push(line[start:index].to_owned())
      start = index + 1
    }
  }
  if start == 0 {
    if only_delimited {
      return None
    }
    return Some(line)
  }
  fields.push(line[start:].to_owned())
  let kept : Array[Byte] = []
  let mut first = true
  for index, field in fields {
    if selected(ranges, index + 1) {
      if !first {
        kept.push(delimiter)
      }
      for byte in field {
        kept.push(byte)
      }
      first = false
    }
  }
  Some(Bytes::from_array(kept))
}

///|
fn cut_characters(line : Bytes, ranges : Array[(Int, Int)]) -> Bytes {
  let out : Array[Byte] = []
  for index, byte in line {
    if selected(ranges, index + 1) {
      out.push(byte)
    }
  }
  Bytes::from_array(out)
}

///|
async fn process_reader(
  reader : &@io.Reader,
  field_ranges : Array[(Int, Int)]?,
  char_ranges : Array[(Int, Int)]?,
  delimiter : Byte,
  only_delimited : Bool,
) -> Unit {
  let scanner = @stream.LineScanner::new(reader)
  while scanner.next() is Some(line) {
    match field_ranges {
      Some(ranges) =>
        match cut_fields(line.data, ranges, delimiter, only_delimited) {
          Some(out) => {
            @stdio.stdout.write(out)
            @stdio.stdout.write("\n")
          }
          None => ()
        }
      None =>
        match char_ranges {
          Some(ranges) => {
            @stdio.stdout.write(cut_characters(line.data, ranges))
            @stdio.stdout.write("\n")
          }
          None => ()
        }
    }
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let parsed = @cli.parse(args, [
    @cli.flag("only-delimited", short='s'),
    @cli.option("delimiter", short='d'),
    @cli.option("fields", short='f'),
    @cli.option("characters", short='c'),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("cut: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: cut (-c LIST | -f LIST [-d CHAR] [-s]) [FILE...]\n",
    )
    return
  }
  let only_delimited = parsed.contains("only-delimited")
  let delimiter = match parsed.last_value("delimiter") {
    Some(d) => {
      let encoded = @utf8.encode(d)
      if encoded.length() != 1 {
        @stdio.stderr.write(
          "cut: the delimiter must be a single byte in C locale\n",
        )
        @sys.exit(2)
        return
      }
      encoded[0]
    }
    None => b'\t'
  }
  let fields_spec = parsed.last_value("fields")
  let chars_spec = parsed.last_value("characters")
  if fields_spec is Some(_) && chars_spec is Some(_) {
    @stdio.stderr.write("cut: fields and characters are mutually exclusive\n")
    @sys.exit(2)
    return
  } else 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 = parsed.operands
  let sources = if files.is_empty() { ["-"] } else { files }
  let mut failed = false
  for path in sources {
    try {
      if path == "-" {
        process_reader(
          @stdio.stdin, field_ranges, char_ranges, delimiter, only_delimited,
        )
      } else {
        let file = @fs.open(path, mode=ReadOnly)
        defer file.close()
        process_reader(
          file, field_ranges, char_ranges, delimiter, only_delimited,
        )
      }
    } catch {
      err => {
        @stdio.stderr.write("cut: \{err}\n")
        failed = true
        continue
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}