"""THOR house plotting style for documentation figures. [AI-Claude]

A single, consistent, minimal/slide look for plots that go into the docs.
Light by default (matches the mkdocs-material indigo theme); pass ``dark=True``
for the dark variant. Exports are transparent by default to follow the page
theme. Two colour strategies: a fixed per-code map (``code_color``) for neutral comparisons, and
a "hero" treatment (``hero_colors``) that gives one series a bold accent and
mutes the rest — the THOR-centric look for docs/slides.

Usage
-----
    import thorplot as tp

    with tp.style():                       # or tp.style(dark=True)
        fig, ax = plt.subplots(figsize=tp.FIGSIZE_WIDE)
        ax.plot(x, y, color=tp.code_color("thor"))
        tp.strip_axes(ax)                  # remove chartjunk spines/ticks
        tp.legend_top(ax)
    tp.save_doc_figure(fig, "my_figure")   # -> docs/assets/images/my_figure.svg

For bar/summary plots use ``tp.hero_bar(...)`` (horizontal bars with in-color
value labels; optionally highlight one series).

Design: no top/right spines, left-aligned bold titles, a subtle y-grid only,
generous whitespace, frameless legends, and a fixed per-code colour map so the
same code is always the same colour across figures.
"""

from contextlib import contextmanager
from pathlib import Path

import matplotlib as mpl
import numpy as np

# --- canonical palette (indigo-anchored, matches mkdocs-material) -------------
PALETTE = {
    "indigo": "#3949ab", "teal": "#00897b", "orange": "#fb8c00",
    "purple": "#8e24aa", "cyan": "#00acc1", "red": "#c0392b",
    "gray": "#9aa0a6", "ink": "#1f2330",
}
# General categorical cycle for arbitrary series.
CYCLE = ["#3949ab", "#00897b", "#fb8c00", "#8e24aa", "#00acc1", "#c0392b", "#5d4037"]

# Fixed per-code colours so a code is always the same colour across figures.
CODE_COLORS = {
    "thor": "#3949ab", "colt": "#00897b", "rascas": "#fb8c00",
    "iltis": "#8e24aa", "analytic": "#9aa0a6",
}
CODE_COLORS_DARK = {
    "thor": "#82b1ff", "colt": "#4dd0a0", "rascas": "#ffb74d",
    "iltis": "#ce93d8", "analytic": "#6b6b85",
}

# Zensical modern slate: hsl(225, 15%, 5%). Used for display/opaque exports;
# documentation exports use the page background through transparency. [AI-Codex]
_DARK_BG, _DARK_FG, _DARK_EDGE = "#0b0c0f", "#e8e8f0", "#44465e"

# --- hero treatment: one bold accent, peers muted (the "minimal" look) --------
HERO_ACCENT = "#5b6cf0"           # THOR (or whichever series is the subject)
HERO_ACCENT_DARK = "#82b1ff"
# muted ramp for the non-hero series (distinguishable but recede)
_MUTED = ["#9aa0c4", "#7e84a3", "#565b78", "#3a3e54"]
_MUTED_DARK = ["#8a90b8", "#6f7496", "#565b78", "#caa9dc"]


def hero_colors(labels, hero="thor", dark=False):
    """Colours for a 'hero' plot: the ``hero`` series gets the bold accent, the
    rest a muted ramp. Use for THOR-centric comparison/summary figures."""
    accent = HERO_ACCENT_DARK if dark else HERO_ACCENT
    muted = _MUTED_DARK if dark else _MUTED
    out, i = [], 0
    for lab in labels:
        if str(lab).lower() == str(hero).lower():
            out.append(accent)
        else:
            out.append(muted[i % len(muted)]); i += 1
    return out

FIGSIZE_WIDE = (9.0, 4.2)     # single wide panel (slide-friendly)
FIGSIZE_DUO = (13.0, 4.6)     # two panels side by side
FIGSIZE_SQUARE = (5.4, 4.6)


def code_color(code, dark=False):
    """Canonical colour for a code name (falls back to gray)."""
    table = CODE_COLORS_DARK if dark else CODE_COLORS
    return table.get(str(code).lower(), PALETTE["gray"])


