"""Style and shared helpers for the short aggregate introductions in this folder.

The house kit's ``house`` module ships inside the installed extension. This
module puts that directory on ``sys.path``, derived from its own location, and
re-exports the kit, as the house note template does: ``figures`` and
``figure`` for matplotlib, ``show`` for a DecL program. What is local is
``GT`` with this folder's defaults, ``wprint``, ``quiet`` and ``refuse``.

The website offers this file for download, so that a reader can run a page's
source. Without the kit beside it, ``GT`` is plain ``greater_tables.GT``,
``figure`` is ``plt.subplots`` and ``figures`` does nothing.

Import with ``from style import wprint, GT, refuse, figures``. A Jupyter kernel
started from a document here has this folder as its working directory, so the
import resolves without a path.
"""

import logging
import re
import sys
import tempfile
from contextlib import contextmanager
from functools import partial
from pathlib import Path
from textwrap import wrap

_KIT = Path(__file__).resolve().parent / '_extensions' / 'house'

if _KIT.exists():
    sys.path.insert(0, str(_KIT))
    import house
    from house import *  # noqa: F403
    _GT, _kit_names = house.GT, house.__all__
else:
    from greater_tables import GT as _GT
    _kit_names = ['figure', 'figures']

    def figures():
        """Leave matplotlib's style as it is: the house style is not installed."""

    def figure(width=None, rows=1, cols=1, height=None, **kwargs):
        """``plt.subplots(rows, cols)``: the house sizes are not installed."""
        import matplotlib.pyplot as plt
        return plt.subplots(rows, cols, **kwargs)

__all__ = [*_kit_names, 'GT', 'quiet', 'refuse', 'wprint']

#: GT with the defaults the hacks notebooks use: no column breaks inside a
#: cell, four decimals. Override per call, for example ``float_format=',.0f'``.
GT = partial(_GT, unbreakable='.*', float_format='.4f')

# a label, then two or more spaces, then the value: an aligned key/value line
_KEY_VALUE = re.compile(r'^(\s*\S.*?\s{2,})\S')

# The system temporary folder, in both slash forms. Its path includes the
# user's name, and the pages publish their output, so wprint shows it as <tmp>.
_TMP = sorted({tempfile.gettempdir(), Path(tempfile.gettempdir()).as_posix()},
              key=len, reverse=True)


def wprint(*args, width=80, sep=' ', hang=True):
    """Print like ``print``, wrapping each line at ``width`` characters.

    Parameters
    ----------
    *args
        Objects to print; each is converted with ``str`` and joined by ``sep``.
    width : int
        Wrap width in characters.
    sep : str
        Separator between ``args``.
    hang : bool
        Indent the continuation of an aligned ``key   value`` line to the
        value column, so a description keeps its two columns.

    Notes
    -----
    Existing line breaks are kept and each line is wrapped on its own, so a
    multi-line description keeps its layout and only its long lines fold. A
    continuation line takes the leading indent of the line it continues, or
    the value column of a key/value line when ``hang`` is set and that column
    sits in the left half of the width.

    The system temporary folder is shown as ``<tmp>``: pages that work in a
    temporary directory print file provenance and error messages containing
    its path, which includes the user's name, and the pages are published.
    """
    text = sep.join(str(a) for a in args)
    for tmp in _TMP:
        text = text.replace(tmp, '<tmp>')
    out = []
    for line in text.splitlines() or ['']:
        indent = line[:len(line) - len(line.lstrip())]
        m = _KEY_VALUE.match(line) if hang else None
        if m and len(m.group(1)) < width // 2:
            indent = ' ' * len(m.group(1))
        out.extend(wrap(line, width, subsequent_indent=indent,
                        break_on_hyphens=False) or [''])
    print('\n'.join(out))


@contextmanager
def quiet(name='aggregate', level=logging.CRITICAL):
    """Raise the ``aggregate`` logger's level for the body of a ``with``.

    The ``Underwriter`` logs a parse or lookup failure at ERROR before it
    raises, so a caught refusal otherwise prints twice, the logged copy
    unwrapped. Use it only around code expected to fail.
    """
    logger = logging.getLogger(name)
    old = logger.level
    logger.setLevel(level)
    try:
        yield
    finally:
        logger.setLevel(old)


def refuse(*items, build=None, errors=Exception, kind=False):
    """Run each item, expecting a refusal, and print the message wrapped.

    Parameters
    ----------
    *items : str or callable
        A string is a DecL program, passed to ``build``; a callable (a
        ``lambda`` around a method call) is called with no arguments.
    build : callable, optional
        The builder; defaults to ``aggregate.build``. Pass an ``Underwriter``
        or a fork to refuse against its recipe base.
    errors : exception class or tuple
        What counts as a refusal; anything else propagates.
    kind : bool
        Prefix each message with the exception's class name.

    Notes
    -----
    A ``KeyError`` message is printed from ``args[0]``, since ``str`` of a
    ``KeyError`` adds quotes. An item that succeeds is reported as such, so a
    refusal that stops refusing shows in the output instead of passing quietly.
    """
    if build is None:
        from aggregate import build
    with quiet():
        for item in items:
            try:
                item() if callable(item) else build(item)
            except errors as e:
                msg = e.args[0] if isinstance(e, KeyError) and e.args else e
                wprint(f'{type(e).__name__}: {msg}' if kind else msg)
            else:
                wprint(f'not refused: {item if isinstance(item, str) else "call"}')
