///|
/// `run_of(char, s, last, start)` is the last position of a consecutive run of `char`
/// in the range `[start, last]` or `start - 1` if `start` is not `char`.
pub fn run_of(
  char~ : Char,
  s : String,
  last~ : CharCodePos,
  start~ : CharCodePos,
) -> Last {
  if start > last || s[start] != char.to_int().to_uint16() {
    return start - 1
  }
  run_of(char~, s, last~, start=start + 1)
}

///|
/// `first_non_blank(s, last, start)` is the first position in the range `[start, last]`
/// that is not blank and `last + 1` if there is none.
pub fn first_non_blank(
  s : String,
  last~ : CharCodePos,
  start~ : CharCodePos,
) -> CharCodePos {
  for k in start..<=last {
    guard s[k].to_char() is Some(c) && @char.is_ascii_blank(c) else { return k }
  }
  last + 1
}

///|
/// `first_non_blank_in_span(s, span)` is `first_non_blank(s, last=span.last, start=span.first)`.
pub fn first_non_blank_in_span(s : String, span : LineSpan) -> CharCodePos {
  first_non_blank(s, last=span.last, start=span.first)
}

///|
/// `last_non_blank(s, first, start)` is the last position in the range `[first, start]`
/// that is not blank and `first - 1` if there is none.
pub fn last_non_blank(
  s : String,
  first~ : CharCodePos,
  start~ : CharCodePos,
) -> CharCodePos {
  if start < first {
    return first - 1
  }
  match s[start] {
    ' ' | '\t' => last_non_blank(s, first~, start=start - 1)
    _ => start
  }
}

///|
/// `rev_drop_spaces(s, first, start)` is the last position in the range `[first, start]`
/// that is not U+0020 and `first - 1` if there is none.
pub fn rev_drop_spaces(
  s : String,
  first~ : CharCodePos,
  start~ : CharCodePos,
) -> CharCodePos {
  if start < first {
    return first - 1
  }
  match s[start] {
    ' ' => rev_drop_spaces(s, first~, start=start - 1)
    _ => start
  }
}

///|
fn push_span(
  line~ : LineSpan,
  first : CharCodePos,
  last : CharCodePos,
  spans : Array[Span],
) -> Unit {
  let first1 = first
  let last1 = last
  // Merge if on the same line
  if spans is [.., { start, span: { pos, first, .. }, }] {
    if line.pos.0 == pos.0 {
      spans[spans.length() - 1] = {
        start,
        span: { ..line, first, last: last1, },
      }
      return
    }
  }
  spans.push({
    start: line.first,
    span: { ..line, first: first1, last: last1, },
  })
}

///|
/// Includes final `char` in spans.
fn[A] accept_to(
  char~ : Char,
  next_line~ : NextLineFn[A],
  s : String,
  lines : A,
  line~ : LineSpan,
  spans : Array[Span],
  after~ : CharCodePos,
) -> (LineSpan, Int32)? {
  for line = line, start = after, k = after + 1 {
    if k > line.last {
      guard next_line(lines) is Some(new_line) else { break None }
      push_span(line~, start, line.last, spans)
      let start = first_non_blank_in_span(s, new_line)
      continue new_line, start, start
    }
    if s[k] == char.to_int().to_uint16() {
      push_span(line~, start, k, spans)
      break Some((line, k))
    }
    continue line, start, k + 1
  }
}

///|
/// Does not not include final `char` in spans and continues on backslashed `char`.
fn[A] accept_upto(
  char~ : Char,
  next_line~ : NextLineFn[A],
  s : String,
  lines : A,
  line~ : LineSpan,
  spans : Array[Span],
  after~ : CharCodePos,
) -> (LineSpan, Int32)? {
  let start = after + 1
  for line = line, prev_bslash = false, start = start, k = start {
    if k > line.last {
      guard next_line(lines) is Some(newline) else { break None }
      if newline.first > newline.last { // Empty
        break None
      }
      push_span(line~, start, line.last, spans)
      let start = first_non_blank_in_span(s, newline)
      let prev_bslash = false
      continue newline, prev_bslash, start, start
    } else {
      if s[k] == char.to_int().to_uint16() && !prev_bslash {
        push_span(line~, start, k - 1, spans)
        break Some((line, k))
      }
      let prev_bslash = s[k] == '\\' && !prev_bslash
      continue line, prev_bslash, start, k + 1
    }
  }
}

///|
/// `first_non_blank_over_nl(next_line, s, lines, line, start)` is the first byte position starting with `start`
/// that is not blank in `line` or on the next line as determined by `next_line`.
/// Returns `None` if there is no such position.
pub fn[A] first_non_blank_over_nl(
  next_line~ : NextLineFn[A],
  s : String,
  lines : A,
  line~ : LineSpan,
  start~ : CharCodePos,
) -> NextLineResult? {
  let nb = first_non_blank(s, last=line.last, start~)
  if nb <= line.last {
    return Some(ThisLine(nb))
  }
  guard next_line(lines) is Some(new_line) else { return None }
  let nb = first_non_blank_in_span(s, new_line)
  if nb > new_line.last {
    return None
  }
  Some(NextLine(new_line, nb))
}

///|
/// Same as `first_non_blank_over_nl()` but pushes skipped data on `spans`.
fn[A] first_non_blank_over_nl1(
  next_line~ : NextLineFn[A],
  s : String,
  lines : A,
  line~ : LineSpan,
  spans : Array[Span],
  start~ : CharCodePos,
) -> (LineSpan, CharCodePos)? {
  match first_non_blank_over_nl(next_line~, s, lines, line~, start~) {
    None => None
    Some(ThisLine(nb)) => {
      let line = { ..line, first: start, } // No layout
      push_span(line~, start, nb - 1, spans)
      Some((line, nb - 1))
    }
    Some(NextLine(new_line, nb)) => {
      let line = { ..line, first: start, } // No layout
      push_span(line~, start, line.last, spans)
      Some((new_line, nb - 1))
    }
  }
}

///|
/// `first_non_escaped_char(c, s, last, start)` is the first `CharCodePos`
/// in the range `[start, last]` that has `c` unescaped and `last + 1` if there is none.
pub fn first_non_escaped_char(
  c : Char,
  s : String,
  last~ : CharCodePos,
  start~ : CharCodePos,
) -> CharCodePos {
  for k = start; ; k = k + 1 {
    if k > last ||
      (s[k] == c.to_int().to_uint16() && (k == start || s[k - 1] != '\\')) {
      break k
    }
  }
}