def _rc(dark):
    # Minimal "editorial/slide" look: bold left titles, no chartjunk, no grid by
    # default, slightly larger type. Opt into a subtle grid per-axes if needed.
    rc = {
        # Title hierarchy: figure title (suptitle_left, 17) > axes title (14)
        # > axes labels (12.5).
        "font.size": 13, "axes.titlesize": 14, "axes.titleweight": "bold",
        "axes.titlelocation": "left", "axes.titlepad": 6, "axes.labelsize": 12.5,
        "axes.spines.top": False, "axes.spines.right": False,
        "axes.grid": False, "axes.axisbelow": True,
        "grid.alpha": 0.18, "grid.linewidth": 0.7,
        "legend.frameon": False, "legend.fontsize": 11.5,
        "figure.dpi": 130, "savefig.dpi": 140, "savefig.bbox": "tight",
        "savefig.transparent": True,  # Also covers direct fig.savefig calls. [AI-Codex]
        "lines.linewidth": 2.6, "lines.solid_capstyle": "round",
        "axes.prop_cycle": mpl.cycler(color=CYCLE),
        "xtick.direction": "out", "ytick.direction": "out",
    }
    if dark:
        rc.update({
            "figure.facecolor": _DARK_BG, "axes.facecolor": _DARK_BG,
            "savefig.facecolor": _DARK_BG, "text.color": _DARK_FG,
            "axes.labelcolor": _DARK_FG, "axes.titlecolor": _DARK_FG,
            "xtick.color": _DARK_FG, "ytick.color": _DARK_FG,
            "axes.edgecolor": _DARK_EDGE, "grid.color": _DARK_FG,
            "axes.prop_cycle": mpl.cycler(color=list(CODE_COLORS_DARK.values())),
        })
    return rc


@contextmanager
def style(dark=False):
    """Context manager applying the THOR house style."""
    with mpl.rc_context(_rc(dark)):
        yield


def use(dark=False):
    """Apply the style globally (for scripts that don't use the context manager)."""
    mpl.rcParams.update(_rc(dark))


def strip_axes(ax, bottom=True, left=False):
    """Remove chartjunk: hide top/right (always) and optionally left spine + yticks."""
    for s in ("top", "right"):
        ax.spines[s].set_visible(False)
    ax.spines["left"].set_visible(left)
    ax.spines["bottom"].set_visible(bottom)
    if not left:
        ax.set_yticks([])


def legend_top(ax, ncol=None, **kw):
    """Frameless legend centred above the axes (slide-style)."""
    handles, labels = ax.get_legend_handles_labels()
    ncol = ncol or len(labels)
    ax.legend(handles, labels, loc="upper center", ncol=ncol, frameon=False,
              columnspacing=1.1, handlelength=1.4, **kw)


def hero_bar(ax, labels, values, colors=None, fmt="{:.0f}", dark=False,
             highlight=None):
    """Horizontal bar chart with in-colour value labels (the 'slide' bar look).

    Pass ``highlight`` (a label) to bold that row. ``colors`` defaults to the
    per-code colour map.
    """
    fg = _DARK_FG if dark else PALETTE["ink"]
    if colors is None:
        colors = [code_color(lbl, dark) for lbl in labels]
    y = np.arange(len(labels))[::-1]
    vmax = max(values) if len(values) else 1.0
    ax.barh(y, values, color=colors, height=0.62, zorder=3)
    for yi, lab, v, col in zip(y, labels, values, colors):
        ax.text(v + vmax * 0.02, yi, fmt.format(v), va="center", ha="left",
                fontsize=13, fontweight="bold", color=col)
        bold = (highlight is not None and str(lab).lower() == str(highlight).lower())
        ax.text(-vmax * 0.02, yi, lab, va="center", ha="right", fontsize=13,
                fontweight="bold" if bold else "normal", color=fg)
    ax.set_xlim(0, vmax * 1.18)
    ax.set_yticks([])
    ax.set_xticks([])
    for s in ("top", "right", "left", "bottom"):
        ax.spines[s].set_visible(False)


# --- map figures (imshow panels) ----------------------------------------- [AI-Claude]
# House conventions for 2D coordinate maps: panels are pure image (no ticks,
# no labels, no frame), a scale bar replaces the axes, and colorbars are
# frameless with markless ticks. See docs/development.md "Figure style".


