///|
/// Rows of a table grouped by section.
pub struct TableRows {
  mut head : Array[Array[Node]]
  mut body : Array[Array[Node]]
  mut foot : Array[Array[Node]]
}

///|
/// Implicit/explicit header state (Ruby `has_header_option`: true, :implicit, nil/false).
pub(all) enum HeaderOption {
  NoHeader
  ExplicitHeader
  ImplicitHeader
  UnsetHeader // Ruby nil
} derive(Eq, Debug)

///|
/// Table-specific data.
pub struct TableData {
  rows : TableRows
  mut columns : Array[Node]
  mut has_header_option : HeaderOption
}

///|
/// Table cell data.
pub struct CellData {
  mut text : String
  mut colspan : Int?
  mut rowspan : Int?
  mut inner_document : Node?
  priv mut cursor : Cursor?
  priv mut reinitialize_args : (Node?, String, Attributes?, Cursor?)?
}

///|
pub fn Node::table_data(self : Node) -> TableData {
  match self.table_ {
    Some(t) => t
    None => abort("not a table")
  }
}

///|
pub fn Node::cell_data(self : Node) -> CellData {
  match self.cell_ {
    Some(c) => c
    None => abort("not a table cell")
  }
}

///|
pub fn Node::rows(self : Node) -> TableRows {
  self.table_data().rows
}

///|
pub fn Node::columns(self : Node) -> Array[Node] {
  self.table_data().columns
}

///|
pub fn Node::colspan(self : Node) -> Int? {
  self.cell_data().colspan
}

///|
pub fn Node::rowspan(self : Node) -> Int? {
  self.cell_data().rowspan
}

///|
pub fn Node::inner_document(self : Node) -> Node? {
  self.cell_data().inner_document
}

///|
/// The column of a table cell, or the table of a column.
pub fn Node::column(self : Node) -> Node? {
  self.parent_
}

///|
fn attr_number(x : Double) -> AttrVal {
  if @rb.is_integral(x) {
    Int(x.to_int())
  } else {
    Float(x)
  }
}

///|
/// Creates a table (Ruby `Table.new parent, attributes`).
pub fn Node::new_table(parent : Node, attributes : Attributes) -> Node {
  let t = Node::alloc(Some(parent), Table)
  t.table_ = Some({
    rows: { head: [], body: [], foot: [], },
    columns: [],
    has_header_option: NoHeader,
  })
  let pcwidth_intval = match attributes.str("width") {
    Some(pcwidth) => {
      let mut v = @rb.to_i(pcwidth)
      if v > 100 || v < 1 {
        if !(v == 0 && (pcwidth == "0" || pcwidth == "0%")) {
          v = 100
        }
      }
      v
    }
    None => 100
  }
  t.attributes.set("tablepcwidth", Int(pcwidth_intval))
  match t.document().attributes.get("pagewidth") {
    Some(pw) if pw.truthy() => {
      let abswidth = @rb.float_truncate(
        pcwidth_intval.to_double() / 100.0 * pw.to_f(),
        4,
      )
      t.attributes.set("tableabswidth", attr_number(abswidth))
    }
    _ => ()
  }
  if attributes.truthy("rotate-option") {
    t.attributes.set_str("orientation", "landscape")
  }
  t
}

///|
/// Ruby `Table#header_row?`.
fn Node::header_row(self : Node) -> HeaderOption {
  let td = self.table_data()
  match td.has_header_option {
    ExplicitHeader | ImplicitHeader if td.rows.body.is_empty() =>
      td.has_header_option
    _ => NoHeader
  }
}

///|
/// A column spec (Ruby Hash with width, halign, valign, style).
pub(all) struct ColSpec {
  mut width : Int
  mut halign : String?
  mut valign : String?
  mut style : String?
} derive(Debug)

