"""Writes the 3D mascots' moves to src/web/src/domain/toonMoves.json, which scripts/mascots/butler.py bakes into the body's clips and
the viewer plays: the shared idle, wave and walk, and twenty-five idles for each voice, five drawn from its 2D rig's moves and twenty
everyday ones played in its own tempo and reach. Every idle turns bones on top of the voice's held pose and frees none, so the props
stay in the hands and a loop made for one voice plays on any of them. Run with python3 scripts/mascots/moves.py.
"""
import json
from pathlib import Path

OUT = Path(__file__).resolve().parents[2] / "src/web/src/domain/toonMoves.json"
STILL = {}

# How each voice moves at rest, read off its 2D moves: frames for one beat at 24 a second, and how far it turns.
CHARACTER = {
    "butler": {"beat": 30, "reach": 0.7},
    "coach": {"beat": 18, "reach": 1.4},
    "colleague": {"beat": 28, "reach": 0.8},
    "homie": {"beat": 22, "reach": 1.1},
    "squire": {"beat": 24, "reach": 1.2},
}


def there_and_back(bone, turn, beat):
    """One bone out to a turn and home again over two beats."""
    return {bone: [(0, STILL), (beat, turn), (2 * beat, STILL)]}


def scaled(turn, reach):
    return {axis: round(value * reach, 2) if axis.startswith("r") else round(value * reach, 4) for axis, value in turn.items()}


