///|
priv struct TableCell {
  content : String
  align : String
}

///|
priv struct TableRow {
  cells : Array[TableCell]
}

///|
priv struct PendingTableCell {
  row : Int
  col : Int
  cell : TableCell
}

///|
priv struct TableSpanBudget {
  mut remaining : Int
}

///|
const MAX_TABLE_COL_SPAN : Int = 1000

///|
const MAX_TABLE_ROW_SPAN : Int = 65534

///|
/// Maximum number of placeholder cells introduced by spans in one table.
const MAX_TABLE_SPAN_EXPANSION : Int = 1000

///|
/// Maximum size of the normalized rectangular Markdown table. Larger tables
/// fall back to rendering their child content without table padding.
const MAX_TABLE_RENDER_CELLS : Int = 100000

///|
fn render_table(ctx : RenderCtx, out : StringBuilder, node : @dom.Node) -> Bool {
  if ctx.is_inside_link {
    return false
  }
  if @domext.attr_or(node, "role", "").to_lower() is "presentation" {
    render_presentation_table(ctx, out, node)
    return true
  }
  let rows : Array[TableRow] = []
  let row_nodes : Array[@dom.Node] = []
  collect_table_row_nodes(row_nodes, node)
  build_table_rows(ctx, rows, row_nodes)
  if rows.length() == 0 {
    return false
  }
  let column_count = table_column_count(rows)
  if column_count == 0 {
    return false
  }
  if column_count > MAX_TABLE_RENDER_CELLS / rows.length() {
    return false
  }
  out.write_string("\n\n")
  write_table_row(out, rows[0], column_count)
  write_table_delimiter(out, rows[0], column_count)
  for i in 1.. Unit {
  let row_nodes : Array[@dom.Node] = []
  collect_table_row_nodes(row_nodes, node)
  let rows : Array[String] = []
  for row_node in row_nodes {
    let cells : Array[String] = []
    for child in row_node.children.copy() {
      match @domext.node_name(child) {
        "th" | "td" => cells.push(render_table_cell(ctx, child))
        _ => ()
      }
    }
    let row = @textutils.trim_space(cells.join(" "))
    if row != "" {
      rows.push(row)
    }
  }
  if rows.length() > 0 {
    out.write_string("\n\n")
    out.write_string(rows.join("\n\n"))
    out.write_string("\n\n")
  }
}

///|
fn collect_table_row_nodes(rows : Array[@dom.Node], node : @dom.Node) -> Unit {
  match @domext.node_name(node) {
    "tr" => rows.push(node)
    "table" | "thead" | "tbody" | "tfoot" =>
      for child in node.children.copy() {
        collect_table_row_nodes(rows, child)
      }
    _ => ()
  }
}

///|
fn build_table_rows(
  ctx : RenderCtx,
  rows : Array[TableRow],
  row_nodes : Array[@dom.Node],
) -> Unit {
  let pending : Array[PendingTableCell] = []
  let span_budget : TableSpanBudget = { remaining: MAX_TABLE_SPAN_EXPANSION }
  for row_index, row_node in row_nodes {
    match
      build_table_row(
        ctx,
        row_node,
        row_index,
        row_nodes.length(),
        pending,
        span_budget,
      ) {
      Some(row) => rows.push(row)
      None => ()
    }
  }
}

///|
fn build_table_row(
  ctx : RenderCtx,
  row_node : @dom.Node,
  row_index : Int,
  row_count : Int,
  pending : Array[PendingTableCell],
  span_budget : TableSpanBudget,
) -> TableRow? {
  let cells : Array[TableCell] = []
  let mut col_index = 0
  for child in row_node.children.copy() {
    match @domext.node_name(child) {
      "th" | "td" => {
        drain_pending_table_cells(cells, pending, row_index, col_index)
        col_index = cells.length()
        let cell = {
          content: render_table_cell(ctx, child),
          align: normalize_table_align(@domext.attr_or(child, "align", "")),
        }
        let requested_col_span = parse_table_span(
          @domext.attr_or(child, "colspan", ""),
        )
        let remaining_rows = row_count - row_index
        let max_row_span = MAX_TABLE_ROW_SPAN.min(remaining_rows)
        let requested_row_span = parse_table_span(
          @domext.attr_or(child, "rowspan", ""),
          max_value=max_row_span,
        )
        let (col_span, row_span) = take_table_spans(
          requested_col_span, requested_row_span, span_budget,
        )
        cells.push(cell)
        for offset in 1.. ()
    }
  }
  drain_pending_table_cells(cells, pending, row_index, col_index)
  if cells.length() == 0 {
    None
  } else {
    Some({ cells, })
  }
}

///|
fn take_table_spans(
  requested_col_span : Int,
  requested_row_span : Int,
  budget : TableSpanBudget,
) -> (Int, Int) {
  let col_span = requested_col_span.min(budget.remaining + 1)
  budget.remaining -= col_span - 1
  let row_span = requested_row_span.min(1 + budget.remaining / col_span)
  budget.remaining -= (row_span - 1) * col_span
  (col_span, row_span)
}

///|
fn drain_pending_table_cells(
  cells : Array[TableCell],
  pending : Array[PendingTableCell],
  row_index : Int,
  start_col : Int,
) -> Unit {
  let mut col_index = start_col
  let mut keep_going = true
  while keep_going {
    match pop_pending_table_cell(pending, row_index, col_index) {
      Some(cell) => {
        cells.push(cell)
        col_index += 1
      }
      None => keep_going = false
    }
  }
}

///|
fn pop_pending_table_cell(
  pending : Array[PendingTableCell],
  row_index : Int,
  col_index : Int,
) -> TableCell? {
  for i in 0.. TableCell {
  { content: "", align }
}

///|
fn normalize_table_align(value : String) -> String {
  match value.to_lower() {
    "left" => "left"
    "center" => "center"
    "right" => "right"
    _ => ""
  }
}

///|
fn parse_table_span(
  value : String,
  max_value? : Int = MAX_TABLE_COL_SPAN,
) -> Int {
  let max_value = max_value.max(1)
  let mut span = 0
  for ch in value {
    if ch < '0' || ch > '9' {
      return 1
    }
    let digit = ch.to_int() - '0'.to_int()
    if span > (max_value - digit) / 10 {
      return max_value
    }
    span = span * 10 + digit
    if span >= max_value {
      return max_value
    }
  }
  if span < 1 {
    1
  } else {
    span
  }
}

///|
fn render_table_cell(ctx : RenderCtx, node : @dom.Node) -> String {
  let buf = StringBuilder()
  render_children(ctx, buf, node)
  let content = @textutils.trim_space(buf.to_string())
  let content = @textutils.trim_consecutive_newlines(content)
  let content = @textutils.trim_unnecessary_hard_line_breaks(content)
  let content = content.replace_all(
    old=@escape.marker_code_block_newline,
    new=" ",
  )
  let content = @textutils.collapse_inline_code_content(content)
  escape_table_pipes(content)
}

///|
fn escape_table_pipes(content : String) -> String {
  let chars = content.to_array()
  let out = StringBuilder(size_hint=content.length())
  let mut i = 0
  while i < chars.length() {
    if chars[i] == @escape.MARKER_ESCAPING &&
      i + 1 < chars.length() &&
      chars[i + 1] == '|' {
      out.write_string("\\|")
      i += 2
      continue
    }
    if chars[i] == '|' {
      out.write_string("\\|")
    } else {
      out.write_char(chars[i])
    }
    i += 1
  }
  out.to_string()
}

///|
fn table_column_count(rows : Array[TableRow]) -> Int {
  let mut count = 0
  for row in rows {
    if row.cells.length() > count {
      count = row.cells.length()
    }
  }
  count
}

///|
fn table_cell_at(row : TableRow, index : Int) -> String {
  match row.cells.get(index) {
    Some(cell) => cell.content
    None => ""
  }
}

///|
fn table_align_at(row : TableRow, index : Int) -> String {
  match row.cells.get(index) {
    Some(cell) => cell.align
    None => ""
  }
}

///|
fn write_table_row(
  out : StringBuilder,
  row : TableRow,
  column_count : Int,
) -> Unit {
  out.write_char('|')
  for i in 0.. Unit {
  out.write_char('|')
  for i in 0.. out.write_string(" :--- |")
      "center" => out.write_string(" :---: |")
      "right" => out.write_string(" ---: |")
      _ => out.write_string(" --- |")
    }
  }
  out.write_char('\n')
}