///|
/// Number of lines included when `--range` gives only a start line.
let default_range_span = 42

///|
/// Errors raised when parsing a `--range` value.
suberror RangeError {
  Malformed
  NotALineNumber(String)
  EndBeforeStart
}

///|
/// Renders each `RangeError` variant as a user-facing message.
impl Show for RangeError with fn output(self, logger) {
  match self {
    Malformed => logger.write_string("expected the form N or N,M")
    NotALineNumber(component) =>
      logger.write_string("\{component} is not a valid line number")
    EndBeforeStart => logger.write_string("expected 1 <= start <= end")
  }
}

///|
/// Parses a 1-indexed line number, rejecting anything that is not a
/// positive integer.
fn parse_line_number(text : String) -> Int raise RangeError {
  let number = @string.parse_int(text) catch {
    _ => raise RangeError::NotALineNumber(text)
  }
  guard number > 0 else { raise RangeError::NotALineNumber(text) }
  number
}

///|
/// Parses `--range` values of the form `N` or `N,M`, where lines are
/// 1-indexed and the end is inclusive. A single `N` spans `N` up to
/// `N + default_range_span`.
fn parse_range(value : String) -> (Int, Int) raise RangeError {
  let parts = value.split(",").map(part => part.trim().to_owned()).to_array()
  match parts {
    [start_str, end_str] => {
      let start = parse_line_number(start_str)
      let end = parse_line_number(end_str)
      guard end >= start else { raise RangeError::EndBeforeStart }
      (start, end)
    }
    [start_str] => {
      let start = parse_line_number(start_str)
      (start, start + default_range_span)
    }
    _ => raise RangeError::Malformed
  }
}

///|
/// Extracts the 1-indexed, inclusive line range [start, end] from `content`.
/// Lines outside the range are not materialized. A trailing newline yields
/// an empty element after the last line; drop it so the range matches what a
/// text editor shows.
fn extract_range(content : String, start : Int, end : Int) -> Array[String] {
  let lines = content
    .split("\n")
    .drop(start - 1)
    .take(end - start + 1)
    .map(line => line.to_owned())
    .to_array()
  let is_last_empty = lines.length() > 0 && lines[lines.length() - 1] == ""
  if content.has_suffix("\n") && is_last_empty {
    ignore(lines.pop())
  }
  lines
}