//! The first pass resolves all block structure, generating an AST.
//! Within a block, items are in a linear chain with potential inline markup
//! identified.

// Each level of brace nesting adds another entry to a hash table.

///|
const MATH_BRACE_CONTEXT_MAX_NESTING : Int = 25

///|
/// State for the first parsing pass.
priv struct FirstPass {
  text : Bytes
  tree : Tree[Item]
  mut begin_list_item : Int?
  mut last_line_blank : Bool
  allocs : Allocations
  options : Options
  // Math environment brace nesting.
  brace_context_stack : Array[Int]
  mut brace_context_next : Int
}

///|
fn run_first_pass(text : Bytes, options : Options) -> (Tree[Item], Allocations) {
  // This is a very naive heuristic for the number of nodes we'll need.
  let start_capacity = (128).max(text.length() / 32)
  let first_pass = FirstPass::{
    text,
    tree: tree_with_capacity(start_capacity),
    begin_list_item: None,
    last_line_blank: false,
    allocs: allocations_new(),
    options,
    brace_context_stack: [],
    brace_context_next: 0,
  }
  first_pass.run()
}

///|
fn FirstPass::run(self : FirstPass) -> (Tree[Item], Allocations) {
  let mut ix = 0
  while ix < self.text.length() {
    ix = self.parse_block(ix)
  }
  while self.tree.spine_len() > 0 {
    self.pop(ix)
  }
  (self.tree, self.allocs)
}

