///| GFM table parsing (split from block_parser.mbt).

///| A table is recognized while a paragraph is being built: as soon as its

///| second line turns out to be a delimiter row with the same number of columns

///| as the first, the paragraph becomes a table.

///|
/// Turn `paragraph` into a table when its second line is a delimiter row.
fn maybe_start_table(paragraph : Node) -> Unit {
  guard paragraph.lines.length() == 2 else { return }
  let header = paragraph.lines[0]
  let delimiter = paragraph.lines[1]
  guard delimiter.contains("|") && delimiter.contains("-") else { return }
  let alignments = parse_table_alignments(delimiter)
  guard !alignments.is_empty() else { return }
  let header_columns = split_table_cells(header)
    .filter(fn(c) { !c.trim(chars=" \t\r\n").is_empty() })
    .length()
  guard header_columns == alignments.length() else { return }
  paragraph.kind = NodeKind::TableNode
  paragraph.alignments = alignments
  let _ = paragraph.lines.pop()
}

///|
/// Build the CST node for a finished table.
fn BlockParser::table_to_block(
  self : BlockParser,
  node : Node,
  span : Span,
) -> Block? {
  guard node.lines.length() >= 1 else { return None }
  let columns = node.alignments.length()
  let header = self.parse_table_row(node.lines[0], columns)
  let rows : Array[Array[TableCell]] = []
  for i = 1; i < node.lines.length(); i = i + 1 {
    rows.push(self.parse_table_row(node.lines[i], columns))
  }
  Some(
    Block::Table(
      header~,
      alignments=node.alignments,
      rows~,
      span~,
      leading_trivia=Trivia::empty(),
      trailing_trivia=Trivia::empty(),
    ),
  )
}

///|
/// Parse table alignments from the delimiter row.
fn parse_table_alignments(line : String) -> Array[TableAlign] {
  let alignments : Array[TableAlign] = []
  let cells = split_table_cells(line)
  for cell in cells {
    let trimmed = cell.trim(chars=" \t\r\n")
    // Every delimiter cell has to be a run of dashes, optionally fenced by
    // colons; nothing else, and never empty.
    if trimmed.is_empty() {
      return []
    }
    let mut dashes = 0
    for i = 0; i < trimmed.length(); i = i + 1 {
      let c = trimmed.unsafe_get(i)
      if c == '-' {
        dashes = dashes + 1
      } else if c != ':' {
        return []
      }
    }
    if dashes == 0 {
      return []
    }
    let starts = trimmed.unsafe_get(0) == ':'
    let ends = trimmed.unsafe_get(trimmed.length() - 1) == ':'
    let align = if starts && ends {
      TableAlign::Center
    } else if starts {
      TableAlign::Left
    } else if ends {
      TableAlign::Right
    } else {
      TableAlign::None
    }
    alignments.push(align)
  }
  alignments
}

///|
/// Parse one table row into cells.
fn BlockParser::parse_table_row(
  self : BlockParser,
  line : String,
  expected_cols : Int,
) -> Array[TableCell] {
  let cells : Array[TableCell] = []
  let raw_cells = split_table_cells(line)
  for i, cell in raw_cells {
    if i >= expected_cols {
      break
    }
    let content = cell.trim(chars=" \t\n\r").to_owned()
    cells.push({
      children: self.parse_inline_content(content),
      span: Span::new(0, content.length()),
    })
  }
  while cells.length() < expected_cols {
    cells.push({ children: [], span: Span::new(0, 0) })
  }
  cells
}

///|
/// Split a table row on unescaped pipes, ignoring the optional outer ones.
fn split_table_cells(line : String) -> Array[StringView] {
  let cells : Array[StringView] = []
  let trimmed = line.trim(chars=" \t\r\n")
  let len = trimmed.length()
  let mut start = 0
  let mut end = len
  if len > 0 && trimmed.unsafe_get(0) == '|' {
    start = 1
  }
  if end > start && trimmed.unsafe_get(end - 1) == '|' {
    // A trailing pipe only closes the row when it is not escaped.
    let mut backslashes = 0
    let mut k = end - 2
    while k >= start && trimmed.unsafe_get(k) == '\\' {
      backslashes = backslashes + 1
      k = k - 1
    }
    if backslashes % 2 == 0 {
      end = end - 1
    }
  }
  let pipe = "|"[:]
  let mut cell_start = start
  let mut search_start = start
  let mut has_escaped_pipe = false
  while search_start < end {
    match trimmed[search_start:end].find(pipe) {
      None => break
      Some(relative) => {
        let found = search_start + relative
        let mut backslashes = 0
        let mut k = found - 1
        while k >= cell_start && trimmed.unsafe_get(k) == '\\' {
          backslashes = backslashes + 1
          k = k - 1
        }
        if backslashes % 2 == 0 {
          let cell = trimmed[cell_start:found]
          cells.push(
            if has_escaped_pipe {
              unescape_table_pipes(cell)
            } else {
              cell
            },
          )
          cell_start = found + 1
          has_escaped_pipe = false
        } else {
          has_escaped_pipe = true
        }
        search_start = found + 1
      }
    }
  }
  let final_cell = trimmed[cell_start:end]
  cells.push(
    if has_escaped_pipe {
      unescape_table_pipes(final_cell)
    } else {
      final_cell
    },
  )
  cells
}

///|
/// Materialize only cells that actually contain an escaped pipe.
fn unescape_table_pipes(cell : StringView) -> StringView {
  let output = StringBuilder(size_hint=cell.length())
  let mut run_start = 0
  let mut i = 0
  while i < cell.length() {
    if cell.unsafe_get(i) == '\\' &&
      i + 1 < cell.length() &&
      cell.unsafe_get(i + 1) == '|' {
      if run_start < i {
        output.write_string(cell[run_start:i].to_owned())
      }
      output.write_char('|')
      i = i + 2
      run_start = i
    } else {
      i = i + 1
    }
  }
  if run_start < cell.length() {
    output.write_string(cell[run_start:].to_owned())
  }
  output.to_string()[:]
}