from __future__ import annotations

import shutil
import subprocess
from pathlib import Path

from PIL import Image

from symmetry_d3_rendering import (
    AXIS,
    BG,
    INK,
    RUST,
    TEAL,
    Projection,
    Renderer,
    ease,
    orbit,
    reflect_in_a_plane,
    reflect_progress,
    rotate_about_axis,
    state_vector,
)


ROOT = Path(__file__).resolve().parents[1]
OUTPUT_DIR = ROOT / "content" / "drafts" / "animations"
SCRATCH = OUTPUT_DIR / "_symmetry_d3_rotations_vs_flips_frames"
FFMPEG = ROOT / ".tools" / "micromamba-anim-root" / "envs" / "anim" / "Library" / "bin" / "ffmpeg.exe"
if not FFMPEG.is_file():
    from imageio_ffmpeg import get_ffmpeg_exe

    FFMPEG = Path(get_ffmpeg_exe())

WIDTH = 1200
HEIGHT = 700
SCALE = 2
FPS = 24

LEFT_ORIGIN = (320.0, 405.0)
RIGHT_ORIGIN = (920.0, 405.0)
PANEL_SCALE = 128.0
ROT_STATE = state_vector(1.34, 1.04, 45.0)
FLIP_STATE = state_vector(1.34, 1.04, 125.0)


def rotation_timeline() -> list[tuple[float, float]]:
    timeline: list[tuple[float, float]] = []
    timeline.extend([(0.0, 0.0)] * 14)

    for step in range(3):
        for index in range(30):
            t = ease(index / 29)
            angle = 120.0 * (step + t)
            trace = (step + t) / 3.0
            timeline.append((angle, trace))
        timeline.extend([(120.0 * (step + 1), (step + 1) / 3.0)] * 8)

    timeline.extend([(360.0, 1.0)] * 28)
    return timeline


def draw_rotation_panel(renderer: Renderer, angle: float, trace: float) -> None:
    projection = Projection.from_view(LEFT_ORIGIN, PANEL_SCALE)
    renderer.coordinate_axes(projection, 1.0)
    renderer.diagonal_axis(projection, 1.0)
    renderer.orbit_trace(orbit(ROT_STATE), projection, RUST, 0.88, progress=trace, width=2.8)
    renderer.vector(rotate_about_axis(ROT_STATE, angle), projection, TEAL, 1.0)
    renderer.component_axis_labels(projection)


def draw_flip_trace(renderer: Renderer, projection: Projection, progress: float, global_progress: float) -> None:
    reflected = reflect_in_a_plane(FLIP_STATE)
    p0 = projection.project(FLIP_STATE)
    p1 = projection.project(reflected)
    trace_alpha = 0.42 + 0.35 * min(1.0, global_progress * 2.0)
    renderer.line([(p0[0], p0[1]), (p1[0], p1[1])], RUST, 2.6, trace_alpha)
    renderer.circle((p0[0], p0[1]), 4.5, RUST, 0.64)
    renderer.circle((p1[0], p1[1]), 4.5, RUST, 0.64)
    renderer.arrow((0.0, 0.0, 0.0), FLIP_STATE, projection, RUST, 1.8, 0.26, head=11.0)

    state = reflect_progress(FLIP_STATE, progress)
    renderer.vector(state, projection, TEAL, 1.0)


def draw_flip_panel(renderer: Renderer, progress: float, global_progress: float) -> None:
    projection = Projection.from_view(RIGHT_ORIGIN, PANEL_SCALE)
    renderer.coordinate_axes(projection, 1.0)
    renderer.reflection_plane(projection, 0.28)
    renderer.diagonal_axis(projection, 1.0)
    draw_flip_trace(renderer, projection, progress, global_progress)
    renderer.component_axis_labels(projection)


def draw_frame(angle: float, trace: float, index: int, total: int) -> Image.Image:
    renderer = Renderer(WIDTH, HEIGHT, SCALE)
    renderer.line([(WIDTH / 2, 46.0), (WIDTH / 2, HEIGHT - 42.0)], (235, 235, 235), 1.2, 1.0)
    renderer.text("rotations", (WIDTH * 0.25, 52.0), 24, INK, 0.92, bold=True)
    renderer.text("flips", (WIDTH * 0.75, 52.0), 24, INK, 0.92, bold=True)
    renderer.equal_components_key((180.0, 112.0))
    renderer.equal_components_key((780.0, 112.0))

    draw_rotation_panel(renderer, angle, trace)

    global_progress = index / max(1, total - 1)
    flip_progress = global_progress * 2.0 if global_progress <= 0.5 else (1.0 - global_progress) * 2.0
    draw_flip_panel(renderer, flip_progress, global_progress)

    return renderer.output()


def build_frames() -> list[Image.Image]:
    timeline = rotation_timeline()
    total = len(timeline)
    return [draw_frame(angle, trace, index, total) for index, (angle, trace) in enumerate(timeline)]


def encode(frames: list[Image.Image]) -> tuple[Path, Path]:
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    if SCRATCH.exists():
        shutil.rmtree(SCRATCH)
    SCRATCH.mkdir()
    try:
        for index, image in enumerate(frames):
            image.save(SCRATCH / f"frame_{index:04d}.png")

        video = OUTPUT_DIR / "symmetry-d3-rotations-vs-flips.mp4"
        subprocess.run(
            [
                str(FFMPEG),
                "-y",
                "-framerate",
                str(FPS),
                "-i",
                str(SCRATCH / "frame_%04d.png"),
                "-c:v",
                "libx264",
                "-pix_fmt",
                "yuv420p",
                "-movflags",
                "+faststart",
                str(video),
            ],
            check=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        poster = OUTPUT_DIR / "symmetry-d3-rotations-vs-flips-poster.png"
        frames[-1].save(poster)
        return video, poster
    finally:
        if SCRATCH.exists():
            shutil.rmtree(SCRATCH)


def main() -> None:
    video, poster = encode(build_frames())
    print(video)
    print(poster)


if __name__ == "__main__":
    main()