///|
/// Returns offset after block.
fn FirstPass::parse_block(self : FirstPass, start_ix : Int) -> Int {
  let mut start_ix = start_ix
  let bytes = self.text
  let mut line_start = LineStart::new(bytes.view(start=start_ix))

  // math spans and their braces are tracked only within a single block
  self.brace_context_stack.clear()
  self.brace_context_next = 0

  let i = line_start.scan_containers(self.tree, self.options)
  for _ in i.. {
        let is_footnote = self.tree.nodes[node_ix].item.body
          is FootnoteDefinition(_)
        if is_footnote {
          if self.last_line_blank {
            self.pop(start_ix)
          }
        }
      }
      None => ()
    }
  }

  // Process new containers
  while true {
    let save = line_start.clone()
    let outer_indent = line_start.scan_space_upto(4)
    guard outer_indent < 4 else {
      line_start.restore(save)
      break
    }
    if self.options.contains(enable_footnotes()) {
      // Footnote definitions
      let container_start = start_ix + line_start.bytes_scanned()
      match self.parse_footnote(container_start) {
        Some(bytecount) => {
          start_ix = container_start + bytecount
          if self.options.contains(enable_old_footnotes()) {
            // gfm footnotes need indented, but old footnotes don't
            match scan_blank_line(bytes.view(start=start_ix)) {
              Some(n) => start_ix += n
              None => ()
            }
          }
          line_start = LineStart::new(bytes.view(start=start_ix))
          continue
        }
        None => ()
      }
    }
    let container_start = start_ix + line_start.bytes_scanned()
    match line_start.scan_list_marker_with_indent(outer_indent) {
      Some((ch, index, indent)) => {
        let after_marker_index = start_ix + line_start.bytes_scanned()
        self.continue_list(container_start - outer_indent, ch, index)
        ignore(
          self.tree.append(Item::{
            start: container_start - outer_indent,
            end: after_marker_index, // will get updated later if item not empty
            body: ListItem(indent),
          }),
        )
        ignore(self.tree.push())
        match scan_blank_line(bytes.view(start=after_marker_index)) {
          Some(n) => {
            self.begin_list_item = Some(after_marker_index + n)
            return after_marker_index + n
          }
          None => ()
        }
        if self.options.contains(enable_tasklists()) {
          match line_start.scan_task_list_marker() {
            Some(is_checked) => {
              let task_list_marker = Item::{
                start: after_marker_index,
                end: start_ix + line_start.bytes_scanned(),
                body: TaskListMarker(is_checked),
              }
              match scan_blank_line(bytes.view(start=task_list_marker.end)) {
                Some(n) => {
                  ignore(self.tree.append(task_list_marker))
                  self.begin_list_item = Some(task_list_marker.end + n)
                  return task_list_marker.end + n
                }
                None => {
                  line_start.scan_all_space()
                  let ix = start_ix + line_start.bytes_scanned()
                  return self.parse_paragraph(ix, Some(task_list_marker))
                }
              }
            }
            None => ()
          }
        }
      }
      None => {
        // Definition list?
        let is_deflist = self.options.contains(enable_definition_list())
        let mut deflist_handled = false
        if is_deflist {
          match self.tree.cur() {
            Some(cur) => {
              let child = self.tree.nodes[cur].child
              let cur_item = self.tree.nodes[cur].item
              let is_para = cur_item.body
                is (Paragraph
                | TightParagraph
                | MaybeDefinitionListTitle
                | DefinitionListDefinition(_))
              if is_para {
                match
                  line_start.scan_definition_list_definition_marker_with_indent(
                    outer_indent,
                  ) {
                  Some(indent2) => {
                    match self.tree.nodes[cur].item.body {
                      Paragraph | TightParagraph => {
                        self.tree.nodes[cur].item.body = DefinitionList(true)
                        let list_idx = self.tree.cur().unwrap()
                        let title_idx = self.tree.create_node(Item::{
                          start: cur_item.start,
                          end: cur_item.end, // will get updated later if item not empty
                          body: DefinitionListTitle,
                        })
                        self.tree.nodes[title_idx].child = child
                        self.tree.nodes[list_idx].child = Some(title_idx)
                        ignore(self.tree.push())
                      }
                      MaybeDefinitionListTitle =>
                        self.tree.nodes[cur].item.body = DefinitionListTitle
                      DefinitionListDefinition(_) => ()
                      _ => ()
                    }
                    let after_marker_index = start_ix +
                      line_start.bytes_scanned()
                    ignore(
                      self.tree.append(Item::{
                        start: container_start - outer_indent,
                        end: after_marker_index, // will get updated later
                        body: DefinitionListDefinition(indent2),
                      }),
                    )
                    match self.tree.peek_up() {
                      Some(up) =>
                        match self.tree.nodes[up].item.body {
                          DefinitionList(_) =>
                            if self.last_line_blank {
                              self.tree.nodes[up].item.body = DefinitionList(
                                false,
                              )
                              self.last_line_blank = false
                            }
                          _ => ()
                        }
                      None => ()
                    }
                    ignore(self.tree.push())
                    match
                      scan_blank_line(bytes.view(start=after_marker_index)) {
                      Some(n) => {
                        self.begin_list_item = Some(after_marker_index + n)
                        return after_marker_index + n
                      }
                      None => ()
                    }
                    deflist_handled = true
                  }
                  None => ()
                }
              }
            }
            None => ()
          }
        }
        guard !deflist_handled else { continue }
        if line_start.scan_blockquote_marker() {
          let kind = if self.options.contains(enable_gfm()) {
            line_start.scan_blockquote_tag()
          } else {
            None
          }
          self.finish_list(start_ix)
          ignore(
            self.tree.append(Item::{
              start: container_start,
              end: 0,
              body: BlockQuote(kind),
            }),
          )
          ignore(self.tree.push())
          if kind is Some(_) {
            // blockquote tag leaves us at the end of the line
            let ix = start_ix + line_start.bytes_scanned()
            let lazy_line_start = LineStart::new(bytes.view(start=ix))
            let tree_position = lazy_line_start.scan_containers(
              self.tree,
              self.options,
            )
            let current_container = tree_position == self.tree.spine_len()
            let interrupt = !lazy_line_start.scan_space(4) &&
              self.scan_paragraph_interrupt(
                bytes.view(start=ix + lazy_line_start.bytes_scanned()),
                current_container,
                tree_position,
              )
            guard !interrupt else { return ix }
            // blockquote tags act as if they were nested in a paragraph
            line_start = lazy_line_start
            line_start.scan_all_space()
            start_ix = ix
            break
          }
          continue
        } else {
          let is_container_fence = self.options.contains(
              enable_container_extensions(),
            ) &&
            scan_ch_repeat(
              bytes.view(start=start_ix + line_start.bytes_scanned()),
              b':',
            ) >
            2
          if is_container_fence {
            let fence_length = scan_while_max(
              bytes.view(start=start_ix + line_start.bytes_scanned()),
              fn(c) { c == b':' },
              255,
            )
            guard self.tree.spine_len() <= 255 else { break }
            let excess_colons = scan_while(
              bytes.view(
                start=start_ix + line_start.bytes_scanned() + fence_length,
              ),
              fn(c) { c == b':' },
            )
            let mut kind_start = start_ix +
              line_start.bytes_scanned() +
              fence_length +
              excess_colons
            kind_start += scan_whitespace_no_nl(bytes.view(start=kind_start))
            let kind_length = scan_while(bytes.view(start=kind_start), fn(c) {
              c.is_ascii_alphanumeric() ||
              c == b'_' ||
              c == b'-' ||
              c == b':' ||
              c == b'.'
            })
            guard kind_length != 0 else { break }
            let kind = unescape(
              self.text,
              kind_start,
              kind_start + kind_length,
              self.tree.is_in_table(),
            )
            let mut summary_start = kind_start + kind_length
            summary_start += scan_whitespace_no_nl(
              bytes.view(start=summary_start),
            )
            let line_end = summary_start +
              scan_nextline(bytes.view(start=summary_start))
            let summary_end = line_end -
              scan_rev_while(bytes.view(start=summary_start, end=line_end), fn(
                c,
              ) {
                c.to_char().is_ascii_whitespace()
              })
            let is_spoiler = eq_ignore_ascii_case(
              @utf8.encode(kind).view(),
              b"spoiler".view(),
            )
            if is_spoiler {
              let summary = unescape(
                self.text,
                summary_start,
                summary_end,
                self.tree.is_in_table(),
              )
              let summary_cow_ix = self.allocs.allocate_cow(summary)
              ignore(
                self.tree.append(Item::{
                  start: container_start,
                  end: 0,
                  body: Container(fence_length, Spoiler, summary_cow_ix),
                }),
              )
            } else {
              let kind_cow_ix = self.allocs.allocate_cow(kind)
              ignore(
                self.tree.append(Item::{
                  start: container_start,
                  end: 0,
                  body: Container(fence_length, Default, kind_cow_ix),
                }),
              )
            }
            ignore(self.tree.push())
            return summary_end + 1
          } else {
            line_start.restore(save)
            break
          }
        }
      }
    }
  }

  if self.options.contains(enable_container_extensions()) {
    let mut pop_count : Int? = None
    let spine = self.tree.walk_spine()
    let mut idx = 0
    while idx < spine.length() {
      let node_ix = spine[spine.length() - 1 - idx]
      match self.tree.nodes[node_ix].item.body {
        Container(length, _, _) =>
          if line_start.scan_closing_container_extensions_fence(length) {
            pop_count = Some(idx + 1)
            break
          }
        _ => break
      }
      idx += 1
    }

    match pop_count {
      Some(c) =>
        for _ in 0.. ()
    }
  }
  let ix = start_ix + line_start.bytes_scanned()

  match scan_blank_line(bytes.view(start=ix)) {
    Some(n) => {
      match self.tree.peek_up() {
        Some(node_ix) =>
          match self.tree.nodes[node_ix].item.body {
            Container(_, _, _) => ()
            BlockQuote(_) => ()
            ListItem(_) | DefinitionListDefinition(_) if self.begin_list_item
              is Some(_) => {
              self.last_line_blank = true
              // This is a blank list item.
              self.tree.nodes[node_ix].item.body = ListItem(0)
            }
            _ => self.last_line_blank = true
          }
        None => self.last_line_blank = true
      }
      return ix + n
    }
    None => ()
  }

  // Save `remaining_space` here to avoid needing to backtrack `line_start` for HTML blocks
  let remaining_space = line_start.remaining_space()

  let indent = line_start.scan_space_upto(4)
  guard indent != 4 else {
    self.finish_list(start_ix)
    let ix = start_ix + line_start.bytes_scanned()
    let remaining_space = line_start.remaining_space()
    return self.parse_indented_code_block(ix, remaining_space)
  }

  let ix = start_ix + line_start.bytes_scanned()

  // metadata blocks cannot be indented
  if indent == 0 {
    match
      scan_metadata_block(
        bytes.view(start=ix),
        self.options.contains(enable_yaml_style_metadata_blocks()),
        self.options.contains(enable_pluses_delimited_metadata_blocks()),
      ) {
      Some((_n, metadata_block_ch)) => {
        self.finish_list(start_ix)
        return self.parse_metadata_block(ix, metadata_block_ch)
      }
      None => ()
    }
  }

  // HTML Blocks
  if bytes.length() > ix && bytes.unsafe_get(ix) == b'<' {
    // Types 1-5 are all detected by one function
    match get_html_end_tag(bytes.view(start=ix + 1)) {
      Some(html_end_tag) => {
        self.finish_list(start_ix)
        return self.parse_html_block_type_1_to_5(
          ix, html_end_tag, remaining_space, indent,
        )
      }
      None => ()
    }

    // Detect type 6
    guard !starts_html_block_type_6(bytes.view(start=ix + 1)) else {
      self.finish_list(start_ix)
      return self.parse_html_block_type_6_or_7(ix, remaining_space, indent)
    }

    // Detect type 7
    guard scan_html_type_7(bytes.view(start=ix)) is None else {
      self.finish_list(start_ix)
      return self.parse_html_block_type_6_or_7(ix, remaining_space, indent)
    }
  }

  match scan_hrule(bytes.view(start=ix)) {
    Ok(n) => {
      self.finish_list(start_ix)
      return self.parse_hrule(n, ix)
    }
    Err(_) => ()
  }

  match scan_atx_heading(bytes.view(start=ix)) {
    Some(atx_size) => {
      self.finish_list(start_ix)
      return self.parse_atx_heading(ix, atx_size)
    }
    None => ()
  }

  match scan_code_fence(bytes.view(start=ix)) {
    Some((n, fence_ch)) => {
      self.finish_list(start_ix)
      return self.parse_fenced_code_block(ix, indent, fence_ch, n)
    }
    None => ()
  }

  // parse refdef
  while true {
    match self.parse_refdef_total(start_ix + line_start.bytes_scanned()) {
      Some((bytecount, label, link_def)) => {
        let folded_label = unicase_fold(label)
        if !self.allocs.refdefs.contains(folded_label) {
          self.allocs.refdefs[folded_label] = link_def
        }
        let container_start = start_ix + line_start.bytes_scanned()
        let mut ix = container_start + bytecount
        // Refdefs act as if they were contained within a paragraph
        match scan_blank_line(bytes.view(start=ix)) {
          Some(nl) => ix += nl
          None => {
            self.finish_list(start_ix)
            return ix
          }
        }
        match self.scan_next_line_or_lazy_continuation(bytes.view(start=ix)) {
          Some(lazy_line_start) => {
            line_start = lazy_line_start
            start_ix = ix
          }
          None => {
            self.finish_list(start_ix)
            return ix
          }
        }
      }
      None => break
    }
  }

  let ix = start_ix + line_start.bytes_scanned()

  self.parse_paragraph(ix, None)
}