"""Billet 92: weapon gestures for the hero, retargeted from Mesh2Motion (CC0).

Headless Blender:
  blender -b --factory-startup --python tools/blender/hero_gestures.py -- \
      <hero.glb> <human-base-animations.glb> <human-addon-animations.glb> <out.glb>

The hero keeps its Meshy rig, weights and eight clips. Each gesture is copied
bone by bone in world space: the source bone's rotation from its rest pose is
applied to the hero bone. Limbs first align their rest direction on the source
(both rigs stand in a T pose, the hero's arms 13 degrees lower); the spine,
neck and clavicles keep their own rest, since the two rigs build them
differently. Fingers have no hero bone and are dropped. Gestures stay in place:
no bone keeps a translation.
"""
import sys

import bpy
from mathutils import Quaternion, Vector

# Mesh2Motion (Unreal-style) bone -> Meshy hero bone.
BONES = {
    'pelvis': 'Hips', 'spine_01': 'Spine02', 'spine_02': 'Spine01', 'spine_03': 'Spine',
    'neck_01': 'neck', 'head': 'Head',
    'clavicle_l': 'LeftShoulder', 'upperarm_l': 'LeftArm', 'lowerarm_l': 'LeftForeArm', 'hand_l': 'LeftHand',
    'clavicle_r': 'RightShoulder', 'upperarm_r': 'RightArm', 'lowerarm_r': 'RightForeArm', 'hand_r': 'RightHand',
    'thigh_l': 'LeftUpLeg', 'calf_l': 'LeftLeg', 'foot_l': 'LeftFoot', 'ball_l': 'LeftToeBase',
    'thigh_r': 'RightUpLeg', 'calf_r': 'RightLeg', 'foot_r': 'RightFoot', 'ball_r': 'RightToeBase',
}
ALIGNED = {'upperarm_l', 'lowerarm_l', 'hand_l', 'upperarm_r', 'lowerarm_r', 'hand_r',
           'thigh_l', 'calf_l', 'foot_l', 'thigh_r', 'calf_r', 'foot_r'}
# Two candidates per family (billet 92): [hero clip, source file, source action].
GESTURES = [
    ('Sword_Attack', 'base', 'Sword_Attack'),                            # estoc
    ('Punch_Jab', 'base', 'Punch_Jab'),                                  # estoc
    ('Sword_Regular_A', 'base', 'Sword_Regular_A'),                      # taille
    ('Sword_Regular_B', 'base', 'Sword_Regular_B'),                      # taille
    ('Chop_Tree', 'base', 'Chop_Tree'),                                  # frappe de haut
    ('Sword_Attack_Air_Vertical', 'addon', 'Sword_Attack_Air_Vertical'), # frappe de haut
    ('OverhandThrow', 'base', 'OverhandThrow'),                          # lancer
    ('Throw_Object', 'addon', 'Throw Object'),                           # lancer
    ('Pistol_Shoot', 'base', 'Pistol_Shoot'),                            # tir
    ('Spell_Simple_Shoot', 'base', 'Spell_Simple_Shoot'),                # tir
    ('Two_hand_Blast', 'addon', 'Two-hand Blast'),                       # jet continu
    ('Farm_Watering', 'base', 'Farm_Watering'),                          # jet continu
]
Y = Vector((0.0, 1.0, 0.0))


def load(path):
    objects, actions = set(bpy.data.objects), set(bpy.data.actions)
    bpy.ops.import_scene.gltf(filepath=path)
    new_objects = [o for o in bpy.data.objects if o not in objects]
    armature = next(o for o in new_objects if o.type == 'ARMATURE')
    return armature, new_objects, {a.name: a for a in bpy.data.actions if a not in actions}


def assign(armature, action):
    data = armature.animation_data or armature.animation_data_create()
    data.action = action
    if hasattr(data, 'action_slot') and data.action_slot is None and len(action.slots):
        data.action_slot = action.slots[0]


def world_rest(armature, bone):
    return (armature.matrix_world.to_3x3().normalized().to_quaternion() @ bone.matrix_local.to_quaternion()).normalized()


