//! Second pass: resolve inline markup and emit events.

// Allowing arbitrary depth nested parentheses inside link destinations
// can create denial of service vulnerabilities. The simplest countermeasure
// is to limit their depth.

///|
const LINK_MAX_NESTED_PARENS : Int = 32

///|
/// Markdown event and source range iterator.
pub struct Parser {
  text : Bytes
  options : Options
  tree : Tree[Item]
  allocs : Allocations
  html_scan_guard : HtmlScanGuard
  // https://github.com/pulldown-cmark/pulldown-cmark/issues/844
  mut link_ref_expansion_limit : Int
  // used by inline passes
  inline_stack : InlineStack
  link_stack : LinkStack
  wikilink_stack : LinkStack
  code_delims : CodeDelims
  math_delims : MathDelims
}

///|
pub fn parse(source : String, options : Options) -> Array[Event] {
  let text = @utf8.encode(source)
  let parser = new_parser(text, options)
  let events : Array[Event] = []
  while true {
    match parser.next_event() {
      Some(event) => events.push(event)
      None => break
    }
  }
  events
}

///|
/// Creates a new event parser for a markdown string with given options.
pub fn new_parser(text : Bytes, options : Options) -> Parser {
  let (tree, allocs) = run_first_pass(text, options)
  tree.reset()
  Parser::{
    text,
    options,
    tree,
    allocs,
    html_scan_guard: html_scan_guard_default(),
    // always allow 100KiB
    link_ref_expansion_limit: text.length().max(100_000),
    inline_stack: inline_stack_new(),
    link_stack: link_stack_new(),
    wikilink_stack: link_stack_new(),
    code_delims: code_delims_new(),
    math_delims: math_delims_new(),
  }
}

///|
/// Returns the next event, or `None` when the document is exhausted.
pub fn Parser::next_event(self : Parser) -> Event? {
  self.next_event_range().map(ev => ev.0)
}

///|
/// Returns the next event and its source range (start, end), or `None`.
pub fn Parser::next_event_range(self : Parser) -> (Event, Int, Int)? {
  match self.tree.cur() {
    None =>
      match self.tree.pop() {
        Some(ix) => {
          let ix = if self.tree.nodes[ix].item.body is TightParagraph {
            // tight paragraphs emit nothing
            ignore(self.tree.next_sibling(ix))
            return self.next_event_range()
          } else {
            ix
          }
          let tag_end = body_to_tag_end(self.tree.nodes[ix].item.body)
          ignore(self.tree.next_sibling(ix))
          let span = (
            self.tree.nodes[ix].item.start,
            self.tree.nodes[ix].item.end,
          )
          Some((End(tag_end), span.0, span.1))
        }
        None => None
      }
    Some(cur_ix) => {
      let cur_ix = if self.tree.nodes[cur_ix].item.body is TightParagraph {
        // tight paragraphs emit nothing
        ignore(self.tree.push())
        self.tree.cur().unwrap()
      } else {
        cur_ix
      }
      if item_body_is_maybe_inline(self.tree.nodes[cur_ix].item.body) {
        self.handle_inline()
      }

      let node = self.tree.nodes[cur_ix]
      let item = node.item
      let event = item_to_event(item, self.text, self.allocs)
      match event {
        Start(_) => ignore(self.tree.push())
        _ => ignore(self.tree.next_sibling(cur_ix))
      }
      Some((event, item.start, item.end))
    }
  }
}

///|
fn body_to_tag_end(body : ItemBody) -> TagEnd {
  match body {
    Paragraph => TagEnd::Paragraph
    Emphasis => TagEnd::Emphasis
    Superscript => TagEnd::Superscript
    Subscript => TagEnd::Subscript
    Strong => TagEnd::Strong
    Strikethrough => TagEnd::Strikethrough
    Highlight => TagEnd::Highlight
    Link(_) => TagEnd::Link
    Image(_) => TagEnd::Image
    Heading(level, _) => TagEnd::Heading(level)
    IndentCodeBlock | FencedCodeBlock(_) => TagEnd::CodeBlock
    Container(_, kind, _) => TagEnd::ContainerBlock(kind)
    BlockQuote(kind) => TagEnd::BlockQuote(kind)
    HtmlBlock => TagEnd::HtmlBlock
    List(_, c, _) => TagEnd::List(c == b'.' || c == b')')
    ListItem(_) => TagEnd::Item
    TableHead => TagEnd::TableHead
    TableCell => TagEnd::TableCell
    TableRow => TagEnd::TableRow
    Table(_) => TagEnd::Table
    FootnoteDefinition(_) => TagEnd::FootnoteDefinition
    MetadataBlock(kind) => TagEnd::MetadataBlock(kind)
    DefinitionList(_) => TagEnd::DefinitionList
    DefinitionListTitle => TagEnd::DefinitionListTitle
    DefinitionListDefinition(_) => TagEnd::DefinitionListDefinition
    _ => abort("unexpected item body")
  }
}