///|
/// Creates the columns from colspecs (Ruby `create_columns`).
fn Node::create_columns(self : Node, colspecs : Array[ColSpec]) -> Unit {
  let cols = []
  let autowidth_cols = []
  let mut width_base = 0
  for colspec in colspecs {
    let a = Attributes::new()
    match colspec.halign {
      Some(h) => a.set_str("halign", h)
      None => ()
    }
    match colspec.valign {
      Some(v) => a.set_str("valign", v)
      None => ()
    }
    a.set("width", Int(colspec.width))
    match colspec.style {
      Some(s) => a.set_str("style", s)
      None => ()
    }
    let col = Node::new_table_column(self, cols.length(), a)
    cols.push(col)
    if colspec.width < 0 {
      autowidth_cols.push(col)
    } else {
      width_base += colspec.width
    }
  }
  self.table_data().columns = cols
  let num_cols = cols.length()
  if num_cols > 0 {
    self.attributes.set("colcount", Int(num_cols))
    let wb = if width_base > 0 || !autowidth_cols.is_empty() {
      Some(width_base)
    } else {
      None
    }
    self.assign_column_widths(
      width_base?=wb,
      autowidth_cols?=if autowidth_cols.is_empty() {
        None
      } else {
        Some(autowidth_cols)
      },
    )
  }
}

///|
/// Ruby numeric addition preserving Integer/Float distinction.
fn num_add(a : AttrVal, b : AttrVal) -> AttrVal {
  match (a, b) {
    (Int(x), Int(y)) => Int(x + y)
    _ => Float(a.to_f() + b.to_f())
  }
}

///|
fn num_sub(a : AttrVal, b : AttrVal) -> AttrVal {
  match (a, b) {
    (Int(x), Int(y)) => Int(x - y)
    _ => Float(a.to_f() - b.to_f())
  }
}

///|
/// Ruby `Table#assign_column_widths`.
fn Node::assign_column_widths(
  self : Node,
  width_base? : Int,
  autowidth_cols? : Array[Node],
) -> Unit {
  let precision = 4
  let columns = self.table_data().columns
  let mut total_width : AttrVal = Int(0)
  let mut col_pcwidth : AttrVal = Int(0)
  match width_base {
    Some(wb0) => {
      let mut wb = wb0
      match autowidth_cols {
        Some(acols) => {
          let autowidth = if wb > 100 {
            log_warn(
              "total column width must not exceed 100% when using autowidth columns; got \{wb}%",
            )
            Int(0)
          } else {
            let aw = @rb.float_truncate(
              (100.0 - wb.to_double()) / acols.length().to_double(),
              precision,
            )
            wb = 100
            attr_number(aw)
          }
          for col in acols {
            col.attributes.set("width", autowidth)
            col.attributes.set_str("autowidth-option", "")
          }
        }
        None => ()
      }
      for col in columns {
        col_pcwidth = col.assign_width(Int(0), Some(wb), precision)
        total_width = num_add(total_width, col_pcwidth)
      }
    }
    None => {
      col_pcwidth = attr_number(
        @rb.float_truncate(100.0 / columns.length().to_double(), precision),
      )
      for col in columns {
        total_width = num_add(
          total_width,
          col.assign_width(col_pcwidth, None, precision),
        )
      }
    }
  }
  if total_width.to_f() != 100.0 {
    let last = columns[columns.length() - 1]
    let v = num_add(num_sub(Int(100), total_width), col_pcwidth)
    let rounded = match v {
      Int(_) => v
      _ => Float(@rb.float_round(v.to_f(), precision))
    }
    last.assign_width(rounded, None, precision) |> ignore
  }
}

///|
/// Ruby `Table::Column#assign_width`; returns the percentage width.
fn Node::assign_width(
  self : Node,
  col_pcwidth : AttrVal,
  width_base : Int?,
  precision : Int,
) -> AttrVal {
  let pc = match width_base {
    Some(wb) =>
      attr_number(
        @rb.float_truncate(
          self.attributes.get("width").unwrap_or(Int(1)).to_f() *
          100.0 /
          wb.to_double(),
          precision,
        ),
      )
    None => col_pcwidth
  }
  match self.parent_ {
    Some(table) =>
      match table.attributes.get("tableabswidth") {
        Some(abs) if abs.truthy() => {
          let colabs = @rb.float_truncate(
            pc.to_f() / 100.0 * abs.to_f(),
            precision,
          )
          self.attributes.set("colabswidth", attr_number(colabs))
        }
        _ => ()
      }
    None => ()
  }
  self.attributes.set("colpcwidth", pc)
  pc
}

