"""Table formatter base — shared logic for ASCII, Markdown, LaTeX, etc.

Provides multi-column layout, auto-width, truncation, pagination,
and categorized tree rendering.  Subclasses override ``_skin()``,
``_hline()``, ``_header()``, ``_wrap()``, ``_cat_prefix()``,
``_cat_indent()``, and ``_clean()`` to produce format-specific output.
"""

import math
from .base import BaseFormatter
from ..helpers import flattenDict, cleanStr, chars


class TableFormatter(BaseFormatter):
    """Base class for tabular formatters (ASCII, Markdown, LaTeX, …)

    Handles multi-column layout, row building, column headers,
    horizontal rules, pagination, and categorized tree output.

    Subclasses only need to define ``_skin()`` and optionally override
    ``_hline()``, ``_header()``, ``_wrap()``, ``_cat_prefix()``, and
    ``_clean()``.

    Skin dict keys::

        sep      – field separator within a column group
        sepCol   – separator between column groups
        start    – prefix for every row
        lstart   – prefix for the last row (defaults to *start*)
        end      – suffix for every row
        blank    – placeholder for missing values
        width    – minimum character width per field
        nCols    – number of column groups (default: 1)
        page     – rows per page (0 = no pagination)
    """

    # ── Skin (override in subclass) ──────────────────────────────────

    def _skin(self, **kwargs):
        """Return a dict of format-specific defaults.

        Called once at the start of ``format()`` / ``format_categorized()``.
        *kwargs* are the caller-supplied overrides; the returned dict is
        merged (caller wins).
        """
        return {
            'sep': '   ',
            'sepCol': '|',
            'start': '',
            'lstart': None,       # None → same as start
            'end': '',
            'blank': ' --- ',
            'width': 'auto',
            'nCols': 1,
            'page': 0,
            'maxwidth': 0,
        }

    def _clean(self, value):
        """Escape / sanitise a cell value for this format."""
        return cleanStr(value)

    def _hline(self, keys, fmt, pos='mid'):
        """Return a horizontal rule string.

        Parameters:
            keys: Column keys
            fmt: Resolved skin dict
            pos (str): Position hint — ``'top'``, ``'mid'``, or ``'bot'``.
                Subclasses can use this to pick junction characters.

        Override in subclass.
        """
        return ''

    def _header(self, keys, titles, fmt):
        """Return a header row string.

        *titles* maps key → display name.  Override in subclass.
        """
        return ''

    def _wrap(self, body, keys, fmt):
        """Wrap the finished body if needed."""
        return body

    def _cat_prefix(self, label, level, depth, is_last, fmt, is_first=False):
        """Return the prefix string for a category heading.

        Parameters:
            label (str): Category label (already title-formatted)
            level (int): 0-based nesting level
            depth (int): Total number of category levels
            is_last (bool): Whether this is the last sibling
            fmt (dict): Resolved skin dict
            is_first (bool): Whether this is the first sibling

        Override in subclass for tree graphics.
        """
        return label

    # ── Core table builder ───────────────────────────────────────────

    def _resolve_fmt(self, **kwargs):
        """Merge skin defaults with caller overrides."""
        fmt = self._skin(**kwargs)
        fmt.update({k: v for k, v in kwargs.items() if v is not None})
        if fmt.get('lstart') is None:
            fmt['lstart'] = fmt['start']
        return fmt

    def _build_row_line(self, item, keys, fmtStr, fmt):
        """Build a single row's column-group parts list."""
        sep = fmt['sep']
        blank = fmt['blank']

        if not keys:
            return [str(item)]

        cells = []
        fitem = flattenDict(item)
        for k in keys:
            if k in fitem:
                cells.append(fmtStr.format(self._clean(fitem[k])))
            else:
                cells.append(fmtStr.format(blank))
        return [sep.join(cells)]

    def _auto_widths(self, dlist, keys, titles, fmt):
        """Compute per-key column widths from data and titles."""
        maxwidth = fmt.get('maxwidth', 0)
        widths = {}
        for k in keys:
            # Start with the title width
            w = len(titles.get(k, k))
            for item in dlist.l:
                fitem = flattenDict(item)
                if k in fitem:
                    w = max(w, len(self._clean(fitem[k])))
            if maxwidth and w > maxwidth:
                w = maxwidth
            widths[k] = w
        return widths

    def _build_rows(self, dlist, keys, titles, fmt):
        """Build data rows as a list of strings (no header/hline).

        Items are laid out row-major: left to right, then next row.
        This means sequential items appear on the same row across
        columns, which keeps pagination order natural.
        """
        nCols = fmt['nCols']
        sep = fmt['sep']
        sepCol = fmt['sepCol']
        start = fmt['start']
        lstart = fmt['lstart']
        end = fmt['end']
        blank = fmt['blank']
        width = fmt['width']

        # Resolve per-key widths
        if width == 'auto':
            col_widths = self._auto_widths(dlist, keys, titles, fmt)
            # Store resolved widths so _header/_hline can use them
            fmt['_col_widths'] = col_widths
        else:
            col_widths = {k: width for k in keys}

        def _fmts(k):
            return f'{{:{col_widths.get(k, 1)}s}}'

        def _trunc(s, k):
            w = col_widths.get(k, 1)
            if len(s) > w:
                return s[:w - 1] + '\u2026'
            return s

        if not keys:
            nCols = 1

        nRows = math.ceil(len(dlist.l) / nCols) if dlist.l else 0

        lines = []
        for irow in range(nRows):
            is_last = (irow == nRows - 1)
            prefix = lstart if is_last else start
            parts = []

            for icol in range(nCols):
                idx = irow * nCols + icol
                try:
                    item = dlist.l[idx]
                except IndexError:
                    item = {}

                if not keys:
                    parts.append(str(item))
                    break

                cells = []
                fitem = flattenDict(item)
                for k in keys:
                    if k in fitem:
                        cells.append(_fmts(k).format(_trunc(self._clean(fitem[k]), k)))
                    else:
                        cells.append(_fmts(k).format(blank))
                parts.append(sep.join(cells))

            lines.append(prefix + sepCol.join(parts) + end)

        return lines

    def _paginate(self, row_lines, keys, titles, fmt):
        """Assemble rows into pages with headers and hlines.

        Returns a single string.
        """
        header = self._header(keys, titles, fmt)
        page = fmt.get('page', 0)

        def _block(chunk):
            parts = []
            hl_top = self._hline(keys, fmt, pos='top')
            hl_mid = self._hline(keys, fmt, pos='mid')
            hl_bot = self._hline(keys, fmt, pos='bot')
            if hl_top:
                parts.append(hl_top)
            if header:
                parts.append(header)
                if hl_mid:
                    parts.append(hl_mid)
            parts.extend(chunk)
            if hl_bot:
                parts.append(hl_bot)
            return '\n'.join(parts)

        blocks = []
        if not page or page <= 0:
            blocks.append(_block(row_lines))
        else:
            for i in range(0, len(row_lines), page):
                blocks.append(_block(row_lines[i:i + page]))

        return '\n'.join(blocks)

    # ── format() ─────────────────────────────────────────────────────

    def format(self, dlist, keys=[], titles={}, **kwargs):
        """Format Dlist as a table

        Parameters:
            dlist: Dlist object
            keys (list): Columns to display
            titles (dict): ``{key: display_name}`` for column headers
            **kwargs: Skin overrides (see class docstring)

        Returns:
            str: Formatted table string
        """
        titles = dict(titles)
        for k in keys:
            if k not in titles:
                titles[k] = k

        fmt = self._resolve_fmt(**kwargs)
        row_lines = self._build_rows(dlist, keys, titles, fmt)
        body = self._paginate(row_lines, keys, titles, fmt)
        return self._wrap(body, keys, fmt)

    # ── Categorized output ───────────────────────────────────────────

    def format_categorized(self, dlist, ctree, keys, titles={}, **kwargs):
        """Format with categories as a nested table

        Parameters:
            dlist: Dlist object
            ctree (list): Category keys (outermost first)
            keys (list): Data columns
            titles (dict): ``{key: format_string_or_name}`` for labels.
                For category keys the value is a format string with
                ``{}`` placeholder (e.g. ``'Type: {}'``).
                For data keys the value is the column header name.
            **kwargs: Skin overrides

        Returns:
            str: Categorized table string
        """
        titles = dict(titles)
        for k in ctree:
            if k not in titles:
                titles[k] = '{}'
        for k in keys:
            if k not in titles:
                titles[k] = k

        fmt = self._resolve_fmt(**kwargs)

        tree = self._build_tree(dlist, ctree, keys, titles, fmt)
        body = self._render_tree(tree, len(ctree), len(ctree),
                                 keys, titles, fmt)
        return self._wrap_categorized(body, keys, fmt)

    def _build_tree(self, dlist, ctree, keys, titles, fmt):
        """Recursively partition dlist into [(label, subtree|rows), …]"""
        out = []
        parts = dlist.partition(ctree[0])       # single-pass split
        remainder = parts.pop(None, None)       # items missing this key

        for c, sub in parts.items():
            label = titles[ctree[0]].format(self._clean(c))

            if len(ctree) > 1:
                child = self._build_tree(sub, ctree[1:], keys, titles, fmt)
            else:
                row_lines = self._build_rows(sub, keys, titles, fmt)
                child = self._paginate(row_lines, keys, titles, fmt)

            out.append((label, child))

        # Items missing this category key → appear first
        if remainder and remainder.l:
            # If ALL items lack this key, use the title as a heading and
            # recurse into deeper ctree levels (supports "void" grouping)
            if not parts and len(ctree) > 1:
                label = titles[ctree[0]].format('').strip()
                child = self._build_tree(remainder, ctree[1:], keys,
                                         titles, fmt)
                out.insert(0, (label, child))
            elif not parts:
                label = titles[ctree[0]].format('').strip()
                row_lines = self._build_rows(remainder, keys, titles, fmt)
                child = self._paginate(row_lines, keys, titles, fmt)
                out.insert(0, (label, child))
            else:
                row_lines = self._build_rows(remainder, keys, titles, fmt)
                child = self._paginate(row_lines, keys, titles, fmt)
                out.insert(0, (None, child))

        return out

    def _render_tree(self, tree, depth, total_depth, keys, titles, fmt,
                     level=0):
        """Render the tree into a string with tree graphics."""
        parts = []
        for idx, (label, child) in enumerate(tree):
            is_last = (idx == len(tree) - 1)

            # Items missing a category key → render table directly
            if label is None:
                if isinstance(child, list):
                    parts.append(
                        self._render_tree(child, depth, total_depth,
                                          keys, titles, fmt, level)
                    )
                else:
                    indent = self._cat_indent(level, total_depth, is_last, fmt)
                    if indent:
                        indented = '\n'.join(
                            indent + line for line in child.split('\n')
                        )
                        parts.append(indented)
                    else:
                        parts.append(child)
                continue

            prefix = self._cat_prefix(label, level, total_depth,
                                      is_last, fmt,
                                      is_first=(idx == 0))
            parts.append(prefix)

            if isinstance(child, list):
                parts.append(
                    self._render_tree(child, depth, total_depth,
                                      keys, titles, fmt, level + 1)
                )
            else:
                # Indent table lines under the tree branch
                indent = self._cat_indent(level, total_depth, is_last, fmt)
                if indent:
                    indented = '\n'.join(
                        indent + line for line in child.split('\n')
                    )
                    parts.append(indented)
                else:
                    parts.append(child)
        return '\n'.join(parts)

    def _cat_indent(self, level, total_depth, is_last, fmt):
        """Return the indent prefix for table lines under a category.

        Override in subclass for tree continuation lines.
        """
        return ''

    def _wrap_categorized(self, body, keys, fmt):
        """Wrap categorized output (override for LaTeX, etc.)."""
        return body
