"""Custom docutils writer for plain text."""

from __future__ import annotations

import math
import os
import re
import textwrap
from itertools import chain, groupby, pairwise
from typing import TYPE_CHECKING, cast

from docutils import nodes, writers
from docutils.utils import column_width

from sphinx import addnodes
from sphinx.locale import _, admonitionlabels
from sphinx.util.docutils import SphinxTranslator

if TYPE_CHECKING:
    from collections.abc import Iterable, Iterator, Sequence
    from typing import Any, ClassVar

    from docutils.nodes import Element, Text

    from sphinx.builders.text import TextBuilder


class Cell:
    """Represents a cell in a table.
    It can span multiple columns or multiple lines.
    """

    def __init__(self, text: str = '', rowspan: int = 1, colspan: int = 1) -> None:
        self.text = text
        self.wrapped: list[str] = []
        self.rowspan = rowspan
        self.colspan = colspan
        self.col: int | None = None
        self.row: int | None = None

    def __repr__(self) -> str:
        return (
            f'<Cell {self.text!r} {self.row}v{self.rowspan}/{self.col}>{self.colspan}>'
        )

    def __hash__(self) -> int:
        return hash((self.col, self.row))

    def __bool__(self) -> bool:
        return bool(self.text) and self.col is not None and self.row is not None

    def wrap(self, width: int) -> None:
        self.wrapped = my_wrap(self.text, width)


class Table:
    """Represents a table, handling cells that can span multiple lines
    or rows, like::

       +-----------+-----+
       | AAA       | BBB |
       +-----+-----+     |
       |     | XXX |     |
       |     +-----+-----+
       | DDD | CCC       |
       +-----+-----------+

    This class can be used in two ways, either:

    - With absolute positions: call ``table[line, col] = Cell(...)``,
      this overwrites any existing cell(s) at these positions.

    - With relative positions: call the ``add_row()`` and
      ``add_cell(Cell(...))`` as needed.

    Cells spanning multiple rows or multiple columns (having a
    colspan or rowspan greater than one) are automatically referenced
    by all the table cells they cover. This is a useful
    representation as we can simply check
    ``if self[x, y] is self[x, y+1]`` to recognize a rowspan.

    Colwidth is not automatically computed, it has to be given, either
    at construction time, or during the table construction.

    Example usage::

       table = Table([6, 6])
       table.add_cell(Cell("foo"))
       table.add_cell(Cell("bar"))
       table.set_separator()
       table.add_row()
       table.add_cell(Cell("FOO"))
       table.add_cell(Cell("BAR"))
       print(table)
       +--------+--------+
       | foo    | bar    |
       |========|========|
       | FOO    | BAR    |
       +--------+--------+

    """

    def __init__(self, colwidth: list[int] | None = None) -> None:
        self.lines: list[list[Cell]] = []
        self.separator = 0
        self.colwidth: list[int] = colwidth if colwidth is not None else []
        self.current_line = 0
        self.current_col = 0

    def add_row(self) -> None:
        """Add a row to the table, to use with ``add_cell()``.  It is not needed
        to call ``add_row()`` before the first ``add_cell()``.
        """
        self.current_line += 1
        self.current_col = 0

    def set_separator(self) -> None:
        """Sets the separator below the current line."""
        self.separator = len(self.lines)

    def add_cell(self, cell: Cell) -> None:
        """Add a cell to the current line, to use with ``add_row()``.  To add
        a cell spanning multiple lines or rows, simply set the
        ``cell.colspan`` or ``cell.rowspan`` BEFORE inserting it into
        the table.
        """
        while self[self.current_line, self.current_col]:
            self.current_col += 1
        self[self.current_line, self.current_col] = cell
        self.current_col += cell.colspan

    def __getitem__(self, pos: tuple[int, int]) -> Cell:
        line, col = pos
        self._ensure_has_line(line + 1)
        self._ensure_has_column(col + 1)
        return self.lines[line][col]

    def __setitem__(self, pos: tuple[int, int], cell: Cell) -> None:
        line, col = pos
        self._ensure_has_line(line + cell.rowspan)
        self._ensure_has_column(col + cell.colspan)
        for dline in range(cell.rowspan):
            for dcol in range(cell.colspan):
                self.lines[line + dline][col + dcol] = cell
                cell.row = line
                cell.col = col

    def _ensure_has_line(self, line: int) -> None:
        while len(self.lines) < line:
            self.lines.append([])

    def _ensure_has_column(self, col: int) -> None:
        for line in self.lines:
            while len(line) < col:
                line.append(Cell())

    def __repr__(self) -> str:
        return '\n'.join(map(repr, self.lines))

    def cell_width(self, cell: Cell, source: list[int]) -> int:
        """Give the cell width, according to the given source (either
        ``self.colwidth`` or ``self.measured_widths``).
        This takes into account cells spanning multiple columns.
        """
        if cell.row is None or cell.col is None:
            msg = 'Cell co-ordinates have not been set'
            raise ValueError(msg)
        width = 0
        for i in range(self[cell.row, cell.col].colspan):
            width += source[cell.col + i]
        return width + (cell.colspan - 1) * 3

    @property
    def cells(self) -> Iterator[Cell]:
        seen: set[Cell] = set()
        for line in self.lines:
            for cell in line:
                if cell and cell not in seen:
                    yield cell
                    seen.add(cell)

    def rewrap(self) -> None:
        """Call ``cell.wrap()`` on all cells, and measure each column width
        after wrapping (result written in ``self.measured_widths``).
        """
        self.measured_widths = self.colwidth[:]
        for cell in self.cells:
            cell.wrap(width=self.cell_width(cell, self.colwidth))
            if not cell.wrapped:
                continue
            if cell.row is None or cell.col is None:
                msg = 'Cell co-ordinates have not been set'
                raise ValueError(msg)
            width = math.ceil(max(column_width(x) for x in cell.wrapped) / cell.colspan)
            for col in range(cell.col, cell.col + cell.colspan):
                self.measured_widths[col] = max(self.measured_widths[col], width)

    def physical_lines_for_line(self, line: list[Cell]) -> int:
        """For a given line, compute the number of physical lines it spans
        due to text wrapping.
        """
        physical_lines = 1
        for cell in line:
            physical_lines = max(physical_lines, len(cell.wrapped))
        return physical_lines

    def __str__(self) -> str:
        out = []
        self.rewrap()

        def writesep(char: str = '-', lineno: int | None = None) -> str:
            """Called on the line *before* lineno.
            Called with no *lineno* for the last sep.
            """
            out: list[str] = []
            for colno, width in enumerate(self.measured_widths):
                if (
                    lineno is not None
                    and lineno > 0
                    and self[lineno, colno] is self[lineno - 1, colno]
                ):
                    out.append(' ' * (width + 2))
                else:
                    out.append(char * (width + 2))
            head = '+' if out[0][0] == '-' else '|'
            tail = '+' if out[-1][0] == '-' else '|'
            glue = [
                '+' if left[0] == '-' or right[0] == '-' else '|'
                for left, right in pairwise(out)
            ]
            glue.append(tail)
            return head + ''.join(chain.from_iterable(zip(out, glue, strict=False)))

        for lineno, line in enumerate(self.lines):
            if self.separator and lineno == self.separator:
                out.append(writesep('=', lineno))
            else:
                out.append(writesep('-', lineno))
            for physical_line in range(self.physical_lines_for_line(line)):
                linestr = ['|']
                for colno, cell in enumerate(line):
                    if cell.col != colno:
                        continue
                    if lineno != cell.row:  # NoQA: SIM114
                        physical_text = ''
                    elif physical_line >= len(cell.wrapped):
                        physical_text = ''
                    else:
                        physical_text = cell.wrapped[physical_line]
                    adjust_len = len(physical_text) - column_width(physical_text)
                    linestr.append(
                        ' '
                        + physical_text.ljust(
                            self.cell_width(cell, self.measured_widths)
                            + 1
                            + adjust_len,
                        )
                        + '|',
                    )
                out.append(''.join(linestr))
        out.append(writesep('-'))
        return '\n'.join(out)


