"""
Copyright Malcolm J. Macleod (malcolm@simulationuniverse.com)

 * This software is free to use, modify, and distribute under creative commons
 * https://creativecommons.org/licenses/by-nc-sa/4.0/ (CC CC BY-NC-SA 4.0).
 * Any derivative works or redistributions must include appropriate credit to
 * Malcolm Macleod and this permission notice shall
 * be included in all copies or substantial portions of the Software.

"""

"""
tau_animation.py  --  the tau loop over two spiral steps
==========================================================
Same format as branch_animation.py and spiral_branch.py.

WHY TWO SPIRAL STEPS
----------------------
Each tau-cycle advances the phase by pi (the half-cycle,
Axiom "minimal bounded oscillation"). One spiral step is B = 15 cycles,
so it accumulates 15 pi. Since B is ODD,

    15 pi = 7(2 pi) + pi  ==  pi  (mod 2 pi)      -> sign -1
    30 pi = 15(2 pi)      ==  0   (mod 2 pi)      -> sign +1

so ONE spiral step ends on -1 and TWO return to +1. The scaffolding has
its own two-step return, for the same reason the branch does, and only
because B is odd. Two steps is therefore the minimum window showing a
complete return.

THIS ONE ANIMATES HONESTLY
----------------------------
30 tau-cycles at 40 frames each is 1200 frames, 40 s at 30 fps, with
every cycle individually visible. No compression, no symbolic
stand-ins, no inset on a fake clock -- unlike the branch animation,
which needed ~20,000x more ticks per frame.


P-PARITY (verified symbolically; panel f)
-------------------------------------------
    quantity              P power   flips under P -> -P   alpha?
    P                        1              YES            no
    M, T                     0              no             no
    V = 2 pi P^2/M           2              no             no
    L = VT                   2              no             no
    momentum = MV            2              no             no
    A = 16 alpha V^3/P^3     3              YES            YES
    K = AV/2pi               5              YES            YES
    I = P^15 T^2/M^12       15              YES            no

Momentum is P^2, hence always positive. A and K carry odd powers of P --
and they are exactly the two alpha-bearing quantities. So the sign of P
is invisible throughout the alpha-free (spiral) sector and visible only
in the alpha-bearing (branch/electromagnetic) sector. Since P itself is
not directly measurable (only P^2 as momentum, and the P^3 inside A),
the sign of P could only ever show up electromagnetically.
"""

import math

# ----------------------------------------------------------------------
# CONSTANTS
# ----------------------------------------------------------------------
PI = math.pi
TAU = 2.0*PI
ALPHA_INV = 137.035999084
OMEGA = math.sqrt(PI**math.e * math.e**(1.0 - math.e))

B = 15                        # tau-cycles per spiral step (integer lock)
STEPS = 2                     # spiral steps to animate
CYCLES = B*STEPS              # 30 tau-cycles
FRAMES_PER_CYCLE = 60
NO_FRAMES = CYCLES*FRAMES_PER_CYCLE

PREVIEW_FRAC = 0.62           # where a static capture is taken


# ----------------------------------------------------------------------
# ONE TAU-CYCLE   (u = ln tau, running 0..1)
# ----------------------------------------------------------------------

def tau_of(u):
    """tau = e^u, from dtau/ds = tau with tau(0)=1."""
    return math.exp(u)


def amplitude_inst(u):
    """A = Omega^(u+1). RESETS each cycle: Omega -> Omega^2, then back.
    The GAIN per cycle is Omega, not Omega^2."""
    return OMEGA**(u + 1.0)


def standing_wave(u):
    """w = sin(pi u): 0 -> 1 -> 0. The fundamental Dirichlet mode,
    eigenvalue lambda_1 = pi^2 (pi is the wavenumber)."""
    return math.sin(PI*u)


# ----------------------------------------------------------------------
# ACCUMULATED ACROSS CYCLES   (c = total cycles elapsed, real-valued)
# ----------------------------------------------------------------------

def cycle_local(c):
    """u within the current cycle. u = 1 exactly at a boundary (closure
    amplitude Omega^2) rather than 0."""
    u = c - math.floor(c)
    if u == 0.0 and c > 0:
        u = 1.0
    return u


