"""Research falsification controls; synthetic motion, not AI model benchmarks."""

from pathlib import Path
import json
import math
import subprocess
import hashlib
import argparse
import numpy as np
from PIL import Image, ImageDraw
import imageio_ffmpeg

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=Path("loop-controls"))
args = parser.parse_args()
OUT = args.output.resolve()
OUT.mkdir(parents=True, exist_ok=True)
FFMPEG = imageio_ffmpeg.get_ffmpeg_exe()
WIDTH, HEIGHT, FPS, N = 480, 270, 30, 120


def frame(angle):
    angle = angle % (2 * math.pi)
    scale = 3
    image = Image.new("RGB", (WIDTH * scale, HEIGHT * scale), (245, 247, 250))
    draw = ImageDraw.Draw(image)
    cx, cy, r = WIDTH * scale / 2, HEIGHT * scale / 2, 75 * scale
    draw.ellipse((cx - r, cy - r, cx + r, cy + r), outline=(185, 195, 207), width=scale)
    x, y = cx + r * math.cos(angle), cy + r * math.sin(angle)
    draw.line((cx, cy, x, y), fill=(70, 85, 105), width=3 * scale)
    draw.ellipse(
        (x - 11 * scale, y - 11 * scale, x + 11 * scale, y + 11 * scale),
        fill=(220, 45, 55),
    )
    draw.ellipse(
        (cx - 4 * scale, cy - 4 * scale, cx + 4 * scale, cy + 4 * scale),
        fill=(35, 50, 70),
    )
    return np.array(image.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS))


clean = [frame(2 * math.pi * i / N) for i in range(N)]
hold = clean + [clean[0]]
two_hold = clean + [clean[0], clean[0]]
ease = [
    frame(2 * math.pi * (3 * (i / (N - 1)) ** 2 - 2 * (i / (N - 1)) ** 3))
    for i in range(N)
]
# Matching pose can reverse direction while the size of each step stays normal.
reverse = [frame(2 * math.pi * i / N) for i in range(N // 2)] + [
    frame(2 * math.pi * (N - i) / N) for i in range(N // 2, N)
]
cases = {
    "clean": clean,
    "one-frame-hold": hold,
    "two-frame-hold": two_hold,
    "ease-in-out": ease,
    "direction-reversal": reverse,
    "repaired-hold": hold[:-1],
}
results = {}
for name, frames in cases.items():
    path = OUT / (name + ".webm")
    if name == "repaired-hold":
        subprocess.run(
            [
                FFMPEG,
                "-y",
                "-v",
                "error",
                "-i",
                str(OUT / "one-frame-hold.webm"),
                "-vf",
                f"trim=end_frame={N},setpts=PTS-STARTPTS",
                "-an",
                "-c:v",
                "libvpx-vp9",
                "-lossless",
                "1",
                "-pix_fmt",
                "yuv420p",
                "-threads",
                "1",
                str(path),
            ],
            check=True,
        )
    else:
        subprocess.run(
            [
                FFMPEG,
                "-y",
                "-v",
                "error",
                "-f",
                "rawvideo",
                "-pix_fmt",
                "rgb24",
                "-s",
                f"{WIDTH}x{HEIGHT}",
                "-r",
                str(FPS),
                "-i",
                "-",
                "-an",
                "-c:v",
                "libvpx-vp9",
                "-lossless",
                "1",
                "-pix_fmt",
                "yuv420p",
                "-threads",
                "1",
                str(path),
            ],
            input=np.stack(frames).tobytes(),
            check=True,
        )
    decoded = subprocess.run(
        [
            FFMPEG,
            "-v",
            "error",
            "-i",
            str(path),
            "-f",
            "rawvideo",
            "-pix_fmt",
            "rgb24",
            "-threads",
            "1",
            "-",
        ],
        capture_output=True,
        check=True,
    ).stdout
    arr = np.frombuffer(decoded, dtype=np.uint8).reshape((-1, HEIGHT, WIDTH, 3))
    diffs = np.abs(
        arr.astype(np.float32) - np.roll(arr, 1, axis=0).astype(np.float32)
    ).mean(axis=(1, 2, 3))
    angles = []
    for picture in arr:
        red = (picture[:, :, 0].astype(int) > picture[:, :, 1].astype(int) + 70) & (
            picture[:, :, 0].astype(int) > picture[:, :, 2].astype(int) + 70
        )
        yy, xx = np.nonzero(red)
        angles.append(
            math.atan2(float(yy.mean()) - HEIGHT / 2, float(xx.mean()) - WIDTH / 2)
        )
    delta = (
        np.arctan2(
            np.sin(np.array(angles) - np.roll(angles, 1)),
            np.cos(np.array(angles) - np.roll(angles, 1)),
        )
        * 180
        / math.pi
    )
    median = float(np.median(diffs))
    results[name] = {
        "frames": len(arr),
        "fps": FPS,
        "duration_seconds": len(arr) / FPS,
        "bytes": path.stat().st_size,
        "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
        "decoded_sha256": hashlib.sha256(decoded).hexdigest(),
        "boundary_mean_absolute_rgb_difference": float(diffs[0]),
        "median_step_rgb_difference": median,
        "boundary_to_median_ratio": float(diffs[0] / median),
        "first_6_step_degrees": delta[:6].tolist(),
        "last_6_step_degrees": delta[-6:].tolist(),
        "last_6_rgb_difference": diffs[-6:].tolist(),
        "first_6_rgb_difference": diffs[:6].tolist(),
    }
assert results["clean"]["decoded_sha256"] == results["repaired-hold"]["decoded_sha256"]
record = {
    "kind": "synthetic diagnostic controls, not AI benchmarks",
    "ffmpeg_version": subprocess.run(
        [FFMPEG, "-version"], capture_output=True, text=True
    ).stdout.splitlines()[0],
    "measurement": "Decoded RGB mean absolute pixel change; signed angular movement from red-object centroid only for these known controls",
    "results": results,
    "conclusion": "Pixel-change magnitude alone cannot classify direction continuity; near-normal boundary magnitude occurs in the reversal control. Do not promise a general smoothness classifier.",
}
(OUT / "loop-controls.json").write_text(
    json.dumps(record, indent=2), encoding="utf8"
)
print(
    json.dumps(
        {
            name: {
                key: value
                for key, value in data.items()
                if key
                in [
                    "frames",
                    "bytes",
                    "boundary_to_median_ratio",
                    "first_6_step_degrees",
                    "last_6_step_degrees",
                ]
            }
            for name, data in results.items()
        },
        indent=2,
    )
)
