///|
/// Analysis of the beginning of a line, including indentation and container
/// markers.
priv struct LineStart {
  mut bytes : BytesView
  mut ix : Int

  // The index in `bytes` after the last tab we scanned; initially zero.
  mut tab_start : Int

  // In contexts where spaces help to define block structure, tabs
  // behave as if they were replaced by spaces with a tab stop of 4
  // characters. If we have scanned past a tab character but not consumed all
  // the horizontal width it contributed, this is the number of spaces
  // logically remaining, before the character at `ix`.
  mut spaces_remaining : Int

  // no thematic breaks can occur before this offset.
  mut min_hrule_offset : Int
}

///|
fn LineStart::new(bytes : BytesView) -> LineStart {
  { bytes, ix: 0, tab_start: 0, spaces_remaining: 0, min_hrule_offset: 0 }
}

///|
fn LineStart::clone(self : LineStart) -> LineStart {
  {
    bytes: self.bytes,
    ix: self.ix,
    tab_start: self.tab_start,
    spaces_remaining: self.spaces_remaining,
    min_hrule_offset: self.min_hrule_offset,
  }
}

///|
fn LineStart::restore(self : LineStart, save : LineStart) -> Unit {
  self.bytes = save.bytes
  self.ix = save.ix
  self.tab_start = save.tab_start
  self.spaces_remaining = save.spaces_remaining
  self.min_hrule_offset = save.min_hrule_offset
}

///|
/// Try to scan a number of spaces.
/// Returns true if all spaces were consumed.
fn LineStart::scan_space(self : LineStart, n_space : Int) -> Bool {
  self.scan_space_inner(n_space) == 0
}

///|
/// Scan a number of spaces up to a maximum.
/// Returns number of spaces scanned.
fn LineStart::scan_space_upto(self : LineStart, n_space : Int) -> Int {
  n_space - self.scan_space_inner(n_space)
}

///|
/// Returns unused remainder of spaces.
fn LineStart::scan_space_inner(self : LineStart, n_space : Int) -> Int {
  let mut n_space = n_space
  // Consume any common prefix between the number of spaces we
  // want and the number of unscanned tab-introduced spaces.
  let n_from_remaining = self.spaces_remaining.min(n_space)
  self.spaces_remaining -= n_from_remaining
  n_space -= n_from_remaining

  while n_space > 0 && self.ix < self.bytes.length() {
    let b = self.bytes.unsafe_get(self.ix)
    if b == b' ' {
      self.ix += 1
      n_space -= 1
    } else if b == b'\t' {
      let spaces = 4 - (self.ix - self.tab_start) % 4
      self.ix += 1
      self.tab_start = self.ix
      let n = spaces.min(n_space)
      n_space -= n

      // Record the unscanned portion of the tab.
      self.spaces_remaining = spaces - n
    } else {
      break
    }
  }
  n_space
}

///|
/// Scan all available ASCII whitespace (not including eol).
fn LineStart::scan_all_space(self : LineStart) -> Unit {
  self.spaces_remaining = 0
  while self.ix < self.bytes.length() {
    let b = self.bytes.unsafe_get(self.ix)
    if b == b' ' || b == b'\t' {
      self.ix += 1
    } else {
      break
    }
  }
}

///|
/// Determine whether we're at end of line (includes end of file).
fn LineStart::is_at_eol(self : LineStart) -> Bool {
  if self.ix < self.bytes.length() {
    let c = self.bytes.unsafe_get(self.ix)
    c == b'\r' || c == b'\n'
  } else {
    true
  }
}

///|
fn LineStart::scan_ch(self : LineStart, c : Byte) -> Bool {
  if self.ix < self.bytes.length() && self.bytes.unsafe_get(self.ix) == c {
    self.ix += 1
    true
  } else {
    false
  }
}

///|
fn LineStart::scan_case_insensitive(self : LineStart, tag : BytesView) -> Bool {
  guard self.bytes.length() - self.ix >= tag.length() else { return false }
  let prefix = self.bytes.view(start=self.ix, end=self.ix + tag.length())
  let ok = eq_ignore_ascii_case(prefix, tag)
  if ok {
    self.ix += tag.length()
  }
  ok
}

///|
fn eq_ignore_ascii_case(a : BytesView, b : BytesView) -> Bool {
  guard a.length() == b.length() else { return false }
  let mut i = 0
  while i < a.length() {
    let x = a.unsafe_get(i).to_int() | 0x20
    let y = b.unsafe_get(i).to_int() | 0x20
    guard x == y else { return false }
    i += 1
  }
  true
}

///|
fn LineStart::scan_blockquote_tag(self : LineStart) -> BlockQuoteKind? {
  let saved_ix = self.ix
  let tag = if self.scan_ch(b'[') && self.scan_ch(b'!') {
    let t = if self.scan_case_insensitive(b"note".view()) {
      Some(Note)
    } else if self.scan_case_insensitive(b"tip".view()) {
      Some(Tip)
    } else if self.scan_case_insensitive(b"important".view()) {
      Some(Important)
    } else if self.scan_case_insensitive(b"warning".view()) {
      Some(Warning)
    } else if self.scan_case_insensitive(b"caution".view()) {
      Some(Caution)
    } else {
      None
    }
    if t is Some(_) && self.scan_ch(b']') {
      match scan_blank_line(self.bytes.view(start=self.ix)) {
        Some(nl) => {
          self.ix += nl
          t
        }
        None => None
      }
    } else {
      None
    }
  } else {
    None
  }
  if tag is None {
    self.ix = saved_ix
  }
  tag
}