def rhythm(bone, turn, beat, times):
    """A bone ticking out and back times over, as a nod or a bounce does."""
    half = max(beat // 2, 4)
    keys = [(0, STILL)]
    for k in range(times):
        keys += [(k * 2 * half + half, turn), ((k + 1) * 2 * half, STILL)]
    return {bone: keys}


def joined(*tracks):
    out = {}
    for track in tracks:
        for bone, keys in track.items():
            out[bone] = keys
    return out


def length(tracks):
    return max(frame for keys in tracks.values() for frame, _ in keys)


# The everyday idles every voice has, each a function of its beat and reach.
def everyday(beat, reach):
    s = lambda turn: scaled(turn, reach)
    return {
        "breathe": joined(there_and_back("torso", s({"rx": 1.5, "y": 0.012}), beat * 2), there_and_back("head", s({"rx": -1}), beat * 2)),
        "look left": there_and_back("head", s({"ry": 14}), beat),
        "look right": there_and_back("head", s({"ry": -14}), beat),
        "look up": there_and_back("head", s({"rx": -10}), beat),
        "glance down": there_and_back("head", s({"rx": 9}), beat),
        "nod": rhythm("head", s({"rx": 7}), beat, 2),
        "shake": joined({"head": [(0, STILL), (beat // 2, s({"ry": 9})), (beat, s({"ry": -9})), (3 * beat // 2, s({"ry": 6})), (2 * beat, STILL)]}),
        "head tilt": there_and_back("head", s({"rz": 9}), beat),
        "shrug": joined(there_and_back("upper_arm_left", s({"rz": -8}), beat // 2 + 4), there_and_back("upper_arm_right", s({"rz": 8}), beat // 2 + 4), there_and_back("head", s({"rz": 3}), beat // 2 + 4)),
        "weight shift": joined(there_and_back("hips", s({"rz": 3}), beat), there_and_back("torso", s({"rz": -3}), beat)),
        "lean in": there_and_back("torso", s({"rx": 5}), beat),
        "lean back": there_and_back("torso", s({"rx": -5}), beat),
        "sigh": joined(there_and_back("torso", s({"rx": 3, "y": -0.01}), beat + 6), there_and_back("head", s({"rx": 5}), beat + 6)),
        "chuckle": joined(rhythm("torso", s({"rx": 2, "y": 0.008}), beat, 3), rhythm("head", s({"rx": -3}), beat, 3)),
        "neck stretch": joined({"head": [(0, STILL), (beat, s({"rz": 12})), (2 * beat, s({"rz": -12})), (3 * beat, STILL)]}),
        "shoulder roll": joined(rhythm("upper_arm_left", s({"rx": -6}), beat, 2), rhythm("upper_arm_right", s({"rx": -6}), beat, 2)),
        "look around": joined({"head": [(0, STILL), (beat, s({"ry": 12, "rx": -3})), (2 * beat, s({"ry": -12, "rx": -3})), (3 * beat, STILL)]}),
        "settle": joined(there_and_back("hips", s({"y": -0.01}), beat // 2 + 4), there_and_back("torso", s({"rx": 2}), beat // 2 + 4)),
        "foot tap": rhythm("foot_right", s({"rx": 10}), beat, 3),
        "hum": joined(rhythm("head", s({"rz": 4}), beat, 4), rhythm("torso", s({"rz": 1.5}), beat, 4)),
    }


# Each voice's five signature idles, read off its 2D moves (voices/rigs/<voice>/<voice>_moves.json), in its beat and reach.
def signatures(voice, beat, reach):
    s = lambda turn: scaled(turn, reach)
    if voice == "butler":
        return {
            "hat tip and bow": joined(there_and_back("upper_arm_right", s({"rz": 10}), beat), there_and_back("torso", s({"rx": 9}), beat), there_and_back("head", s({"rx": 6}), beat)),
            "tray offer": joined(there_and_back("upper_arm_left", s({"rx": -9}), beat), there_and_back("forearm_left", s({"rx": -6}), beat), there_and_back("torso", s({"ry": -5}), beat)),
            "little two step": joined(rhythm("hips", s({"rz": 4}), beat, 2), rhythm("thigh_left", s({"rx": -8}), beat, 2), rhythm("head", s({"rz": -3}), beat, 2)),
            "polish and inspect": joined(there_and_back("head", s({"rx": 10, "rz": 5}), beat), rhythm("forearm_right", s({"rz": 8}), beat, 2)),
            "straighten up": joined(there_and_back("torso", s({"rx": -4, "y": 0.015}), beat), there_and_back("head", s({"rx": -4}), beat)),
        }
    if voice == "coach":
        return {
            "jog on the spot": joined(rhythm("hips", s({"y": 0.03}), beat, 4), rhythm("thigh_left", s({"rx": -14}), beat, 4), rhythm("thigh_right", s({"rx": -14}), beat, 4)),
            "fist pump": joined(rhythm("upper_arm_left", s({"rz": -14}), beat, 2), rhythm("forearm_left", s({"rz": -10}), beat, 2)),
            "whistle call": joined(there_and_back("head", s({"rx": -8}), beat), there_and_back("upper_arm_right", s({"rx": -12}), beat), there_and_back("torso", s({"rx": -3}), beat)),
            "two squats": joined(rhythm("hips", s({"y": -0.06}), beat * 2, 2), rhythm("thigh_left", s({"rx": -12}), beat * 2, 2), rhythm("thigh_right", s({"rx": -12}), beat * 2, 2)),
            "clipboard check": joined(there_and_back("head", s({"rx": 10, "ry": -8}), beat), there_and_back("forearm_right", s({"rx": -8}), beat)),
        }
    if voice == "colleague":
        return {
            "coffee sip": joined(there_and_back("upper_arm_left", s({"rx": -10}), beat), there_and_back("forearm_left", s({"rx": -12}), beat), there_and_back("head", s({"rx": -5}), beat)),
            "glasses push": joined(there_and_back("upper_arm_right", s({"rx": -10}), beat // 2 + 6), there_and_back("forearm_right", s({"rx": -12}), beat // 2 + 6)),
            "desk stretch": joined(there_and_back("upper_arm_left", s({"rz": -20}), beat * 2), there_and_back("upper_arm_right", s({"rz": 20}), beat * 2), there_and_back("torso", s({"rx": -5}), beat * 2)),
            "typing rhythm": joined(rhythm("forearm_left", s({"rx": 4}), beat // 2, 4), rhythm("forearm_right", s({"rx": 4}), beat // 2 + 2, 4)),
            "think": joined(there_and_back("head", s({"rz": 8, "rx": -4}), beat), there_and_back("torso", s({"rz": 2}), beat)),
        }
    if voice == "homie":
        return {
            "pull up": joined(there_and_back("torso", s({"rx": -6, "y": 0.02}), beat), there_and_back("hips", s({"y": 0.02}), beat)),
            "music nod": rhythm("head", s({"rx": 8}), beat, 4),
            "seat bounce": joined(rhythm("hips", s({"y": 0.025}), beat, 3), rhythm("torso", s({"rx": 2}), beat, 3)),
            "fist bump": joined(there_and_back("upper_arm_right", s({"rx": -12}), beat), there_and_back("forearm_right", s({"rx": -8}), beat)),
            "chill lean": joined(there_and_back("torso", s({"rz": 5, "rx": -3}), beat * 2), there_and_back("head", s({"rz": -4}), beat * 2)),
        }
    return {
        "leg swing and salute": joined(there_and_back("thigh_left", s({"rx": -10}), beat), there_and_back("upper_arm_left", s({"rx": -10}), beat), there_and_back("head", s({"rx": -4}), beat)),
        "shield salute": joined(there_and_back("upper_arm_right", s({"rx": -10}), beat), there_and_back("torso", s({"ry": 4}), beat)),
        "quill flourish": joined(rhythm("forearm_left", s({"rz": 12}), beat, 2), rhythm("upper_arm_left", s({"rx": -6}), beat, 2)),
        "peer into the distance": joined(there_and_back("head", s({"ry": 12, "rx": -5}), beat * 2), there_and_back("torso", s({"rx": -3}), beat * 2)),
        "chest out": joined(there_and_back("torso", s({"rx": -6, "y": 0.015}), beat), there_and_back("head", s({"rx": -5}), beat)),
    }


def shared():
    swing = lambda a, b: [(0, {"rx": a}), (10, {"rx": b}), (19, {"rx": a}), (29, {"rx": b}), (38, {"rx": a})]
    return [
        {"name": "idle", "frames": 58, "frees": [], "tracks": {
            "torso": [(0, STILL), (29, {"rx": 1.5, "y": 0.012, "sy": 1.015}), (58, STILL)],
            "head": [(0, STILL), (19, {"ry": 4, "rz": 3, "rx": -2}), (41, {"ry": -3, "rz": -2}), (58, STILL)],
            "hips": [(0, STILL), (29, {"y": 0.008}), (58, STILL)],
        }},
        {"name": "wave", "frames": 43, "frees": ["upper_arm_right", "forearm_right"], "tracks": {
            "upper_arm_right": [(0, STILL), (10, {"rz": 150, "ry": -20}), (33, {"rz": 150, "ry": -20}), (43, STILL)],
            "forearm_right": [(0, STILL), (10, {"rz": -20}), (17, {"rz": 30}), (24, {"rz": -30}), (31, {"rz": 30}), (36, {"rz": -10}), (43, STILL)],
            "head": [(0, STILL), (12, {"rz": -6}), (33, {"rz": -6}), (43, STILL)],
        }},
        {"name": "walk", "frames": 38, "frees": ["upper_arm_right", "thigh_left", "thigh_right", "shin_left", "shin_right", "foot_left", "foot_right"], "tracks": {
            "hips": [(f, {"y": 0.03 * (f % 10 == 5)}) for f in (0, 5, 10, 14, 19, 24, 29, 33, 38)],
            "thigh_left": swing(-30, 30),
            "shin_left": [(0, {"rx": 20}), (5, {"rx": 40}), (10, STILL), (19, {"rx": 20}), (24, {"rx": 40}), (29, STILL), (38, {"rx": 20})],
            "foot_left": swing(-18, 28),
            "thigh_right": swing(30, -30),
            "shin_right": [(0, STILL), (10, {"rx": 20}), (14, {"rx": 40}), (19, STILL), (29, {"rx": 20}), (33, {"rx": 40}), (38, STILL)],
            "foot_right": swing(28, -18),
            "upper_arm_right": swing(-25, 25),
            "torso": [(0, {"ry": 4}), (10, {"ry": -4}), (19, {"ry": 4}), (29, {"ry": -4}), (38, {"ry": 4})],
        }},
    ]


def idles():
    out = []
    for voice, character in CHARACTER.items():
        moves = {**signatures(voice, character["beat"], character["reach"]), **everyday(character["beat"], character["reach"])}
        for name, tracks in moves.items():
            out.append({"name": f"{voice} {name}", "voice": voice, "frames": length(tracks), "frees": [], "tracks": tracks})
    return out


if __name__ == "__main__":
    moves = shared() + idles()
    OUT.write_text(json.dumps(moves, indent=1) + "\n")
    print(len(moves), "moves:", ", ".join(f"{v} {sum(m.get('voice') == v for m in moves)}" for v in CHARACTER))