///|
/// Partitions header and footer rows (Ruby `partition_header_footer`).
fn Node::partition_header_footer(self : Node, attrs : Attributes) -> Unit {
  let td = self.table_data()
  let body = td.rows.body
  let mut num_body_rows = body.length()
  self.attributes.set("rowcount", Int(num_body_rows))
  if num_body_rows > 0 {
    if td.has_header_option == ExplicitHeader ||
      td.has_header_option == ImplicitHeader {
      let first = body.remove(0)
      td.rows.head = [first.map(cell => cell.reinitialize(true))]
      num_body_rows -= 1
    } else if td.has_header_option == UnsetHeader {
      td.has_header_option = NoHeader
      let first = body.remove(0)
      body.insert(0, first.map(cell => cell.reinitialize(false)))
    }
  }
  if num_body_rows > 0 && attrs.truthy("footer-option") {
    td.rows.foot = [body.pop().unwrap()]
  }
}

///|
/// Creates a table column (Ruby `Table::Column.new table, index, attributes`).
fn Node::new_table_column(
  table : Node,
  index : Int,
  attributes : Attributes,
) -> Node {
  let col = Node::alloc(Some(table), TableColumn)
  col.level = 0
  col.style = attributes.str("style")
  attributes.set("colnumber", Int(index + 1))
  if !attributes.truthy("width") {
    attributes.set("width", Int(1))
  }
  attributes.set_default("halign", Str("left"))
  attributes.set_default("valign", Str("top"))
  col.attributes.update(attributes)
  col
}

///|
/// The table of a column.
pub fn Node::table(self : Node) -> Node? {
  self.parent_
}

///|
/// Creates a table cell (Ruby `Table::Cell.new column, text, attributes, opts`).
fn Node::new_table_cell(
  column : Node?,
  cell_text : String,
  attributes : Attributes?,
  cursor : Cursor?,
) -> Node {
  let parent = column
  let c = match parent {
    Some(col) => Node::alloc(Some(col), TableCell)
    None => abort("cell without column")
  }
  let cd : CellData = {
    text: "",
    colspan: None,
    rowspan: None,
    inner_document: None,
    cursor: None,
    reinitialize_args: None,
  }
  c.cell_ = Some(cd)
  if c.document().sourcemap() {
    c.source_location = cursor.map(cu => cu.dup())
  }
  let mut cell_style : String? = None
  let mut in_header_row = NoHeader
  let mut cell_text = cell_text
  match column {
    Some(col) => {
      in_header_row = match col.parent_ {
        Some(table) => table.header_row()
        None => NoHeader
      }
      if in_header_row != NoHeader {
        if in_header_row == ImplicitHeader {
          let s = match col.style {
            Some(s) => Some(s)
            None =>
              match attributes {
                Some(a) => a.str("style")
                None => None
              }
          }
          match s {
            Some(st) => {
              if st == "asciidoc" || st == "literal" {
                cd.reinitialize_args = Some(
                  (column, cell_text, attributes.map(a => a.copy()), cursor),
                )
              }
              cell_style = None
            }
            None => ()
          }
        }
      } else {
        cell_style = col.style
      }
      c.attributes.update(col.attributes)
    }
    None => ()
  }
  let mut asciidoc = false
  let mut literal = false
  let mut normal_psv = false
  let mut inner_cursor : Cursor? = None
  match attributes {
    Some(attrs) => {
      if !attrs.is_empty() {
        cd.colspan = attrs.remove("colspan").map(v => v.to_i())
        cd.rowspan = attrs.remove("rowspan").map(v => v.to_i())
        if in_header_row == NoHeader {
          match attrs.str("style") {
            Some(s) => cell_style = Some(s)
            None => ()
          }
        }
        c.attributes.update(attrs)
      }
      match cell_style {
        Some("asciidoc") => {
          asciidoc = true
          inner_cursor = cursor
          cell_text = @rb.rstrip(cell_text)
          if cell_text.has_prefix("\n") {
            let mut lines_advanced = 1
            cell_text = @rb.from(cell_text, 1)
            while cell_text.has_prefix("\n") {
              lines_advanced += 1
              cell_text = @rb.from(cell_text, 1)
            }
            match inner_cursor {
              Some(ic) => ic.advance(lines_advanced)
              None => ()
            }
          } else {
            cell_text = @rb.lstrip(cell_text)
          }
        }
        Some("literal") => {
          literal = true
          cell_text = @rb.rstrip(cell_text)
          while cell_text.has_prefix("\n") {
            cell_text = @rb.from(cell_text, 1)
          }
        }
        _ => {
          normal_psv = true
          cell_text = @rb.strip(cell_text)
        }
      }
    }
    None =>
      if cell_style == Some("asciidoc") {
        asciidoc = true
        inner_cursor = cursor
      }
  }
  if asciidoc {
    let doc = c.document()
    let parent_doctitle = doc.attributes.remove("doctitle")
    let inner_lines = @rb.split(cell_text, "\n", limit=-1)
    if !inner_lines.is_empty() && inner_lines[0].contains("::") {
      let unprocessed_line1 = inner_lines[0]
      let pre = Reader::new_preprocessor(
        doc,
        [unprocessed_line1],
        cursor?=inner_cursor,
      )
      let preprocessed = pre.read_lines()
      if !(unprocessed_line1 == preprocessed.get(0).unwrap_or("\u{0}") &&
        preprocessed.length() < 2) {
        inner_lines.remove(0) |> ignore
        for i, l in preprocessed {
          inner_lines.insert(i, l)
        }
      }
    }
    let opts = Options::new(standalone=false)
    opts.parent = Some(doc)
    opts.cursor = inner_cursor
    cd.inner_document = Some(new_document(Some(inner_lines), opts))
    match parent_doctitle {
      Some(v) => doc.attributes.set("doctitle", v)
      None => ()
    }
    c.subs = []
  } else if literal {
    c.content_model = Verbatim
    c.subs = basic_subs.copy()
  } else {
    if normal_psv {
      if in_header_row != NoHeader {
        cd.cursor = cursor
      } else {
        c.catalog_cell_inline_anchor(cell_text, cursor)
      }
    }
    c.content_model = Simple
    c.subs = normal_subs.copy()
  }
  cd.text = cell_text
  c.style = cell_style
  c
}