///|
fn item_to_event(item : Item, text : Bytes, allocs : Allocations) -> Event {
  let tag = match item.body {
    Text(_) =>
      return Text(@utf8.decode_lossy(text.view(start=item.start, end=item.end)))
    Code(cow_ix) => return Code(allocs.take_cow(cow_ix))
    SynthesizeText(cow_ix) => return Text(allocs.take_cow(cow_ix))
    SynthesizeChar(c) => return Text(c.to_string())
    HtmlBlock => Tag::HtmlBlock
    Html =>
      return Html(
        replace_nuls(
          @utf8.decode_lossy(text.view(start=item.start, end=item.end)),
        ),
      )
    InlineHtml =>
      return InlineHtml(
        replace_nuls(
          @utf8.decode_lossy(text.view(start=item.start, end=item.end)),
        ),
      )
    OwnedInlineHtml(cow_ix) => return InlineHtml(allocs.take_cow(cow_ix))
    SoftBreak => return SoftBreak
    HardBreak(_) => return HardBreak
    FootnoteReference(cow_ix) =>
      return FootnoteReference(allocs.take_cow(cow_ix))
    TaskListMarker(checked) => return TaskListMarker(checked)
    Rule => return Rule
    Paragraph => Tag::Paragraph
    Emphasis => Tag::Emphasis
    Superscript => Tag::Superscript
    Subscript => Tag::Subscript
    Strong => Tag::Strong
    Strikethrough => Tag::Strikethrough
    Highlight => Tag::Highlight
    Link(link_ix) => {
      let (link_type, dest_url, title, id) = allocs.take_link(link_ix)
      Tag::Link(link_type~, dest_url~, title~, id~)
    }
    Image(link_ix) => {
      let (link_type, dest_url, title, id) = allocs.take_link(link_ix)
      Tag::Image(link_type~, dest_url~, title~, id~)
    }
    Heading(level, Some(heading_ix)) => {
      let attrs = allocs.heading_at(heading_ix)
      Tag::Heading(
        level~,
        id=attrs.id,
        classes=attrs.classes,
        attrs=attrs.attrs,
      )
    }
    Heading(level, None) => Tag::Heading(level~, id=None, classes=[], attrs=[])
    FencedCodeBlock(cow_ix) => Tag::CodeBlock(Fenced(allocs.take_cow(cow_ix)))
    IndentCodeBlock => Tag::CodeBlock(Indented)
    Container(_, kind, cow_ix) =>
      Tag::ContainerBlock(kind, allocs.take_cow(cow_ix))
    BlockQuote(kind) => Tag::BlockQuote(kind)
    List(_, c, listitem_start) =>
      if c == b'.' || c == b')' {
        Tag::List(Some(listitem_start))
      } else {
        Tag::List(None)
      }
    ListItem(_) => Tag::Item
    TableHead => Tag::TableHead
    TableCell => Tag::TableCell
    TableRow => Tag::TableRow
    Table(alignment_ix) => Tag::Table(allocs.take_alignment(alignment_ix))
    FootnoteDefinition(cow_ix) =>
      Tag::FootnoteDefinition(allocs.take_cow(cow_ix))
    MetadataBlock(kind) => Tag::MetadataBlock(kind)
    Math(cow_ix, is_display) => {
      let s = allocs.take_cow(cow_ix)
      return if is_display { DisplayMath(s) } else { InlineMath(s) }
    }
    DefinitionList(_) => Tag::DefinitionList
    DefinitionListTitle => Tag::DefinitionListTitle
    DefinitionListDefinition(_) => Tag::DefinitionListDefinition
    _ => abort("unexpected item body")
  }
  Start(tag)
}