def phase_total(c):
    """Accumulated phase = pi * c. Does NOT reset -- if it did, every
    closure would give -Omega^2 and nothing would ever return."""
    return PI*c


def sign_total(c):
    return math.cos(phase_total(c))


def gain_total(c):
    """Omega^n, n = completed cycles. A staircase, never resets.
    Distinct from amplitude_inst, which is a sawtooth."""
    return OMEGA**math.floor(c)


def psi_state(c):
    """Psi = A e^{i phi}: instantaneous amplitude, accumulated phase."""
    a = amplitude_inst(cycle_local(c))
    p = phase_total(c)
    return a*math.cos(p), a*math.sin(p)


def is_render(c, tol=1e-9):
    """A render occurs when the cycle counter completes B cycles."""
    n = c/B
    return abs(n - round(n)) < tol and c > 0


# ----------------------------------------------------------------------
# DIAGNOSTICS
# ----------------------------------------------------------------------

def diagnostics():
    print("="*70)
    print("TAU LOOP ANIMATION -- parameters")
    print("="*70)
    print(f"  Omega          = {OMEGA:.12f}")
    print(f"  Omega^2        = {OMEGA**2:.10f}   (closure amplitude)")
    print(f"  B              = {B} tau-cycles per spiral step")
    print(f"  spiral steps   = {STEPS}  ->  {CYCLES} tau-cycles total")
    print(f"  NO_FRAMES      = {NO_FRAMES}  ({FRAMES_PER_CYCLE}/cycle)")
    print(f"  duration @30fps= {NO_FRAMES/30:.0f} s, every cycle visible")

    print(f"\n  PHASE AND SIGN AT EACH SPIRAL STEP  (B = {B} is ODD)")
    print(f"  {'step':>6}{'cycles':>8}{'phase':>12}{'mod 2pi':>11}{'sign':>7}")
    for s in range(STEPS + 1):
        c = s*B
        ph = phase_total(c)
        print(f"  {s:>6}{c:>8}{ph/PI:>10.0f} pi{(ph % TAU)/PI:>10.0f} pi"
              f"{sign_total(c):>7.0f}")
    print(f"  -> one step ends on -1; two return to +1.")

    print(f"\n  TWO AMPLITUDES -- they behave differently")
    print(f"  {'cycle':>7}{'instantaneous A':>18}{'accumulated Omega^n':>22}")
    for n in [1, 5, 10, 15, 20, 30]:
        print(f"  {n:>7}{amplitude_inst(1.0):>18.4f}{OMEGA**n:>22.4f}")
    print(f"  instantaneous: SAWTOOTH, resets every cycle")
    print(f"  accumulated  : STAIRCASE, never resets")

    print(f"\n  THE SIGN QUESTION AT RENDER")
    I = PI**2*OMEGA**B
    print(f"    Omega^{B}            = {OMEGA**B:.4f}")
    print(f"    I = pi^2 Omega^{B}   = {I:.4f}   (quoted POSITIVE in the paper)")
    print(f"    accumulated sign at first render = {sign_total(B):+.0f}")
    print(f"    so the SIGNED state there is      {sign_total(B)*I:+.4f}")
    print(f"    at the second render sign = {sign_total(2*B):+.0f}, state "
          f"{sign_total(2*B)*I:+.4f}")

    print(f"\n  P-PARITY (why this matters, and where it could be seen)")
    for nm, p, a in [("P", 1, "no"), ("M, T", 0, "no"), ("V, L, momentum", 2, "no"),
                     ("A", 3, "YES"), ("K", 5, "YES"), ("I", 15, "no")]:
        print(f"    {nm:<18}P^{p:<3}{'flips' if p % 2 else 'invariant':>11}"
              f"   alpha: {a}")
    print(f"    momentum = 2 pi P^2 is EVEN -> always positive")
    print(f"    A, K are ODD in P and are the only alpha-bearing quantities,")
    print(f"    so the sign of P is visible ONLY electromagnetically.")


# ----------------------------------------------------------------------
# ANIMATION
# ----------------------------------------------------------------------