class TextWrapper(textwrap.TextWrapper):
    """Custom subclass that uses a different word separator regex."""

    wordsep_re = re.compile(
        r'(\s+|'                                  # any whitespace
        r'(?<=\s)(?::[a-z-]+:)?`\S+|'             # interpreted text start
        r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|'   # hyphenated words
        r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))'    # em-dash
    )  # fmt: skip

    def _wrap_chunks(self, chunks: list[str]) -> list[str]:
        """The original _wrap_chunks uses len() to calculate width.

        This method respects wide/fullwidth characters for width adjustment.
        """
        lines: list[str] = []
        if self.width <= 0:
            raise ValueError('invalid width %r (must be > 0)' % self.width)

        chunks.reverse()

        while chunks:
            cur_line = []
            cur_len = 0

            if lines:
                indent = self.subsequent_indent
            else:
                indent = self.initial_indent

            # Note that column_width(x) > len(x) is possible,
            # but _handle_long_word() handles negative widths.
            width = self.width - column_width(indent)

            if self.drop_whitespace and not chunks[-1].strip() and lines:
                del chunks[-1]

            while chunks:
                chunk_width = column_width(chunks[-1])
                if cur_len + chunk_width <= width:
                    cur_line.append(chunks.pop())
                    cur_len += chunk_width
                else:
                    break

            if chunks and column_width(chunks[-1]) > width:
                self._handle_long_word(chunks, cur_line, cur_len, width)

            if self.drop_whitespace and cur_line and not cur_line[-1].strip():
                del cur_line[-1]

            if cur_line:
                lines.append(indent + ''.join(cur_line))

        return lines

    @staticmethod
    def _find_break_end(word: str, space_left: int) -> int:
        """Break word by Unicode width instead of len(word).

        The returned position 'end' satisfies::

            assert column_width(word[:end]) <= space_left
            assert end == len(word) or column_width(word[:end+1]) > space_left
        """
        total = 0
        for end, c in enumerate(word, start=1):
            total += column_width(c)
            if total > space_left:
                return end - 1
        return len(word)

    def _split(self, text: str) -> list[str]:
        """Override original method that only split by 'wordsep_re'.

        This '_split' splits wide-characters into chunks by one character.
        """
        chunks: list[str] = []
        for chunk in super()._split(text):
            for w, g in groupby(chunk, _column_width_safe):
                if w <= 1:
                    chunks += super()._split(''.join(g))
                else:
                    chunks += g
        return chunks

    def _handle_long_word(
        self, reversed_chunks: list[str], cur_line: list[str], cur_len: int, width: int
    ) -> None:
        """Override using self._find_break() instead of str.find()."""
        # Make sure at least one character is stripped off on every pass.
        #
        # Do NOT use space_left = max(width - cur_len, 1) as corner cases
        # with "self.drop_whitespace == False" and "self.width == 1" fail.
        space_left = 1 if width < 1 else (width - cur_len)

        if self.break_long_words:
            # Some characters may have len(X) < space_left < column_width(X)
            # so we should only wrap chunks for which len(X) > space_left.
            end = space_left
            chunk = reversed_chunks[-1]
            if space_left > 0:
                end = self._find_break_end(chunk, space_left)
            if end == 0 and space_left:
                # force processing at least one character
                end = 1
            cur_line.append(chunk[:end])
            reversed_chunks[-1] = chunk[end:]
        elif not cur_line:
            cur_line.append(reversed_chunks.pop())


