///|
priv struct Parser {
  /// Input file.
  file : FilePath
  /// Input string.
  i : String
  /// Scratch buffer.
  buf : StringBuilder
  /// Whether extensions are enabled.
  exts : Bool
  /// Don't compute locations.
  no_locs : Bool
  /// Don't compute layouts.
  no_layouts : Bool
  /// Compute heading IDs.
  heading_auto_ids : Bool
  nested_links : Bool
  defs : LabelDefs
  resolver : LabelResolverFn
  /// Current `CloserIndex`, for inline parsing.
  mut cidx : CloserIndex
  /// Current line, only used during block parsing.
  mut curr_line_pos : LinePos
  /// First char of line - 1 on empty lines.
  mut curr_line_last_char : CharCodePos
  mut curr_char : CharCodePos
  mut curr_char_col : Col
  /// `curr_line_last_char + 1` if none.
  mut next_non_blank : CharCodePos
  mut next_non_blank_col : Col

  // Number of cols consumed from tab if i[curr_char] = '\t'.
  mut tab_consumed_cols : Col
}

///|
fn Parser::new(
  defs~ : LabelDefs = Map([]),
  resolver~ : LabelResolverFn = LabelContext::default_resolver,
  nested_links~ : Bool = false,
  heading_auto_ids~ : Bool = false,
  layout~ : Bool = false,
  locs~ : Bool = false,
  file~ : FilePath = file_path_none,
  strict~ : Bool = true,
  i : String,
) -> Parser {
  {
    file,
    i,
    buf: StringBuilder(size_hint=512),
    no_locs: !locs,
    no_layouts: !layout,
    exts: !strict,
    heading_auto_ids,
    nested_links,
    defs,
    resolver,
    cidx: CloserIndex::new(),
    curr_line_pos: LinePos(1, 0),
    curr_line_last_char: -1,
    curr_char: 0,
    curr_char_col: 0,
    next_non_blank: 0,
    next_non_blank_col: 0,
    tab_consumed_cols: 0,
  }
}

///|
fn Parser::find_label_defining_key(self : Parser, key : String) -> Label? {
  match self.defs.get(key) {
    Some(LinkDef(ld)) => ld.v.defined_label
    Some(FootnoteDef(ld)) => ld.v.defined_label
    None => None
  }
}

///|
fn Parser::set_label_def(self : Parser, l : Label, def : LabelDef) -> Unit {
  self.defs[l.key] = def
}

///|
fn Parser::def_label(self : Parser, l : Label) -> Label? {
  (self.resolver)(Def(self.find_label_defining_key(l.key), l))
}

///|
fn Parser::find_def_for_ref(
  self : Parser,
  image~ : Bool,
  ref_ : Label,
) -> Label? {
  let kind : LinkKind = if image { Image } else { Link }
  let def = self.find_label_defining_key(ref_.key)
  (self.resolver)(Ref(kind, ref_, def))
}

// fn Parser::debug_span(self : Parser, s : LineSpan) -> String {
//   self.i.substring(start=s.first, end=s.last + 1)
// }

// fn Parser::debug_line(self : Parser) -> String {
//   self.i.substring(start=self.curr_line_pos.1, end=self.curr_line_last_char)
// }

///|
fn Parser::curr_line_span(
  self : Parser,
  first~ : CharCodePos,
  last~ : CharCodePos,
) -> LineSpan {
  { first, last, pos: self.curr_line_pos, }
}

// Making metas and text locations. This is centralized here to be able
// to disable their creation which has a non-negligible impact on
// performance.

///|
fn Parser::meta(self : Parser, loc : TextLoc) -> Meta {
  if self.no_locs {
    return Meta::none()
  }
  Meta::new(loc~)
}

///|
fn Parser::text_loc_of_span(self : Parser, span : LineSpan) -> TextLoc {
  if self.no_locs {
    return TextLoc::none()
  }
  {
    file: self.file,
    first_ccode: span.first,
    last_ccode: span.last,
    first_line: span.pos,
    last_line: span.pos,
  }
}

///|
fn Parser::text_loc_of_lines(
  self : Parser,
  first~ : CharCodePos,
  last~ : CharCodePos,
  first_line~ : LineSpan,
  last_line~ : LineSpan,
) -> TextLoc {
  if self.no_locs {
    return TextLoc::none()
  }
  {
    file: self.file,
    first_ccode: first,
    last_ccode: last,
    first_line: first_line.pos,
    last_line: last_line.pos,
  }
}