def build_animation(save_path=None, fps=30):
    import matplotlib
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation
    import numpy as np

    plt.style.use("default")
    FG, BG = "#111111", "white"
    plt.rcParams.update({
        "figure.facecolor": BG, "axes.facecolor": BG, "savefig.facecolor": BG,
        "text.color": FG, "axes.labelcolor": FG, "axes.edgecolor": FG,
        "xtick.color": FG, "ytick.color": FG, "axes.titlecolor": FG,
    })

    fig = plt.figure(figsize=(16, 9), facecolor=BG)
    axA = fig.add_subplot(2, 3, 1)      # Psi complex plane
    axB = fig.add_subplot(2, 3, 2)      # tau and standing wave, this cycle
    axC = fig.add_subplot(2, 3, 3)      # instantaneous amplitude, sawtooth
    axD = fig.add_subplot(2, 3, 4)      # accumulated gain, staircase (log)
    axE = fig.add_subplot(2, 3, 5)      # accumulated phase and sign
    axF = fig.add_subplot(2, 3, 6)      # render events and the sign question

    cs = np.linspace(0, CYCLES, NO_FRAMES)

    # ---- (a) static ghost of the whole path ----
    ghost = [psi_state(c) for c in np.linspace(0, CYCLES, 4000)]
    axA.plot([p[0] for p in ghost], [p[1] for p in ghost],
             color="#e0e0e0", lw=0.8, zorder=1)
    lim = OMEGA**2*1.25
    axA.set_xlim(-lim, lim); axA.set_ylim(-lim, lim)
    axA.axhline(0, color="#999999", lw=0.6); axA.axvline(0, color="#999999", lw=0.6)
    axA.scatter([OMEGA], [0], s=60, color="green", zorder=4, label="$\\Omega$")
    axA.scatter([-OMEGA**2], [0], s=120, marker="*", color="red", zorder=4,
                label="$-\\Omega^2$ (odd closes)")
    axA.scatter([OMEGA**2], [0], s=120, marker="*", color="darkgreen", zorder=4,
                label="$+\\Omega^2$ (even closes)")
    # NOTE: every legend below sets loc= explicitly. matplotlib's default
    # is loc="best", which re-solves the placement on EVERY draw to avoid
    # overlapping the data. With blit=False each frame is a full redraw,
    # so the legend box hops between corners as the trace grows -- it
    # looks like flickering markers but the markers never move. Fixed
    # locations are chosen to sit in corners the data cannot reach
    # (|Psi| <= Omega^2 while the axis limit is 1.25 Omega^2, so the
    # corners are outside the trace radius).
    axA.set_aspect("equal"); axA.grid(alpha=0.2); axA.legend(fontsize=7, loc="lower left")
    axA.set_title("(a) $\\Psi = A\\,e^{i\\phi}$: each cycle a half-turn",
                  fontsize=10)
    axA.set_xlabel("Re"); axA.set_ylabel("Im")

    # ---- (b) within-cycle ----
    us = np.linspace(0, 1, 200)
    axB.plot(us, [tau_of(u) for u in us], color="#c0392b", lw=1.6,
             label="$\\tau=e^u$")
    axB.plot(us, [standing_wave(u) for u in us], color="#16a085", lw=1.6,
             label="$w=\\sin\\pi u$")
    axB.axhline(math.e, ls=":", color="gray", lw=1)
    axB.set_xlim(0, 1); axB.set_ylim(-0.15, 3.0)
    axB.grid(alpha=0.2); axB.legend(fontsize=8, loc="upper left")
    axB.set_title("(b) inside one cycle: $u=\\ln\\tau$ runs $0\\to1$", fontsize=10)
    axB.set_xlabel("$u$")

    # ---- (c) sawtooth ----
    axC.plot(cs, [amplitude_inst(cycle_local(c)) for c in cs],
             color="#2980b9", lw=1.2)
    axC.axhline(OMEGA, ls=":", color="gray"); axC.axhline(OMEGA**2, ls=":", color="gray")
    for s in range(1, STEPS + 1):
        axC.axvline(s*B, color="#c0392b", lw=1.2, ls="--")
    axC.set_xlim(0, CYCLES); axC.grid(alpha=0.2)
    axC.set_title("(c) instantaneous $A=\\Omega^{u+1}$: SAWTOOTH\n"
                  "resets each cycle, gain is $\\Omega$", fontsize=10)
    axC.set_xlabel("tau-cycles")

    # ---- (d) staircase ----
    axD.semilogy(cs, [gain_total(c) for c in cs], color="#8e44ad", lw=1.5)
    for s in range(1, STEPS + 1):
        axD.axvline(s*B, color="#c0392b", lw=1.2, ls="--")
    axD.axhline(OMEGA**B, ls=":", color="gray")
    axD.set_xlim(0, CYCLES); axD.grid(alpha=0.2, which="both")
    axD.set_title(f"(d) accumulated gain $\\Omega^n$: STAIRCASE\n"
                  f"$\\Omega^{{{B}}}$={OMEGA**B:,.0f} at each render", fontsize=10)
    axD.set_xlabel("tau-cycles")

    # ---- (e) phase and sign ----
    axE.plot(cs, [phase_total(c)/PI for c in cs], color="#8e44ad", lw=1.4,
             label="$\\phi/\\pi$ (accumulates)")
    axE.plot(cs, [sign_total(c) for c in cs], color="#c0392b", lw=1.4,
             label="$\\cos\\phi$ (sign)")
    for s in range(1, STEPS + 1):
        axE.axvline(s*B, color="#c0392b", lw=1.2, ls="--")
    axE.set_xlim(0, CYCLES); axE.grid(alpha=0.2); axE.legend(fontsize=8, loc="upper left")
    axE.set_title("(e) accumulated phase and sign", fontsize=10)
    axE.set_xlabel("tau-cycles")

    # ---- (f) P-parity: WHICH quantities carry the sign ----
    #
    # This panel replaced a static one that re-plotted the same
    # sign_total() curve as (e), differing only by two annotations. A
    # panel that never moves inside an animation reads as a bug, and the
    # redundancy meant there was nothing for a marker to track.
    #
    # What it shows instead: the power of P in each quantity. Under
    # P -> -P, odd powers flip and even powers do not. Momentum = 2 pi P^2
    # is even, hence always positive; A carries P^3 and K carries P^5,
    # both odd -- and A, K are the ONLY alpha-bearing quantities. So among
    # directly measurable quantities the sign of P is invisible in the
    # alpha-free sector and visible only in the alpha-bearing one.
    # P itself is not directly measurable (only P^2 as momentum, and the
    # P^3 inside A), so the sign of P could only ever appear
    # electromagnetically.
    PPAR = [("$M$", 0, False), ("$T$", 0, False),
            ("momentum $2\\pi P^2$", 2, False),
            ("$V$", 2, False), ("$L$", 2, False),
            ("$A$", 3, True), ("$K$", 5, True)]
    names = [p[0] for p in PPAR]
    powers = [p[1] for p in PPAR]
    ypos = list(range(len(PPAR)))
    fbars = axF.barh(ypos, powers, color="#b8c9d9", edgecolor="#555555")
    axF.set_yticks(ypos); axF.set_yticklabels(names, fontsize=8)
    axF.set_xlim(0, 6.4); axF.invert_yaxis()
    axF.set_xlabel("power of $P$")
    for i, (nm, p, has_a) in enumerate(PPAR):
        axF.text(p + 0.12, i, ("odd" if p % 2 else "even")
                 + ("  $\\alpha$" if has_a else ""),
                 va="center", fontsize=7.5,
                 color=("#c0392b" if p % 2 else "#31708f"))
    axF.grid(alpha=0.2, axis="x")
    axF.set_title("(f) which quantities carry the sign of $P$\n"
                  "odd powers flip; $A$, $K$ are odd AND the only "
                  "$\\alpha$-bearing ones", fontsize=9)

    # ---- dynamic artists ----
    trace,  = axA.plot([], [], color="#d35400", lw=2.2, zorder=3)
    head,   = axA.plot([], [], "o", color="black", ms=8, zorder=5)
    bt,     = axB.plot([], [], "o", color="#c0392b", ms=8)
    bw,     = axB.plot([], [], "o", color="#16a085", ms=8)
    hc,     = axC.plot([], [], "o", color="black", ms=7)
    hd,     = axD.plot([], [], "o", color="black", ms=7)
    he,     = axE.plot([], [], "o", color="black", ms=7)
    ftxt    = axF.text(0.98, 0.97, "", transform=axF.transAxes, ha="right",
                       va="top", fontsize=8.5, family="monospace",
                       bbox=dict(boxstyle="round,pad=0.35", facecolor="white",
                                 edgecolor="#bbbbbb", alpha=0.95))
    status  = fig.text(0.015, 0.015, "", family="monospace", fontsize=9)

    def init():
        for a in (trace, head, bt, bw, hc, hd, he):
            a.set_data([], [])
        return []

    def update(f):
        c = cs[f]
        pts = [psi_state(q) for q in np.linspace(max(0, c - 2.0), c, 300)]
        trace.set_data([p[0] for p in pts], [p[1] for p in pts])
        x, y = psi_state(c); head.set_data([x], [y])

        u = cycle_local(c)
        bt.set_data([u], [tau_of(u)])
        bw.set_data([u], [standing_wave(u)])
        hc.set_data([c], [amplitude_inst(u)])
        hd.set_data([c], [gain_total(c)])
        he.set_data([c], [sign_total(c)])

        # (f) animates: odd-power bars turn red when the current sign of
        # P is negative; even-power bars never change. The panel is a
        # structural statement, and what moves is which half of it is
        # currently flipped.
        s = sign_total(c)
        for bar, (_, p, has_a) in zip(fbars, PPAR):
            if p % 2 and s < 0:
                bar.set_color("#e08b8b")           # odd, currently flipped
            elif p % 2:
                bar.set_color("#8fbf8f")           # odd, currently positive
            else:
                bar.set_color("#b8c9d9")           # even, invariant
        # report the VALUE, not a rounded +-1: mid-cycle the phase factor
        # is between the extremes, and printing "sign = -1" at cos = -0.03
        # overstates it.
        ftxt.set_text(f"$\\cos\\phi$ = {s:+.3f}\n"
                      f"A, K {'flipped' if s < 0 else 'positive'}")

        n_done = int(math.floor(c))
        step = n_done//B
        status.set_text(
            f"tau-cycle {c:6.2f} / {CYCLES}     spiral step {step} / {STEPS}\n"
            f"u = {u:5.3f}   tau = {tau_of(u):6.4f}   "
            f"A = {amplitude_inst(u):7.4f}   w = {standing_wave(u):+6.3f}\n"
            f"accumulated phase = {phase_total(c)/PI:6.2f} pi   "
            f"sign = {sign_total(c):+6.3f}   gain = {gain_total(c):,.1f}")
        return []

    update(int(PREVIEW_FRAC*NO_FRAMES))
    ani = FuncAnimation(fig, update, frames=NO_FRAMES, init_func=init,
                        blit=False, interval=1000/fps)
    fig.suptitle(f"The $\\tau$ loop over {STEPS} spiral steps "
                 f"({CYCLES} cycles, $B={B}$)", fontsize=13)
    fig.tight_layout(rect=[0, 0.07, 1, 0.95])
    if save_path:
        ani.save(save_path, fps=fps, dpi=110)
        print(f"  saved {save_path}")
    return ani


def save_frame(frac=PREVIEW_FRAC, path="tau_frame.png"):
    """One frame to PNG -- no interactive backend needed."""
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    global PREVIEW_FRAC
    PREVIEW_FRAC = frac
    build_animation()
    plt.savefig(path, dpi=110)
    print(f"  saved {path}  (frame at {frac:.0%})")


if __name__ == "__main__":
    diagnostics()

    # ------------------------------------------------------------------
    # SPYDER: the default "inline" backend does NOT animate -- it captures
    # one static image. Either set
    #   Tools > Preferences > IPython console > Graphics > Backend: Qt5
    # (then restart the kernel), or use the snapshot path, which needs no
    # interactive backend:
    #
    save_frame(0.62)
    #
    # ANIMATION -- uncomment ONE:
    import matplotlib.pyplot as plt
    # ani = build_animation(); plt.show()
    #
    ani = build_animation(save_path="tau_loop.mp4", fps=30)
    # ------------------------------------------------------------------
    print("\n  animation commented out; see __main__.")