///|
fn LineStart::scan_blockquote_marker(self : LineStart) -> Bool {
  if self.scan_ch(b'>') {
    ignore(self.scan_space(1))
    true
  } else {
    false
  }
}

///|
fn LineStart::scan_closing_container_extensions_fence(
  self : LineStart,
  length : Int,
) -> Bool {
  let fence_length = scan_while_max(
    self.bytes.view(start=self.ix),
    fn(c) { c == b':' },
    255,
  )
  if fence_length >= length {
    self.ix = self.ix + fence_length
    true
  } else {
    false
  }
}

///|
/// Scan a definition marker.
fn LineStart::scan_definition_list_definition_marker_with_indent(
  self : LineStart,
  indent : Int,
) -> Int? {
  let save = self.clone()
  if self.scan_ch(b':') {
    let save = self.clone()
    if self.scan_space(5) {
      self.restore(save)
      Some(indent + 1 + self.scan_space_upto(1))
    } else {
      self.restore(save)
      Some(indent + 1 + self.scan_space_upto(5))
    }
  } else {
    self.restore(save)
    None
  }
}

///|
/// Scan a list marker.
fn LineStart::scan_list_marker_with_indent(
  self : LineStart,
  indent : Int,
) -> (Byte, Int64, Int)? {
  let save = self.clone()
  if self.ix < self.bytes.length() {
    let c = self.bytes.unsafe_get(self.ix)
    if c == b'-' || c == b'+' || c == b'*' {
      if self.ix >= self.min_hrule_offset {
        // there could be an hrule here
        match scan_hrule(self.bytes.view(start=self.ix)) {
          Ok(_) => {
            self.restore(save)
            return None
          }
          Err(min_offset) => self.min_hrule_offset = min_offset
        }
      }
      self.ix += 1
      guard !(self.scan_space(1) || self.is_at_eol()) else {
        return self.finish_list_marker(c, 0, indent + 2)
      }
    } else if c.to_char().is_ascii_digit() {
      let start_ix = self.ix
      let mut ix = self.ix + 1
      let mut val = Int64::from_int(c.to_int() - 0x30)
      while ix < self.bytes.length() && ix - start_ix < 10 {
        let c = self.bytes.unsafe_get(ix)
        ix += 1
        if c.to_char().is_ascii_digit() {
          val = val * 10 + Int64::from_int(c.to_int() - 0x30)
        } else if c == b')' || c == b'.' {
          self.ix = ix
          guard !(self.scan_space(1) || self.is_at_eol()) else {
            return self.finish_list_marker(c, val, indent + 1 + ix - start_ix)
          }
          break
        } else {
          break
        }
      }
    }
  }
  self.restore(save)
  None
}

///|
fn LineStart::finish_list_marker(
  self : LineStart,
  c : Byte,
  start : Int64,
  indent : Int,
) -> (Byte, Int64, Int)? {
  let mut indent = indent
  let save = self.clone()

  // skip the rest of the line if it's blank
  guard scan_blank_line(self.bytes.view(start=self.ix)) is None else {
    return Some((c, start, indent))
  }

  let post_indent = self.scan_space_upto(4)
  if post_indent < 4 {
    indent += post_indent
  } else {
    self.restore(save)
  }
  Some((c, start, indent))
}

///|
/// Returns Some(is_checked) when a task list marker was found.
fn LineStart::scan_task_list_marker(self : LineStart) -> Bool? {
  let save = self.clone()
  ignore(self.scan_space_upto(3))

  if !self.scan_ch(b'[') {
    self.restore(save)
    return None
  }
  let is_checked = if self.ix < self.bytes.length() {
    let c = self.bytes.unsafe_get(self.ix)
    if c.is_ascii_whitespace_no_nl() {
      self.ix += 1
      false
    } else if c == b'x' || c == b'X' {
      self.ix += 1
      true
    } else {
      self.restore(save)
      return None
    }
  } else {
    self.restore(save)
    return None
  }
  if !self.scan_ch(b']') {
    self.restore(save)
    return None
  }
  let after = if self.ix < self.bytes.length() {
    self.bytes.unsafe_get(self.ix).to_char().is_ascii_whitespace()
  } else {
    true
  }
  if !after {
    self.restore(save)
    return None
  }
  Some(is_checked)
}

///|
fn LineStart::bytes_scanned(self : LineStart) -> Int {
  self.ix
}

///|
fn LineStart::remaining_space(self : LineStart) -> Int {
  self.spaces_remaining
}

///|
/// Returns number of containers scanned.
fn LineStart::scan_containers(
  self : LineStart,
  tree : Tree[Item],
  options : Options,
) -> Int {
  let mut i = 0
  let spine = tree.walk_spine()
  for idx in 0.. {
        let save = self.clone()
        ignore(self.scan_space(3))
        if !self.scan_blockquote_marker() {
          self.restore(save)
          break
        }
      }
      ListItem(indent) => {
        let save = self.clone()
        if !self.scan_space(indent) && !self.is_at_eol() {
          self.restore(save)
          break
        }
      }
      DefinitionListDefinition(indent) => {
        let save = self.clone()
        if !self.scan_space(indent) && !self.is_at_eol() {
          self.restore(save)
          break
        }
      }
      FootnoteDefinition(_) if options.options_has_gfm_footnotes() => {
        let save = self.clone()
        if !self.scan_space(4) && !self.is_at_eol() {
          self.restore(save)
          break
        }
      }
      _ => ()
    }
    i += 1
  }
  i
}