# SPDX-License-Identifier: GPL-2.0-or-later


"""
Functions related to anim and pose.
"""

from collections.abc import Collection

from typing import Optional, FrozenSet, Set, Union, Iterable, cast
import dataclasses
import functools
import re

import bpy
from bpy.types import (
    Action,
    Bone,
    Context,
    FCurve,
    Keyframe,
    Object,
    TimelineMarker
)

from asset_library.common.bl_utils import active_catalog_id, split_path

FCurveValue = Union[float, int]

pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
"""RegExp for matching FCurve data paths."""

def is_pose(action):
    for fc in action.fcurves:
        if len(fc.keyframe_points) > 1:
            return False

    return True

def get_bone_visibility(data_path):
    bone, prop = split_path(data_path)
    
    ob = bpy.context.object
    b_layers = [i for i, val in enumerate(ob.pose.bones[bone].bone.layers) if val]
    
    rig_layers = [(i, val) for i, val in enumerate(ob.data.layers)]

    return ob.data.layers[b_layers[0]]

def get_keyframes(action, selected=False, includes=[]):
    if selected:
        # keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points if k.select_control_point and get_bone_visibility(f.data_path)])
        keyframes = []
        for f in action.fcurves:
            bone, prop = split_path(f.data_path)
            for k in f.keyframe_points:
                if bone not in includes:
                    continue
                if not k.select_control_point:
                    continue
                if not get_bone_visibility(f.data_path):
                    continue
                
                keyframes += [int(k.co[0])]                    
        if len(keyframes) <= 1:
            keyframes = [bpy.context.scene.frame_current]
    else:
        keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points])

    return keyframes

def get_marker(action):
    if action.pose_markers:
        markers = action.pose_markers
        return next((m.name for m in markers if m.frame == bpy.context.scene.frame_current), None)

def reset_bone(bone, transform=True, custom_props=True):
    if transform:
        bone.location = (0, 0, 0)
        if bone.rotation_mode == "QUATERNION":
            bone.rotation_quaternion = (0, 0, 0, 0)
        elif bone.rotation_mode == "AXIS_ANGLE":
            bone.rotation_axis_angle = (0, 0, 0, 0)
        else:
            bone.rotation_euler = (0, 0, 0)
            
        bone.scale = (1, 1, 1)
    
    if custom_props:
        for key, value in bone.items():
            try:
                id_prop = bone.id_properties_ui(key)
            except TypeError:
                continue
                
            if not isinstance(value, (int, float)) or not id_prop:
                continue
            bone[key] = id_prop.as_dict()['default']

def is_asset_action(action):
    return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0))

def conform_action(action):
    tags = ('pose', 'anim')

    if any(tag in action.asset_data.tags.keys() for tag in tags):
        return

    for fc in action.fcurves:
        action.asset_data['is_single_frame'] = True
        if len(fc.keyframe_points) > 1:
            action.asset_data['is_single_frame'] = False
            break

    if action.asset_data['is_single_frame']:
        action.asset_data.tags.new('pose')
    else:
        action.asset_data.tags.new('anim')

def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]):
    ## Clean Keyframe Before/After Range
    for fc in action.fcurves:
        bone, prop = split_path(fc.data_path)
        # !! Mush Mush dependent. Need to be fix
        if bone in excludes or bone not in includes:
            action.fcurves.remove(fc)
            continue

        # Add Keyframe At Start/End Range
        for fr in (frame_start, frame_end):
            fc_val = fc.evaluate(fr)
            fc.keyframe_points.insert(frame=fr, value=fc_val)
            fc.update()

        # Remove Keyframe out of range
        for k in reversed(fc.keyframe_points):                    
            if int(k.co[0]) not in range(frame_start, frame_end+1):
                fc.keyframe_points.remove(k)
                fc.update()

def append_action(action_path='', action_name=''):
    print(f'Loading {action_name} from: {action_path}')

    with bpy.data.libraries.load(str(action_path), link=False) as (data_from, data_to):
        data_to.actions = [action_name]

    return data_to.actions[0]

def apply_anim(action_lib, ob, bones=[]):
    from mathutils import Vector

    scn = bpy.context.scene

    if not ob.animation_data:
        ob.animation_data_create()
    
    action = ob.animation_data.action
    
    if not action:
        action = bpy.data.actions.new(ob.name)
        ob.animation_data.action = action

    keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points])
    if not keys:
        print(f'The action {action_lib.name} has no keyframes')
        return

    first_key = keys[0]
    key_offset = scn.frame_current - first_key

    key_attr = ('type', 'interpolation', 'handle_left_type', 'handle_right_type',
        'amplitude', 'back', 'easing', 'period', 'handle_right', 'handle_left'
    )
    for fc in action_lib.fcurves:
        bone_name, prop_name = split_path(fc.data_path)

        if bones and bone_name not in bones:
            continue

        action_fc = action.fcurves.find(fc.data_path, index=fc.array_index)
        if not action_fc:
            action_fc = action.fcurves.new(
                fc.data_path,
                index=fc.array_index,
                action_group=fc.group.name if fc.group else fc.data_path.split('"')[1]
            )
        
        for kf_lib in fc.keyframe_points:
            kf = action_fc.keyframe_points.insert(
                frame=kf_lib.co[0] + key_offset,
                value=kf_lib.co[1]
            )
            for attr in key_attr:
                src_val = getattr(kf_lib, attr)
                if attr.startswith('handle') and 'type' not in attr:
                    src_val += Vector((key_offset, 0))

                setattr(kf, attr, src_val)

        fc.update()
    
    # redraw graph areas
    for window in bpy.context.window_manager.windows:
        screen = window.screen
        for area in screen.areas:
            if area.type == 'GRAPH_EDITOR':
                area.tag_redraw()