"""Draw the complex plane, and addition as one arrow carried to another.

One grid helper serves both, so the paper a reader learns in the first
figure is the paper the second argues on. One unit per square.

Writes plane.svg and addition.svg beside this file.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
matplotlib.rcParams["svg.fonttype"] = "path"
import matplotlib.pyplot as plt

TEAL, INDIGO, ROSE, AMBER = "#00706B", "#3A49A6", "#A93356", "#B36A1E"
INK, MUTED, GRID, FINE = "#0D1620", "#5A6874", "#D3DBE3", "#E1E7ED"


def imag_label(y):
    """Name a tick on the vertical axis, which counts in i rather than in units."""
    return "i" if y == 1 else "-i" if y == -1 else f"{y}i"


def grid(xlim, ylim, size, step=1.0, ticks=True):
    """One complex plane, ruled like graph paper and ready to draw on.

    Five fine squares to each ruled one, which is how squared paper is
    printed. The fine lines carry no meaning. They are there so a length
    can be read off by counting rather than estimated.
    """
    fig, ax = plt.subplots(figsize=size)
    lo_x, hi_x = int(np.floor(xlim[0])), int(np.ceil(xlim[1]))
    lo_y, hi_y = int(np.floor(ylim[0])), int(np.ceil(ylim[1]))
    fine = step / 5
    for x in np.arange(lo_x, hi_x + fine / 2, fine):
        ax.axvline(x, lw=0.5, color=FINE, zorder=0)
    for y in np.arange(lo_y, hi_y + fine / 2, fine):
        ax.axhline(y, lw=0.5, color=FINE, zorder=0)
    for x in np.arange(lo_x, hi_x + step / 2, step):
        ax.axvline(x, lw=0.8, color=GRID, zorder=0)
    for y in np.arange(lo_y, hi_y + step / 2, step):
        ax.axhline(y, lw=0.8, color=GRID, zorder=0)
    ax.axhline(0, lw=1.3, color=INK, zorder=1)
    ax.axvline(0, lw=1.3, color=INK, zorder=1)

    # Ticks are drawn by hand, because the two axes are labelled differently.
    for x in ([] if not ticks else range(lo_x, hi_x + 1)):
        if x == 0 or not xlim[0] < x < xlim[1]:
            continue
        ax.plot([x, x], [-0.09, 0.09], lw=1.1, color=INK, zorder=2)
        ax.annotate(f"{x}", (x, -0.17), ha="center", va="top", fontsize=10, color=INK)
    for y in ([] if not ticks else range(lo_y, hi_y + 1)):
        if y == 0 or not ylim[0] < y < ylim[1]:
            continue
        ax.plot([-0.07, 0.07], [y, y], lw=1.1, color=INK, zorder=2)
        ax.annotate(imag_label(y), (-0.17, y), ha="right", va="center",
                    fontsize=10, color=INK)
    if ticks:
        ax.annotate("0", (-0.17, -0.17), ha="right", va="top", fontsize=10, color=INK)
    ax.annotate("real axis", (xlim[1] - 0.1, 0.2), ha="right", va="bottom",
                fontsize=11, color=MUTED)
    ax.annotate("imaginary axis", (0.2, ylim[1] - 0.1), ha="left", va="top",
                fontsize=11, color=MUTED)

    ax.set_xlim(*xlim)
    ax.set_ylim(*ylim)
    ax.set_aspect("equal")
    ax.set_xticks([])
    ax.set_yticks([])
    for s in ax.spines.values():
        s.set_visible(False)
    return fig, ax


def arrow(ax, tip, colour, tail=0j, lw=1.6, style="-|>"):
    ax.annotate("", xy=(tip.real, tip.imag), xytext=(tail.real, tail.imag),
                arrowprops=dict(arrowstyle=style, lw=lw, color=colour,
                                shrinkA=0, shrinkB=0), zorder=3)


def arc(ax, ang, radius, colour, start=0.0):
    """An arc from one angle to another, for showing what a turn does."""
    t = np.linspace(start, ang, 240)
    ax.plot(radius * np.cos(t), radius * np.sin(t), lw=1.4, color=colour, zorder=2)
    mid = (start + ang) / 2
    return radius * np.cos(mid), radius * np.sin(mid)


# What each drawing colour becomes in the file. The page carries these
# drawings rather than linking them, so they inherit the reader's theme
# instead of answering the operating system. An img element could not:
# it is a separate document and cannot read --ink from the page.
PROPERTY = {TEAL: "--teal", INDIGO: "--indigo", ROSE: "--rose",
            AMBER: "--amber", INK: "--ink", MUTED: "--muted",
            GRID: "--line", FINE: "--sunk"}


def save(fig, name):
    """Write the figure, then make it the page's own drawing.

    Three passes over what matplotlib produced. Every colour becomes a
    custom property with the literal kept as a fallback. Every id is given
    the file's stem, because several of these are inlined into one page and
    matplotlib numbers them from one in each. And six decimal places become
    two, which is 0.005 of a point, well under a pixel at any size the page
    uses. A number whose whole part is zero is left alone: those are the
    glyph scale factors, where rounding would resize the text.
    """
    import re

    fig.tight_layout()
    # transparent, so the page shows through. Without it matplotlib
    # paints an opaque white rectangle behind everything, and the
    # figure then reads as a light card whatever the theme does to
    # the ink drawn on it.
    fig.savefig(name, transparent=True)
    text = open(name, encoding="utf-8").read()

    stem = re.sub(r"[^A-Za-z0-9]+", "-", name.rsplit(".", 1)[0]).strip("-")
    text = re.sub(r'(\sid=")([^"]+)"',
                  lambda m: f'{m.group(1)}{stem}-{m.group(2)}"', text)
    text = re.sub(r'((?:xlink:)?href="#)([^"]+)"',
                  lambda m: f'{m.group(1)}{stem}-{m.group(2)}"', text)
    text = re.sub(r'(url\(#)([^)]+)\)',
                  lambda m: f'{m.group(1)}{stem}-{m.group(2)})', text)

    for literal, prop in PROPERTY.items():
        text = re.sub(re.escape(literal), f"var({prop}, {literal.lower()})",
                      text, flags=re.IGNORECASE)

    text = re.sub(r"(\d+)\.(\d{3,})",
                  lambda m: (m.group(0) if m.group(1) == "0"
                             else f"{float(m.group(0)):.2f}".rstrip("0").rstrip(".")),
                  text)

    with open(name, "w", encoding="utf-8") as f:
        f.write(text)
    print(f"{name} written")


# One number, placed by its two parts.
fig, ax = grid((-3.6, 4.6), (-2.8, 3.6), (7.4, 5.6))
a, b = 3, 2
ax.plot([a, a], [0, b], lw=1.3, ls=(0, (4, 3)), color=TEAL, zorder=3)
ax.plot([0, a], [b, b], lw=1.3, ls=(0, (4, 3)), color=TEAL, zorder=3)
ax.plot(a, b, "o", ms=8, color=ROSE, zorder=4)
ax.annotate("3 + 2i", (a + 0.18, b + 0.16), ha="left", va="bottom", fontsize=13, color=ROSE)
ax.annotate("a = 3", (a / 2, b + 0.12), ha="center", va="bottom", fontsize=11, color=TEAL)
ax.annotate("b = 2", (a + 0.14, b / 2), ha="left", va="center", fontsize=11, color=TEAL)
ax.plot(-2, 0, "o", ms=5.5, color=AMBER, zorder=3)
ax.annotate("-2, a real number", (-2, -0.3), ha="center", va="top", fontsize=10, color=AMBER)
ax.plot(0, -2, "o", ms=5.5, color=INDIGO, zorder=3)
ax.annotate("-2i, purely imaginary", (0.24, -2), ha="left", va="center",
            fontsize=10, color=INDIGO)
save(fig, "plane.svg")



# Addition, as one arrow carried to the tip of the other.
z1, z2 = 3 + 2j, -1 + 1j
tot = z1 + z2
fig, ax = grid((-2.6, 4.6), (-1.6, 4.2), (7.0, 5.6))
arrow(ax, z1, TEAL)
arrow(ax, z2, INDIGO)
arrow(ax, tot, INDIGO, tail=z1)                     # the same step, moved
ax.plot([z2.real, tot.real], [z2.imag, tot.imag], lw=1.0, ls=(0, (3, 3)),
        color=MUTED, zorder=2)
ax.plot([0, z1.real], [0, z1.imag], lw=0)             # keeps the limits honest
ax.plot(tot.real, tot.imag, "o", ms=8, color=ROSE, zorder=4)
ax.annotate("3 + 2i", (z1.real + 0.15, z1.imag - 0.1), ha="left", va="top",
            fontsize=12, color=TEAL)
ax.annotate("-1 + i", (z2.real - 0.15, z2.imag + 0.1), ha="right", va="bottom",
            fontsize=12, color=INDIGO)
ax.annotate("2 + 3i", (tot.real + 0.18, tot.imag + 0.14), ha="left", va="bottom",
            fontsize=13, color=ROSE)
ax.annotate("the same step, from the tip\nof the first",
            ((z1.real + tot.real) / 2 + 0.14, (z1.imag + tot.imag) / 2),
            ha="left", va="center", fontsize=10, color=INDIGO)
save(fig, "addition.svg")