def imshow_panel(ax, title=None):
    """Strip an imshow axes down to the image: no ticks, labels, or frame."""
    ax.set_xticks([])
    ax.set_yticks([])
    for s in ax.spines.values():
        s.set_visible(False)
    if title is not None:
        ax.set_title(title, loc="left")


def scalebar(ax, length, label, color="white", frac_x=0.06, frac_y=0.07):
    """Scale bar (data units) in the lower-left corner of an imshow panel.

    One bar per figure is enough when panels share the scale. White works on
    most dark-toned colormaps; pass ``color`` for bright maps.
    """
    x0, x1 = ax.get_xlim()
    y0, y1 = ax.get_ylim()
    bx = x0 + (x1 - x0) * frac_x
    by = y0 + (y1 - y0) * frac_y
    ax.plot([bx, bx + length], [by, by], color=color, lw=3.5,
            solid_capstyle="butt")
    ax.text(bx + length / 2, by + (y1 - y0) * 0.018, label, color=color,
            ha="center", va="bottom", fontsize=12, fontweight="bold")


def colorbar_minimal(fig, mappable, ax, label=None, **kw):
    """Frameless colorbar with markless ticks (labels only).

    A tick mark protruding from a frame that isn't there reads as leftover
    chrome — with the outline hidden, only the tick labels remain.
    """
    kw.setdefault("shrink", 0.8)
    kw.setdefault("pad", 0.015)
    kw.setdefault("aspect", 30)
    cbar = fig.colorbar(mappable, ax=ax, **kw)
    if label is not None:
        cbar.set_label(label)
    cbar.outline.set_visible(False)
    cbar.minorticks_off()
    cbar.ax.tick_params(size=0)
    return cbar


def suptitle_left(fig, text, dark=False, **kw):
    """Left-aligned bold figure title (the house title placement)."""
    kw.setdefault("fontsize", 17)
    fig.suptitle(text, x=0.02, ha="left", fontweight="bold",
                 color=_DARK_FG if dark else PALETTE["ink"], **kw)


# docs/assets/images relative to this file (scripts/ -> repo root -> docs/...)
DOC_ASSETS = Path(__file__).resolve().parents[1] / "docs" / "assets" / "images"

# Reference width (inches) of a full-column docs figure. The docs page
# stretches every image to the content-column width, so a narrower figure
# would show proportionally larger fonts. Author full-width figures at this
# width; embed narrower ones at width = fig_width / DOC_FULL_WIDTH of the
# column (save_doc_figure prints the attribute to use).
DOC_FULL_WIDTH = 12.2


def save_doc_figure(fig, name, fmt="svg", also_pdf=False, *, transparent=True, output_dir=None, dpi=None):
    """Save a figure into docs/assets/images/<name>.<fmt>.

    ``output_dir`` overrides the docs asset directory; ``dpi`` controls raster
    resolution. Pass ``transparent=False`` for an opaque standalone export. [AI-Codex]

    SVG is the default for line/bar docs figures: vector-crisp at any zoom,
    smaller than PNG for line plots, and not blocked by .gitignore (so it
    commits without `-f` and diffs as text). Use ``fmt="png"`` for raster-heavy
    figures (volume renders, dense scatter) where SVG would balloon.
    """
    destination = DOC_ASSETS if output_dir is None else Path(output_dir)
    destination.mkdir(parents=True, exist_ok=True)
    out = destination / f"{name}.{fmt}"
    # Transparent background so the figure blends with the docs page in BOTH
    # light and dark themes (the #only-light/#only-dark image split assumes the
    # figure adapts to the page background, not a fixed slab colour).
    fig.savefig(out, transparent=transparent, dpi=dpi)
    if also_pdf:
        fig.savefig(destination / f"{name}.pdf", transparent=transparent, dpi=dpi)
    pct = min(100, round(100 * fig.get_size_inches()[0] / DOC_FULL_WIDTH))
    if pct < 100:
        print(f'{name}.{fmt}: embed with {{ width="{pct}%" }} '
              f"(figure is narrower than the {DOC_FULL_WIDTH}\" full-width reference)")
    return out