///|
fn Parser::meta_of_spans(
  self : Parser,
  first~ : LineSpan,
  last~ : LineSpan,
) -> Meta {
  if self.no_locs {
    return Meta::none()
  }
  self.meta(
    self.text_loc_of_lines(
      first_line=first,
      last_line=last,
      first=first.first,
      last=last.last,
    ),
  )
}

///|
fn Parser::meta_of_metas(self : Parser, first~ : Meta, last~ : Meta) -> Meta {
  if self.no_locs {
    return Meta::none()
  }
  self.meta(first.loc.span(last.loc))
}

///|
fn Parser::clean_raw_span(
  self : Parser,
  pad~ : Int32 = 0,
  span : LineSpan,
) -> StringNode {
  {
    v: @char.utf_16_clean_raw(
      pad~,
      self.buf,
      self.i,
      first=span.first,
      last=span.last,
    ),
    meta: self.meta(self.text_loc_of_span(span)),
  }
}

///|
fn Parser::clean_unref_span(self : Parser, span : LineSpan) -> StringNode {
  {
    v: @char.utf_16_clean_unref(
      self.buf,
      self.i,
      first=span.first,
      last=span.last,
    ),
    meta: self.meta(self.text_loc_of_span(span)),
  }
}

///|
fn Parser::clean_unesc_unref_span(self : Parser, span : LineSpan) -> StringNode {
  {
    v: @char.utf_16_clean_unesc_unref(
      self.buf,
      self.i,
      first=span.first,
      last=span.last,
    ),
    meta: self.meta(self.text_loc_of_span(span)),
  }
}

///|
fn Parser::layout_clean_raw_span(
  self : Parser,
  pad~ : Int32 = 0,
  span : LineSpan,
) -> StringNode {
  if self.no_layouts {
    return layout_empty
  }
  {
    meta: self.meta(self.text_loc_of_span(span)),
    v: self.layout_clean_raw_span1(pad~, span),
  }
}

///|
fn Parser::layout_clean_raw_span1(
  self : Parser,
  pad~ : Int32 = 0,
  span : LineSpan,
) -> String {
  if self.no_layouts {
    return ""
  }
  @char.utf_16_clean_raw(
    pad~,
    self.buf,
    self.i,
    first=span.first,
    last=span.last,
  )
}

///|
fn Parser::tight_block_lines(
  self : Parser,
  spans~ : ArrayView[Span],
) -> Array[Tight] {
  self._tight_block_lines(Parser::clean_unesc_unref_span, spans~)
}

///|
fn Parser::raw_tight_block_lines(
  self : Parser,
  spans~ : ArrayView[Span],
) -> Array[Tight] {
  self._tight_block_lines(fn(p, s) { p.clean_raw_span(s) }, spans~)
}

///|
priv struct ParserCleanSpanFn((Parser, LineSpan) -> StringNode)

///|
fn Parser::_tight_block_lines(
  self : Parser,
  f : ParserCleanSpanFn,
  spans~ : ArrayView[Span],
) -> Array[Tight] {
  let acc = []
  for i, span in spans {
    match (span, i) {
      ({ span: fst_ln, .. }, 0) =>
        acc.push({ blanks: "", node: f(self, fst_ln), })
      ({ start: line_start, span, }, _) => {
        let layout = if self.no_layouts || span.first <= line_start {
          ""
        } else {
          @char.utf_16_clean_raw(
            self.buf,
            self.i,
            first=line_start,
            last=span.first - 1,
          )
        }
        acc.push({ blanks: layout, node: f(self, span), })
      }
    }
  }
  acc
}

///|
fn Parser::first_non_blank_in_span(
  self : Parser,
  span : LineSpan,
) -> CharCodePos {
  @cmark_base.first_non_blank_in_span(self.i, span)
}

///|
fn[A] Parser::first_non_blank_over_nl(
  self : Parser,
  next_line~ : NextLineFn[A],
  lines : A,
  line : LineSpan,
  start~ : CharCodePos,
) -> (LineSpan, Array[StringNode], Int32)? {
  match
    @cmark_base.first_non_blank_over_nl(
      next_line~,
      self.i,
      lines,
      line~,
      start~,
    ) {
    None => None
    Some(ThisLine(nb)) => {
      let layout = if nb == start {
        []
      } else {
        [self.clean_raw_span({ ..line, first: start, last: nb - 1, })]
      }
      Some((line, layout, nb))
    }
    Some(NextLine(newline, nb)) => {
      let first_layout = self.clean_raw_span({ ..line, first: start, })
      let next_layout = self.clean_raw_span({ ..newline, last: nb - 1, })
      let layout = [first_layout, next_layout]
      Some((newline, layout, nb))
    }
  }
}