///|
/// Ruby `Table::Cell#reinitialize`.
fn Node::reinitialize(self : Node, has_header : Bool) -> Node {
  let cd = self.cell_data()
  if has_header {
    cd.reinitialize_args = None
  } else {
    match cd.reinitialize_args {
      Some((col, text, attrs, cursor)) =>
        return Node::new_table_cell(col, text, attrs, cursor)
      None => self.style = self.attributes.str("style")
    }
  }
  if cd.cursor is Some(_) {
    self.catalog_cell_inline_anchor(cd.text, None)
  }
  self
}

///|
fn Node::catalog_cell_inline_anchor(
  self : Node,
  cell_text : String,
  cursor : Cursor?,
) -> Unit {
  let cd = self.cell_data()
  let cursor = match cursor {
    Some(c) => Some(c)
    None => {
      let c = cd.cursor
      cd.cursor = None
      c
    }
  }
  if !cell_text.has_prefix("[[") {
    return
  }
  match leading_inline_anchor_rx.find(cell_text) {
    Some(m) =>
      catalog_inline_anchor(m.at(1), m.group(2), self, cursor, self.document())
    None => ()
  }
}

///|
/// Content of a table cell: paragraphs of converted text (Ruby `Cell#content`).
fn Node::cell_content(self : Node) -> Array[String] {
  let cd = self.cell_data()
  let cell_style = self.style
  if cell_style == Some("asciidoc") {
    return [cd.inner_document.unwrap().convert()]
  }
  let parent = self.parent_.unwrap()
  if cd.text.contains("\n\n") {
    blank_line_rx
    .split(self.text().unwrap_or(""))
    .map(para => {
      match cell_style {
        Some(st) if st != "header" =>
          Node::new_inline(parent, Quoted, text=para, type_=st).convert()
        _ => para
      }
    })
  } else {
    let subbed = self.text().unwrap_or("")
    if subbed == "" {
      []
    } else {
      match cell_style {
        Some(st) if st != "header" =>
          [Node::new_inline(parent, Quoted, text=subbed, type_=st).convert()]
        _ => [subbed]
      }
    }
  }
}

///|
/// Content of a table cell as an array of paragraphs.
pub fn Node::cell_paragraphs(self : Node) -> Array[String] {
  self.cell_content()
}

///|
/// Lines of a table cell text.
pub fn Node::cell_lines(self : Node) -> Array[String] {
  @rb.split(self.cell_data().text, "\n")
}

// ---------------------------------------------------------------------------
// Table parser context (Ruby `Table::ParserContext`)