def _column_width_safe(x: str) -> int:
    # Handle characters that are 0-width. We should refine
    # the grouping to prevent splitting a word at combining
    # characters or in a group of combining characters with
    # at most one non-combining character as the combining
    # characters may act on the right or left character.
    #
    # See https://github.com/sphinx-doc/sphinx/issues/13741.
    return max(1, column_width(x))


MAXWIDTH = 70
STDINDENT = 3


def my_wrap(text: str, width: int = MAXWIDTH, **kwargs: Any) -> list[str]:
    w = TextWrapper(width=width, **kwargs)
    return w.wrap(text)


class TextWriter(writers.Writer):  # type: ignore[type-arg]
    supported = ('text',)
    settings_spec = ('No options here.', '', ())
    settings_defaults: ClassVar[dict[str, Any]] = {}

    output: str

    def __init__(self, builder: TextBuilder) -> None:
        super().__init__()
        self.builder = builder

    def translate(self) -> None:
        assert isinstance(self.document, nodes.document)
        visitor = self.builder.create_translator(self.document, self.builder)
        self.document.walkabout(visitor)
        self.output = cast('TextTranslator', visitor).body


class TextTranslator(SphinxTranslator):
    builder: TextBuilder

    def __init__(self, document: nodes.document, builder: TextBuilder) -> None:
        super().__init__(document, builder)

        newlines = self.config.text_newlines
        if newlines == 'windows':
            self.nl = '\r\n'
        elif newlines == 'native':
            self.nl = os.linesep
        else:
            self.nl = '\n'
        self.sectionchars = self.config.text_sectionchars
        self.add_secnumbers = self.config.text_add_secnumbers
        self.secnumber_suffix = self.config.text_secnumber_suffix
        self.states: list[list[tuple[int, str | list[str]]]] = [[]]
        self.stateindent = [0]
        self.list_counter: list[int] = []
        self.sectionlevel = 0
        self.lineblocklevel = 0
        self.table: Table
        self.in_production_list = False

        self.context: list[str] = []
        """Heterogeneous stack.

        Used by visit_* and depart_* functions in conjunction with the tree
        traversal. Make sure that the pops correspond to the pushes.
        """

    def add_text(self, text: str) -> None:
        self.states[-1].append((-1, text))

    def new_state(self, indent: int = STDINDENT) -> None:
        self.states.append([])
        self.stateindent.append(indent)

    def end_state(
        self,
        wrap: bool = True,
        end: Sequence[str] | None = ('',),
        first: str | None = None,
    ) -> None:
        content = self.states.pop()
        maxindent = sum(self.stateindent)
        indent = self.stateindent.pop()
        result: list[tuple[int, list[str]]] = []
        toformat: list[str] = []

        def do_format() -> None:
            if not toformat:
                return
            if wrap:
                res = my_wrap(''.join(toformat), width=MAXWIDTH - maxindent)
            else:
                res = ''.join(toformat).splitlines()
            if end:
                res += end
            result.append((indent, res))

        for itemindent, item in content:
            if itemindent == -1:
                toformat.append(item)  # type: ignore[arg-type]
            else:
                do_format()
                result.append((indent + itemindent, item))  # type: ignore[arg-type]
                toformat = []
        do_format()
        if first is not None and result:
            # insert prefix into first line (ex. *, [1], See also, etc.)
            newindent = result[0][0] - indent
            if result[0][1] == ['']:
                result.insert(0, (newindent, [first]))
            else:
                text = first + result[0][1].pop(0)
                result.insert(0, (newindent, [text]))

        self.states[-1].extend(result)

    def visit_document(self, node: Element) -> None:
        self.new_state(0)

    def depart_document(self, node: Element) -> None:
        self.end_state()
        self.body = self.nl.join(
            line and (' ' * indent + line)
            for indent, lines in self.states[0]
            for line in lines
        )
        # XXX header/footer?

    def visit_section(self, node: Element) -> None:
        self._title_char = self.sectionchars[self.sectionlevel]
        self.sectionlevel += 1

    def depart_section(self, node: Element) -> None:
        self.sectionlevel -= 1

    def visit_topic(self, node: Element) -> None:
        self.new_state(0)

    def depart_topic(self, node: Element) -> None:
        self.end_state()

    visit_sidebar = visit_topic
    depart_sidebar = depart_topic

    def visit_rubric(self, node: Element) -> None:
        self.new_state(0)
        self.add_text('-[ ')

    def depart_rubric(self, node: Element) -> None:
        self.add_text(' ]-')
        self.end_state()

    def visit_compound(self, node: Element) -> None:
        pass

    def depart_compound(self, node: Element) -> None:
        pass

    def visit_glossary(self, node: Element) -> None:
        pass

    def depart_glossary(self, node: Element) -> None:
        pass

    def visit_title(self, node: Element) -> None:
        if isinstance(node.parent, nodes.Admonition):
            self.add_text(node.astext() + ': ')
            raise nodes.SkipNode
        self.new_state(0)

    def get_section_number_string(self, node: Element) -> str:
        if isinstance(node.parent, nodes.section):
            anchorname = '#' + node.parent['ids'][0]
            numbers = self.builder.secnumbers.get(anchorname)
            if numbers is None:
                numbers = self.builder.secnumbers.get('')
            if numbers is not None:
                return '.'.join(map(str, numbers)) + self.secnumber_suffix
        return ''

    def depart_title(self, node: Element) -> None:
        if isinstance(node.parent, nodes.section):
            char = self._title_char
        else:
            char = '^'
        text = ''
        text = ''.join(x[1] for x in self.states.pop() if x[0] == -1)  # type: ignore[misc]
        if self.add_secnumbers:
            text = self.get_section_number_string(node) + text
        self.stateindent.pop()
        title = ['', text, '%s' % (char * column_width(text)), '']
        if len(self.states) == 2 and len(self.states[-1]) == 0:
            # remove an empty line before title if it is first section title in the document
            title.pop(0)
        self.states[-1].append((0, title))

    def visit_subtitle(self, node: Element) -> None:
        pass

    def depart_subtitle(self, node: Element) -> None:
        pass

    def visit_attribution(self, node: Element) -> None:
        self.add_text('-- ')

    def depart_attribution(self, node: Element) -> None:
        pass

    #############################################################
    # Domain-specific object descriptions
    #############################################################

    # Top-level nodes
    #################

    def visit_desc(self, node: Element) -> None:
        pass

    def depart_desc(self, node: Element) -> None:
        pass

    def visit_desc_signature(self, node: Element) -> None:
        self.new_state(0)

    def depart_desc_signature(self, node: Element) -> None:
        # XXX: wrap signatures in a way that makes sense
        self.end_state(wrap=False, end=None)

    def visit_desc_signature_line(self, node: Element) -> None:
        pass

    def depart_desc_signature_line(self, node: Element) -> None:
        self.add_text('\n')

    def visit_desc_content(self, node: Element) -> None:
        self.new_state()
        self.add_text(self.nl)

    def depart_desc_content(self, node: Element) -> None:
        self.end_state()

    def visit_desc_inline(self, node: Element) -> None:
        pass

    def depart_desc_inline(self, node: Element) -> None:
        pass

    # Nodes for high-level structure in signatures
    ##############################################

    def visit_desc_name(self, node: Element) -> None:
        pass

    def depart_desc_name(self, node: Element) -> None:
        pass

    def visit_desc_addname(self, node: Element) -> None:
        pass

    def depart_desc_addname(self, node: Element) -> None:
        pass

    def visit_desc_type(self, node: Element) -> None:
        pass

    def depart_desc_type(self, node: Element) -> None:
        pass

    def visit_desc_returns(self, node: Element) -> None:
        self.add_text(' -> ')

    def depart_desc_returns(self, node: Element) -> None:
        pass

    def _visit_sig_parameter_list(
        self,
        node: Element,
        parameter_group: type[Element],
        sig_open_paren: str,
        sig_close_paren: str,
    ) -> None:
        """Visit a signature parameters or type parameters list.

        The *parameter_group* value is the type of a child node acting as a required parameter
        or as a set of contiguous optional parameters.
        """
        self.add_text(sig_open_paren)
        self.is_first_param = True
        self.optional_param_level = 0
        self.params_left_at_level = 0
        self.param_group_index = 0
        # Counts as what we call a parameter group are either a required parameter, or a
        # set of contiguous optional ones.
        self.list_is_required_param = [
            isinstance(c, parameter_group) for c in node.children
        ]
        self.required_params_left = sum(self.list_is_required_param)
        self.param_separator = ', '
        self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
        self.trailing_comma = node.get('multi_line_trailing_comma', False)
        if self.multi_line_parameter_list:
            self.param_separator = self.param_separator.rstrip()
        self.context.append(sig_close_paren)

    def _depart_sig_parameter_list(self, node: Element) -> None:
        sig_close_paren = self.context.pop()
        self.add_text(sig_close_paren)

    def visit_desc_parameterlist(self, node: Element) -> None:
        self._visit_sig_parameter_list(node, addnodes.desc_parameter, '(', ')')

    def depart_desc_parameterlist(self, node: Element) -> None:
        self._depart_sig_parameter_list(node)

    def visit_desc_type_parameter_list(self, node: Element) -> None:
        self._visit_sig_parameter_list(node, addnodes.desc_type_parameter, '[', ']')

    def depart_desc_type_parameter_list(self, node: Element) -> None:
        self._depart_sig_parameter_list(node)

    def visit_desc_parameter(self, node: Element) -> None:
        on_separate_line = self.multi_line_parameter_list
        if on_separate_line and not (
            self.is_first_param and self.optional_param_level > 0
        ):
            self.new_state()
        if self.is_first_param:
            self.is_first_param = False
        elif not on_separate_line and not self.required_params_left:
            self.add_text(self.param_separator)
        if self.optional_param_level == 0:
            self.required_params_left -= 1
        else:
            self.params_left_at_level -= 1

        self.add_text(node.astext())

        is_required = self.list_is_required_param[self.param_group_index]
        if on_separate_line:
            len_lirp = len(self.list_is_required_param)
            is_last_group = self.param_group_index + 1 == len_lirp
            next_is_required = (
                not is_last_group
                and self.list_is_required_param[self.param_group_index + 1]
            )
            opt_param_left_at_level = self.params_left_at_level > 0
            if (
                opt_param_left_at_level
                or is_required
                and (is_last_group or next_is_required)
            ):
                if not is_last_group or opt_param_left_at_level or self.trailing_comma:
                    self.add_text(self.param_separator)
                self.end_state(wrap=False, end=None)

        elif self.required_params_left:
            self.add_text(self.param_separator)

        if is_required:
            self.param_group_index += 1
        raise nodes.SkipNode

    def visit_desc_type_parameter(self, node: Element) -> None:
        self.visit_desc_parameter(node)

    def visit_desc_optional(self, node: Element) -> None:
        self.params_left_at_level = sum(
            isinstance(c, addnodes.desc_parameter) for c in node.children
        )
        self.optional_param_level += 1
        self.max_optional_param_level = self.optional_param_level
        if self.multi_line_parameter_list:
            # If the first parameter is optional, start a new line and open the bracket.
            if self.is_first_param:
                self.new_state()
                self.add_text('[')
            # Else, if there remains at least one required parameter, append the
            # parameter separator, open a new bracket, and end the line.
            elif self.required_params_left:
                self.add_text(self.param_separator)
                self.add_text('[')
                self.end_state(wrap=False, end=None)
            # Else, open a new bracket, append the parameter separator, and end the
            # line.
            else:
                self.add_text('[')
                self.add_text(self.param_separator)
                self.end_state(wrap=False, end=None)
        else:
            self.add_text('[')

    def depart_desc_optional(self, node: Element) -> None:
        self.optional_param_level -= 1
        level = self.optional_param_level
        if self.multi_line_parameter_list:
            max_level = self.max_optional_param_level
            len_lirp = len(self.list_is_required_param)
            is_last_group = self.param_group_index + 1 == len_lirp
            # If it's the first time we go down one level, add the separator before the
            # bracket, except if this is the last parameter and the parameter list
            # should not feature a trailing comma.
            if level == max_level - 1 and (
                not is_last_group or level > 0 or self.trailing_comma
            ):
                self.add_text(self.param_separator)
            self.add_text(']')
            # End the line if we have just closed the last bracket of this group of
            # optional parameters.
            if level == 0:
                self.end_state(wrap=False, end=None)

        else:
            self.add_text(']')
        if level == 0:
            self.param_group_index += 1

    def visit_desc_annotation(self, node: Element) -> None:
        pass

    def depart_desc_annotation(self, node: Element) -> None:
        pass

    ##############################################

    def visit_figure(self, node: Element) -> None:
        self.new_state()

    def depart_figure(self, node: Element) -> None:
        self.end_state()

    def visit_caption(self, node: Element) -> None:
        pass

    def depart_caption(self, node: Element) -> None:
        pass

    def visit_productionlist(self, node: Element) -> None:
        self.new_state()
        self.in_production_list = True

    def depart_productionlist(self, node: Element) -> None:
        self.in_production_list = False
        self.end_state(wrap=False)

    def visit_production(self, node: Element) -> None:
        pass

    def depart_production(self, node: Element) -> None:
        pass

    def visit_footnote(self, node: Element) -> None:
        label = cast('nodes.label', node[0])
        self._footnote = label.astext().strip()
        self.new_state(len(self._footnote) + 3)

    def depart_footnote(self, node: Element) -> None:
        self.end_state(first='[%s] ' % self._footnote)

    def visit_citation(self, node: Element) -> None:
        if len(node) and isinstance(node[0], nodes.label):
            self._citlabel = node[0].astext()
        else:
            self._citlabel = ''
        self.new_state(len(self._citlabel) + 3)

    def depart_citation(self, node: Element) -> None:
        self.end_state(first='[%s] ' % self._citlabel)

    def visit_label(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_legend(self, node: Element) -> None:
        pass

    def depart_legend(self, node: Element) -> None:
        pass

    # XXX: option list could use some better styling

    def visit_option_list(self, node: Element) -> None:
        pass

    def depart_option_list(self, node: Element) -> None:
        pass

    def visit_option_list_item(self, node: Element) -> None:
        self.new_state(0)

    def depart_option_list_item(self, node: Element) -> None:
        self.end_state()

    def visit_option_group(self, node: Element) -> None:
        self._firstoption = True

    def depart_option_group(self, node: Element) -> None:
        self.add_text('     ')

    def visit_option(self, node: Element) -> None:
        if self._firstoption:
            self._firstoption = False
        else:
            self.add_text(', ')

    def depart_option(self, node: Element) -> None:
        pass

    def visit_option_string(self, node: Element) -> None:
        pass

    def depart_option_string(self, node: Element) -> None:
        pass

    def visit_option_argument(self, node: Element) -> None:
        self.add_text(node['delimiter'])

    def depart_option_argument(self, node: Element) -> None:
        pass

    def visit_description(self, node: Element) -> None:
        pass

    def depart_description(self, node: Element) -> None:
        pass

    def visit_tabular_col_spec(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_colspec(self, node: Element) -> None:
        self.table.colwidth.append(node['colwidth'])
        raise nodes.SkipNode

    def visit_tgroup(self, node: Element) -> None:
        pass

    def depart_tgroup(self, node: Element) -> None:
        pass

    def visit_thead(self, node: Element) -> None:
        pass

    def depart_thead(self, node: Element) -> None:
        pass

    def visit_tbody(self, node: Element) -> None:
        self.table.set_separator()

    def depart_tbody(self, node: Element) -> None:
        pass

    def visit_row(self, node: Element) -> None:
        if self.table.lines:
            self.table.add_row()

    def depart_row(self, node: Element) -> None:
        pass

    def visit_entry(self, node: Element) -> None:
        self.entry = Cell(
            rowspan=node.get('morerows', 0) + 1,
            colspan=node.get('morecols', 0) + 1,
        )
        self.new_state(0)

    def depart_entry(self, node: Element) -> None:
        text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop())
        self.stateindent.pop()
        self.entry.text = text
        self.table.add_cell(self.entry)
        del self.entry

    def visit_table(self, node: Element) -> None:
        if hasattr(self, 'table'):
            msg = 'Nested tables are not supported.'
            raise NotImplementedError(msg)
        self.new_state(0)
        self.table = Table()

    def depart_table(self, node: Element) -> None:
        self.add_text(str(self.table))
        del self.table
        self.end_state(wrap=False)

    def visit_acks(self, node: Element) -> None:
        bullet_list = cast('nodes.bullet_list', node[0])
        list_items = cast('Iterable[nodes.list_item]', bullet_list)
        self.new_state(0)
        self.add_text(', '.join(n.astext() for n in list_items) + '.')
        self.end_state()
        raise nodes.SkipNode

    def visit_image(self, node: Element) -> None:
        if 'alt' in node.attributes:
            self.add_text(_('[image: %s]') % node['alt'])
        self.add_text(_('[image]'))
        raise nodes.SkipNode

    def visit_transition(self, node: Element) -> None:
        indent = sum(self.stateindent)
        self.new_state(0)
        self.add_text('=' * (MAXWIDTH - indent))
        self.end_state()
        raise nodes.SkipNode

    def visit_bullet_list(self, node: Element) -> None:
        self.list_counter.append(-1)

    def depart_bullet_list(self, node: Element) -> None:
        self.list_counter.pop()

    def visit_enumerated_list(self, node: Element) -> None:
        self.list_counter.append(node.get('start', 1) - 1)

    def depart_enumerated_list(self, node: Element) -> None:
        self.list_counter.pop()

    def visit_definition_list(self, node: Element) -> None:
        self.list_counter.append(-2)

    def depart_definition_list(self, node: Element) -> None:
        self.list_counter.pop()

    def visit_list_item(self, node: Element) -> None:
        if self.list_counter[-1] == -1:
            # bullet list
            self.new_state(2)
        elif self.list_counter[-1] == -2:
            # definition list
            pass
        else:
            # enumerated list
            self.list_counter[-1] += 1
            self.new_state(len(str(self.list_counter[-1])) + 2)

    def depart_list_item(self, node: Element) -> None:
        if self.list_counter[-1] == -1:
            self.end_state(first='* ')
        elif self.list_counter[-1] == -2:
            pass
        else:
            self.end_state(first='%s. ' % self.list_counter[-1])

    def visit_definition_list_item(self, node: Element) -> None:
        self._classifier_count_in_li = len(list(node.findall(nodes.classifier)))

    def depart_definition_list_item(self, node: Element) -> None:
        pass

    def visit_term(self, node: Element) -> None:
        self.new_state(0)

    def depart_term(self, node: Element) -> None:
        if not self._classifier_count_in_li:
            self.end_state(end=None)

    def visit_classifier(self, node: Element) -> None:
        self.add_text(' : ')

    def depart_classifier(self, node: Element) -> None:
        self._classifier_count_in_li -= 1
        if not self._classifier_count_in_li:
            self.end_state(end=None)

    def visit_definition(self, node: Element) -> None:
        self.new_state()

    def depart_definition(self, node: Element) -> None:
        self.end_state()

    def visit_field_list(self, node: Element) -> None:
        pass

    def depart_field_list(self, node: Element) -> None:
        pass

    def visit_field(self, node: Element) -> None:
        pass

    def depart_field(self, node: Element) -> None:
        pass

    def visit_field_name(self, node: Element) -> None:
        self.new_state(0)

    def depart_field_name(self, node: Element) -> None:
        self.add_text(':')
        self.end_state(end=None)

    def visit_field_body(self, node: Element) -> None:
        self.new_state()

    def depart_field_body(self, node: Element) -> None:
        self.end_state()

    def visit_centered(self, node: Element) -> None:
        pass

    def depart_centered(self, node: Element) -> None:
        pass

    def visit_hlist(self, node: Element) -> None:
        pass

    def depart_hlist(self, node: Element) -> None:
        pass

    def visit_hlistcol(self, node: Element) -> None:
        pass

    def depart_hlistcol(self, node: Element) -> None:
        pass

    def visit_admonition(self, node: Element) -> None:
        self.new_state(0)

    def depart_admonition(self, node: Element) -> None:
        self.end_state()

    def _visit_admonition(self, node: Element) -> None:
        self.new_state(2)

    def _depart_admonition(self, node: Element) -> None:
        label = admonitionlabels[node.tagname]
        indent = sum(self.stateindent) + len(label)
        if (
            len(self.states[-1]) == 1
            and self.states[-1][0][0] == 0
            and MAXWIDTH - indent >= sum(len(s) for s in self.states[-1][0][1])
        ):
            # short text: append text after admonition label
            self.stateindent[-1] += len(label)
            self.end_state(first=label + ': ')
        else:
            # long text: append label before the block
            self.states[-1].insert(0, (0, [self.nl]))
            self.end_state(first=label + ':')

    visit_attention = _visit_admonition
    depart_attention = _depart_admonition
    visit_caution = _visit_admonition
    depart_caution = _depart_admonition
    visit_danger = _visit_admonition
    depart_danger = _depart_admonition
    visit_error = _visit_admonition
    depart_error = _depart_admonition
    visit_hint = _visit_admonition
    depart_hint = _depart_admonition
    visit_important = _visit_admonition
    depart_important = _depart_admonition
    visit_note = _visit_admonition
    depart_note = _depart_admonition
    visit_tip = _visit_admonition
    depart_tip = _depart_admonition
    visit_warning = _visit_admonition
    depart_warning = _depart_admonition
    visit_seealso = _visit_admonition
    depart_seealso = _depart_admonition

    def visit_versionmodified(self, node: Element) -> None:
        self.new_state(0)

    def depart_versionmodified(self, node: Element) -> None:
        self.end_state()

    def visit_literal_block(self, node: Element) -> None:
        self.new_state()

    def depart_literal_block(self, node: Element) -> None:
        self.end_state(wrap=False)

    def visit_doctest_block(self, node: Element) -> None:
        self.new_state(0)

    def depart_doctest_block(self, node: Element) -> None:
        self.end_state(wrap=False)

    def visit_line_block(self, node: Element) -> None:
        self.new_state()
        self.lineblocklevel += 1

    def depart_line_block(self, node: Element) -> None:
        self.lineblocklevel -= 1
        self.end_state(wrap=False, end=None)
        if not self.lineblocklevel:
            self.add_text('\n')

    def visit_line(self, node: Element) -> None:
        pass

    def depart_line(self, node: Element) -> None:
        self.add_text('\n')

    def visit_block_quote(self, node: Element) -> None:
        self.new_state()

    def depart_block_quote(self, node: Element) -> None:
        self.end_state()

    def visit_compact_paragraph(self, node: Element) -> None:
        pass

    def depart_compact_paragraph(self, node: Element) -> None:
        pass

    def visit_paragraph(self, node: Element) -> None:
        if (
            not isinstance(node.parent, nodes.Admonition)
            or isinstance(node.parent, addnodes.seealso)
        ):  # fmt: skip
            self.new_state(0)

    def depart_paragraph(self, node: Element) -> None:
        if (
            not isinstance(node.parent, nodes.Admonition)
            or isinstance(node.parent, addnodes.seealso)
        ):  # fmt: skip
            self.end_state()

    def visit_target(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_index(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_toctree(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_substitution_definition(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_pending_xref(self, node: Element) -> None:
        pass

    def depart_pending_xref(self, node: Element) -> None:
        pass

    def visit_reference(self, node: Element) -> None:
        if self.add_secnumbers:
            numbers = node.get('secnumber')
            if numbers is not None:
                self.add_text('.'.join(map(str, numbers)) + self.secnumber_suffix)

    def depart_reference(self, node: Element) -> None:
        pass

    def visit_number_reference(self, node: Element) -> None:
        text = nodes.Text(node.get('title', '#'))
        self.visit_Text(text)
        raise nodes.SkipNode

    def visit_download_reference(self, node: Element) -> None:
        pass

    def depart_download_reference(self, node: Element) -> None:
        pass

    def visit_emphasis(self, node: Element) -> None:
        self.add_text('*')

    def depart_emphasis(self, node: Element) -> None:
        self.add_text('*')

    def visit_literal_emphasis(self, node: Element) -> None:
        self.add_text('*')

    def depart_literal_emphasis(self, node: Element) -> None:
        self.add_text('*')

    def visit_strong(self, node: Element) -> None:
        self.add_text('**')

    def depart_strong(self, node: Element) -> None:
        self.add_text('**')

    def visit_literal_strong(self, node: Element) -> None:
        if self.in_production_list:
            return
        self.add_text('**')

    def depart_literal_strong(self, node: Element) -> None:
        if self.in_production_list:
            return
        self.add_text('**')

    def visit_abbreviation(self, node: Element) -> None:
        self.add_text('')

    def depart_abbreviation(self, node: Element) -> None:
        if explanation := node.get('explanation', ''):
            self.add_text(f' ({explanation})')

    def visit_manpage(self, node: Element) -> None:
        return self.visit_literal_emphasis(node)

    def depart_manpage(self, node: Element) -> None:
        return self.depart_literal_emphasis(node)

    def visit_title_reference(self, node: Element) -> None:
        self.add_text('*')

    def depart_title_reference(self, node: Element) -> None:
        self.add_text('*')

    def visit_literal(self, node: Element) -> None:
        if self.in_production_list:
            return
        self.add_text('"')

    def depart_literal(self, node: Element) -> None:
        if self.in_production_list:
            return
        self.add_text('"')

    def visit_subscript(self, node: Element) -> None:
        self.add_text('_')

    def depart_subscript(self, node: Element) -> None:
        pass

    def visit_superscript(self, node: Element) -> None:
        self.add_text('^')

    def depart_superscript(self, node: Element) -> None:
        pass

    def visit_footnote_reference(self, node: Element) -> None:
        self.add_text('[%s]' % node.astext())
        raise nodes.SkipNode

    def visit_citation_reference(self, node: Element) -> None:
        self.add_text('[%s]' % node.astext())
        raise nodes.SkipNode

    def visit_Text(self, node: Text) -> None:
        self.add_text(node.astext())

    def depart_Text(self, node: Text) -> None:
        pass

    def visit_generated(self, node: Element) -> None:
        pass

    def depart_generated(self, node: Element) -> None:
        pass

    def visit_inline(self, node: Element) -> None:
        if 'xref' in node['classes'] or 'term' in node['classes']:
            self.add_text('*')

    def depart_inline(self, node: Element) -> None:
        if 'xref' in node['classes'] or 'term' in node['classes']:
            self.add_text('*')

    def visit_container(self, node: Element) -> None:
        pass

    def depart_container(self, node: Element) -> None:
        pass

    def visit_problematic(self, node: Element) -> None:
        self.add_text('>>')

    def depart_problematic(self, node: Element) -> None:
        self.add_text('<<')

    def visit_system_message(self, node: Element) -> None:
        self.new_state(0)
        self.add_text('<SYSTEM MESSAGE: %s>' % node.astext())
        self.end_state()
        raise nodes.SkipNode

    def visit_comment(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_meta(self, node: Element) -> None:
        # only valid for HTML
        raise nodes.SkipNode

    def visit_raw(self, node: Element) -> None:
        if 'text' in node.get('format', '').split():
            self.new_state(0)
            self.add_text(node.astext())
            self.end_state(wrap=False)
        raise nodes.SkipNode

    def visit_math(self, node: nodes.math) -> None:
        pass

    def depart_math(self, node: nodes.math) -> None:
        pass

    def visit_math_block(self, node: nodes.math_block) -> None:
        self.new_state()

    def depart_math_block(self, node: nodes.math_block) -> None:
        self.end_state()
