"""Shared look for the printed-instruction sheets."""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Circle, Polygon as MPoly
from matplotlib.collections import PolyCollection
from scipy.ndimage import gaussian_filter
import isodraw as iso

FIELD, INK   = '#E8B923', '#4A5257'
BG, PANEL    = '#26313A', '#2C3841'
TXT, DIM     = '#F2F7FA', '#9FB4C0'
ACC, OK      = '#37B6E9', '#5FD08A'
AZ, EL       = -20, 56


def background(fig, seed=3):
    """Dark slate with a little tooth to it, so the panels do not read as flat."""
    rng = np.random.default_rng(seed)
    n = gaussian_filter(rng.normal(0, 1, (240, 620)), 2.2)
    n = (n - n.min()) / (np.ptp(n) + 1e-9)
    yy = np.linspace(-1, 1, 240)[:, None]
    vig = 1 - 0.22 * (yy ** 2)
    base = np.array([0.149, 0.192, 0.227])
    img = base[None, None, :] * (0.88 + 0.24 * n[..., None]) * vig[..., None]
    ax = fig.add_axes([0, 0, 1, 1], zorder=-10)
    ax.imshow(np.clip(img, 0, 1), aspect='auto', interpolation='bilinear')
    ax.axis('off')
    return ax


def shadow(ax, mesh, offset=(0, 0, 0), az=AZ, el=EL, spread=1.6, drop=(0.9, -1.6)):
    """Soft contact shadow from the part's projected silhouette."""
    from shapely.geometry import Polygon as SPoly
    from shapely.ops import unary_union
    tris = mesh.triangles + np.asarray(offset, float)
    xy, _ = iso.project(tris.reshape(-1, 3), az, el)
    xy = xy.reshape(-1, 3, 2)
    polys = [SPoly(t) for t in xy[::3]]
    sil = unary_union([p.buffer(0) for p in polys if p.is_valid and p.area > 1e-6])
    for k, a in ((spread * 2.2, 0.05), (spread * 1.3, 0.07), (spread * 0.6, 0.09), (0.0, 0.11)):
        g = sil.buffer(k)
        for gg in (g.geoms if hasattr(g, 'geoms') else [g]):
            ax.add_patch(MPoly(np.array(gg.exterior.coords) + np.array(drop),
                               closed=True, facecolor='#0A1116', edgecolor='none',
                               alpha=a, zorder=0))


class Scene:
    """Collects every solid in a panel so they can be rasterised in ONE pass.

    Depth has to be resolved across parts, not just within a part -- an insert
    sitting in a tray interleaves with it.
    """

    def __init__(self):
        self.items = []
        self.shadows = []

    def add(self, meshes, offset=(0, 0, 0), shade=True):
        for m, c in meshes:
            self.items.append((m, c, offset))
        if shade:
            self.shadows.append((meshes[0][0], offset))
        return self


EXT = (-33.0, 33.0, -27.05, 27.05)


def render(ax, scene, extent=EXT, px=1000, az=AZ, el=EL, z=2):
    for m, off in scene.shadows:
        shadow(ax, m, off, az, el)
    im = iso.raster(scene.items, extent, px=px, azim=az, elev=el)
    ax.imshow(im, extent=extent, interpolation='bilinear', zorder=z)
    ax.set_xlim(extent[0], extent[1])
    ax.set_ylim(extent[2], extent[3])


def part(ax, meshes, offset=(0, 0, 0), az=AZ, el=EL, z=2, shade=True):
    render(ax, Scene().add(meshes, offset, shade), az=az, el=el, z=z)


def arrow(ax, p0, p1, lw=5.0, col=ACC, z=9, scale=30):
    ax.add_patch(FancyArrowPatch(p0, p1, arrowstyle='-|>', mutation_scale=scale,
                                 lw=lw, color=col, zorder=z,
                                 shrinkA=0, shrinkB=0, capstyle='round'))


def sparks(ax, x, y, r=2.4, col=ACC, z=9):
    for a in (25, 90, 155, 215, 305):
        t = np.radians(a)
        ax.plot([x + r * 0.45 * np.cos(t), x + r * np.cos(t)],
                [y + r * 0.45 * np.sin(t), y + r * np.sin(t)],
                color=col, lw=2.4, solid_capstyle='round', zorder=z)


def tick(ax, x, y, r=4.2, z=10):
    ax.add_patch(Circle((x, y), r, facecolor='none', edgecolor=OK, lw=2.6, zorder=z))
    ax.plot([x - r * .42, x - r * .08, x + r * .46],
            [y + r * .03, y - r * .34, y + r * .40],
            color=OK, lw=3.0, solid_capstyle='round', solid_joinstyle='round', zorder=z)


def cross(ax, x, y, r=3.2, col=ACC, z=9):
    ax.plot([x - r, x + r], [y, y], color=col, lw=3.4, solid_capstyle='round', zorder=z)
    ax.plot([x, x], [y - r, y + r], color=col, lw=3.4, solid_capstyle='round', zorder=z)


def keyring(ax, P, R=7.0, az=AZ, el=EL, lift=0.0, thick=None, corner=-1):
    """Split ring threaded through the corner hole."""
    import tagkit as tk
    from shapely.geometry import Point as _Pt
    th = np.linspace(0, 2 * np.pi, 400)
    t = P.height if thick is None else thick
    d = np.array([0.7071 * corner, -0.7071, 0.0])
    hole_c = np.array([corner * P.hole_off, -P.hole_off, lift + t * 0.5])
    ctr = hole_c + R * d
    ring = ctr + R * np.cos(th)[:, None] * (-d) + R * np.sin(th)[:, None] * np.array([0, 0, 1.])
    outline = tk.rrect(P.size, P.size, P.corner_r)
    over = np.array([outline.contains(_Pt(x, y)) for x, y in ring[:, :2]])
    hidden = over & (ring[:, 2] < lift + t)
    rxy, _ = iso.project(ring, az, el)
    for mask, z in ((hidden, 1), (~hidden, 9)):
        idx = np.where(mask)[0]
        if not len(idx):
            continue
        for seg in np.split(idx, np.where(np.diff(idx) > 1)[0] + 1):
            if len(seg) < 2:
                continue
            ax.plot(rxy[seg, 0], rxy[seg, 1], color='#D8E2E8', lw=4.6,
                    solid_capstyle='round', zorder=z)
            ax.plot(rxy[seg, 0], rxy[seg, 1], color='#7B8A93', lw=1.4,
                    solid_capstyle='round', zorder=z)


def panel(fig, left, width, bottom=0.20, height=0.66):
    ax = fig.add_axes([left, bottom, width, height], zorder=1)
    ax.set_facecolor('none')
    for s in ax.spines.values():
        s.set_visible(False)
    ax.set_xticks([]); ax.set_yticks([])
    return ax


def divider(fig, x, y0=0.13, y1=0.90):
    ln = fig.add_axes([x, y0, 0.0012, y1 - y0], zorder=2)
    ln.set_facecolor('#48606F'); ln.set_alpha(0.55)
    ln.set_xticks([]); ln.set_yticks([])
    for s in ln.spines.values():
        s.set_visible(False)