///|
priv struct TableParserContext {
  reader : Reader
  table : Node
  format : String
  mut colcount : Int
  mut buffer : String
  delimiter : String
  delimiter_rx : @regex.Regex
  cellspecs : Array[Attributes?]
  mut cell_open : Bool
  active_rowspans : Array[Int]
  mut column_visits : Int
  mut current_row : Array[Node]
  mut linenum : Int
  start_cursor : Cursor
}

///|
fn TableParserContext::new(
  reader : Reader,
  table : Node,
  attributes : Attributes,
) -> TableParserContext {
  let start_cursor = reader.mark()
  let nested = table.document().is_nested()
  let mut format = "psv"
  let mut xsv = if nested { "!sv" } else { "psv" }
  match attributes.get("format") {
    Some(v) => {
      let f = v.to_s()
      if f == "psv" || f == "csv" || f == "dsv" || f == "tsv" {
        if f == "tsv" {
          format = "csv"
          xsv = "tsv"
        } else {
          format = f
          xsv = if f == "psv" && nested { "!sv" } else { f }
        }
      } else {
        log_error(
          "illegal table format: \{f}",
          source_location=reader.cursor_at_prev_line(),
        )
      }
    }
    None => ()
  }
  let default_delim = fn(x : String) -> String {
    match x {
      "psv" => "|"
      "csv" => ","
      "dsv" => ":"
      "tsv" => "\t"
      _ => "!"
    }
  }
  let delimiter = match attributes.get("separator") {
    Some(v) => {
      let sep = if v.truthy() { v.to_s() } else { "" }
      if sep == "" {
        default_delim(xsv)
      } else if sep == "\\t" {
        "\t"
      } else {
        sep
      }
    }
    None => default_delim(xsv)
  }
  let delimiter_rx = @regex.re(regex_escape(delimiter))
  let columns = table.table_data().columns
  {
    reader,
    table,
    format,
    colcount: if columns.is_empty() {
      -1
    } else {
      columns.length()
    },
    buffer: "",
    delimiter,
    delimiter_rx,
    cellspecs: [],
    cell_open: false,
    active_rowspans: [0],
    column_visits: 0,
    current_row: [],
    linenum: -1,
    start_cursor,
  }
}