def retarget(hero, source, action, name, bones=None):
    target_of = {s: h for s, h in (bones or BONES).items() if s in source.data.bones and h in hero.data.bones}
    rest = {s: world_rest(source, source.data.bones[s]) for s in target_of}
    hero_rest = {}
    for s, h in target_of.items():
        own = world_rest(hero, hero.data.bones[h])
        align = Quaternion()
        if s in ALIGNED:
            align = (own @ Y).rotation_difference(rest[s] @ Y)
        hero_rest[h] = (align @ own).normalized()
    source_of = {h: s for s, h in target_of.items()}
    assign(source, action)
    clip = bpy.data.actions.new(name)
    assign(hero, clip)
    hero_world = hero.matrix_world.to_3x3().normalized().to_quaternion().inverted()
    first, last = (int(round(v)) for v in action.frame_range)
    previous = {}
    for frame in range(first, last + 1):
        bpy.context.scene.frame_set(frame)
        posed = {}
        for bone in hero.data.bones:  # parents come first
            s = source_of.get(bone.name)
            if s is None:
                continue
            source_pose = (source.matrix_world.to_3x3().normalized().to_quaternion() @ source.pose.bones[s].matrix.to_quaternion()).normalized()
            posed[bone.name] = hero_world @ ((source_pose @ rest[s].inverted()) @ hero_rest[bone.name])
        for bone in hero.data.bones:
            pose_bone = hero.pose.bones[bone.name]
            pose_bone.rotation_mode = 'QUATERNION'
            if bone.name in posed:
                own_rest = bone.matrix_local.to_quaternion()
                if bone.parent is None:
                    basis = own_rest.inverted() @ posed[bone.name]
                else:
                    parent_pose = _armature_pose(bone.parent, posed)
                    local_rest = bone.parent.matrix_local.to_quaternion().inverted() @ own_rest
                    basis = local_rest.inverted() @ parent_pose.inverted() @ posed[bone.name]
            else:
                basis = Quaternion()
            basis.normalize()
            if bone.name in previous and previous[bone.name].dot(basis) < 0.0:
                basis.negate()
            previous[bone.name] = basis.copy()
            pose_bone.rotation_quaternion = basis
            pose_bone.location = (0.0, 0.0, 0.0)
            pose_bone.scale = (1.0, 1.0, 1.0)
            for path in ('rotation_quaternion', 'location', 'scale'):
                pose_bone.keyframe_insert(path, frame=frame - first, group=bone.name)
    return clip


def _armature_pose(bone, posed):
    """A parent without a source bone rests: its pose follows its own parent."""
    if bone.name in posed:
        return posed[bone.name]
    rest = bone.matrix_local.to_quaternion()
    if bone.parent is None:
        return rest
    parent_rest = bone.parent.matrix_local.to_quaternion()
    return _armature_pose(bone.parent, posed) @ parent_rest.inverted() @ rest


def main():
    hero_path, base_path, addon_path, out_path = sys.argv[sys.argv.index('--') + 1:][:4]
    bpy.ops.wm.read_factory_settings(use_empty=True)
    hero, hero_objects, hero_actions = load(hero_path)
    original = hero.animation_data.action if hero.animation_data else None
    sources = {}
    for key, path in (('base', base_path), ('addon', addon_path)):
        sources[key] = load(path)
    made = []
    for name, key, action_name in GESTURES:
        armature, _, actions = sources[key]
        # A temporary name: the source action may still hold the final one.
        made.append((retarget(hero, armature, actions[action_name], name + '__hero'), name))
    for armature, objects, actions in sources.values():
        for action in actions.values():
            bpy.data.actions.remove(action)
        for obj in objects:
            bpy.data.objects.remove(obj, do_unlink=True)
    for clip, name in made:
        clip.name = name
    assign(hero, original) if original else None
    bpy.ops.object.select_all(action='DESELECT')
    for obj in hero_objects:
        obj.select_set(True)
    bpy.context.view_layer.objects.active = hero
    bpy.ops.export_scene.gltf(filepath=out_path, export_format='GLB', use_selection=True,
                              use_active_scene=True, export_animations=True, export_animation_mode='ACTIONS')
    print('HERO GESTURES', len(hero_actions), '+', len(made), '->', out_path)


if __name__ == '__main__':
    main()