///|
/// Escapes regex metacharacters (Ruby `Regexp.escape`).
pub fn regex_escape(s : String) -> String {
  let sb = StringBuilder()
  for c in s {
    match c {
      '.'
      | '*'
      | '?'
      | '+'
      | '^'
      | '$'
      | '|'
      | '('
      | ')'
      | '['
      | ']'
      | '{'
      | '}'
      | '\\'
      | '/'
      | '-' => {
        sb.write_char('\\')
        sb.write_char(c)
      }
      '\n' => sb.write_string("\\n")
      '\t' => sb.write_string("\\t")
      ' ' => sb.write_string("\\ ")
      _ => sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
fn TableParserContext::skip_past_delimiter(
  self : TableParserContext,
  pre : String,
) -> Unit {
  self.buffer = "\{self.buffer}\{pre}\{self.delimiter}"
}

///|
fn TableParserContext::skip_past_escaped_delimiter(
  self : TableParserContext,
  pre : String,
) -> Unit {
  self.buffer = "\{self.buffer}\{@rb.chop(pre)}\{self.delimiter}"
}

///|
fn TableParserContext::buffer_has_unclosed_quotes(
  self : TableParserContext,
  append? : String,
) -> Bool {
  let q = "\""
  let record = match append {
    Some(a) => @rb.strip(self.buffer + a)
    None => @rb.strip(self.buffer)
  }
  if record == q {
    true
  } else if record.has_prefix(q) {
    let qq = q + q
    let trailing_quote = record.has_suffix(q)
    if (trailing_quote && record.has_suffix(qq)) || record.has_prefix(qq) {
      let r = record.replace_all(old=qq, new="")
      r.has_prefix(q) && !r.has_suffix(q)
    } else {
      !trailing_quote
    }
  } else {
    false
  }
}

///|
fn TableParserContext::push_cellspec(
  self : TableParserContext,
  spec : Attributes?,
) -> Unit {
  self.cellspecs.push(Some(spec.unwrap_or(Attributes::new())))
}

///|
fn TableParserContext::close_open_cell(
  self : TableParserContext,
  next_cellspec? : Attributes,
) -> Unit {
  self.push_cellspec(next_cellspec)
  if self.cell_open {
    self.close_cell(eol=true)
  }
  self.linenum += 1
}

///|
fn TableParserContext::close_cell(
  self : TableParserContext,
  eol? : Bool = false,
) -> Unit {
  let mut cell_text = ""
  let mut cellspec : Attributes? = None
  let mut repeat = 1
  if self.format == "psv" {
    cell_text = self.buffer
    self.buffer = ""
    if self.cellspecs.is_empty() {
      log_error(
        "table missing leading separator; recovering automatically",
        source_location=self.start_cursor.dup(),
      )
      cellspec = Some(Attributes::new())
    } else {
      let spec = self.cellspecs.remove(0).unwrap_or(Attributes::new())
      repeat = match spec.remove("repeatcol") {
        Some(v) => v.to_i()
        None => 1
      }
      cellspec = Some(spec)
    }
  } else {
    cell_text = @rb.strip(self.buffer)
    self.buffer = ""
    if self.format == "csv" && cell_text != "" && cell_text.contains("\"") {
      if cell_text.has_prefix("\"") && cell_text.has_suffix("\"") {
        if cell_text.length() >= 2 {
          cell_text = @rb.squeeze(
            @rb.strip(@rb.slice(cell_text, 1, cell_text.length() - 1)),
            chars="\"",
          )
        } else {
          log_error(
            "unclosed quote in CSV data; setting cell to empty",
            source_location=self.reader.cursor_at_prev_line(),
          )
          cell_text = ""
        }
      } else {
        cell_text = @rb.squeeze(cell_text, chars="\"")
      }
    }
  }
  let table = self.table
  let td = table.table_data()
  for i in 1..<=repeat {
    let column = if self.colcount == -1 {
      let col = Node::new_table_column(
        table,
        td.columns.length() + i - 1,
        Attributes::new(),
      )
      td.columns.push(col)
      match cellspec {
        Some(spec) =>
          match spec.get("colspan") {
            Some(cs) => {
              let extra = cs.to_i() - 1
              if extra > 0 {
                let offset = td.columns.length()
                for j in 0.. ()
          }
        None => ()
      }
      Some(col)
    } else {
      td.columns.get(self.current_row.length())
    }
    let cursor_before_mark = self.reader.cursor_before_mark()
    let cell = Node::new_table_cell(
      column,
      cell_text,
      cellspec,
      Some(cursor_before_mark),
    )
    self.reader.mark() |> ignore
    let cd = cell.cell_data()
    match cd.rowspan {
      Some(rs) if rs != 1 => self.activate_rowspan(rs, cd.colspan.unwrap_or(1))
      _ => ()
    }
    self.column_visits += cd.colspan.unwrap_or(1)
    self.current_row.push(cell)
    let row_status = self.end_of_row()
    if row_status > -1 &&
      (self.colcount != -1 || self.linenum > 0 || (eol && i == repeat)) {
      if row_status > 0 {
        log_error(
          "dropping cell because it exceeds specified number of columns",
          source_location=cursor_before_mark,
        )
        self.close_row(drop=true)
      } else {
        self.close_row()
      }
    }
  }
  self.cell_open = false
}

///|
fn TableParserContext::close_table(self : TableParserContext) -> Unit {
  if self.column_visits == 0 {
    return
  }
  log_error(
    "dropping cells from incomplete row detected end of table",
    source_location=self.reader.cursor_before_mark(),
  )
}

///|
fn TableParserContext::close_row(
  self : TableParserContext,
  drop? : Bool = false,
) -> Unit {
  if !drop {
    self.table.table_data().rows.body.push(self.current_row)
  }
  if self.colcount == -1 {
    self.colcount = self.column_visits
  }
  self.column_visits = 0
  self.current_row = []
  self.active_rowspans.remove(0) |> ignore
  if self.active_rowspans.is_empty() {
    self.active_rowspans.push(0)
  }
}

///|
fn TableParserContext::activate_rowspan(
  self : TableParserContext,
  rowspan : Int,
  colspan : Int,
) -> Unit {
  for i in 1.. Int {
  if self.colcount == -1 {
    0
  } else {
    let v = self.column_visits + self.active_rowspans[0]
    if v < self.colcount {
      -1
    } else if v == self.colcount {
      0
    } else {
      1
    }
  }
}