#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import traceback, sys, uuid, os, json

#
# Example for exporting ActorGroup scene rules
#

def log_exception_traceback():
    exc_type, exc_value, exc_tb = sys.exc_info()
    data = traceback.format_exception(exc_type, exc_value, exc_tb)
    print(str(data))

def get_node_names(sceneGraph, nodeTypeName, testEndPoint = False, validList = None):
    import azlmbr.scene.graph
    import scene_api.scene_data

    node = sceneGraph.get_root()
    nodeList = []
    children = []
    paths = []

    while node.IsValid():
        # store children to process after siblings
        if sceneGraph.has_node_child(node):
            children.append(sceneGraph.get_node_child(node))

        nodeName = scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node))
        paths.append(nodeName.get_path())
        
        include = True 
        
        if (validList is not None):
            include = False # if a valid list filter provided, assume to not include node name
            name_parts = nodeName.get_path().split('.')
            for valid in validList:
                if (valid in name_parts[-1]):
                    include = True
                    break

        # store any node that has provides specifc data content
        nodeContent = sceneGraph.get_node_content(node)
        if include and nodeContent.CastWithTypeName(nodeTypeName):
            if testEndPoint is not None:
                include = sceneGraph.is_node_end_point(node) is testEndPoint
            if include:
                if (len(nodeName.get_path())):
                    nodeList.append(scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node)))

        # advance to next node
        if sceneGraph.has_node_sibling(node):
            node = sceneGraph.get_node_sibling(node)
        elif children:
            node = children.pop()
        else:
            node = azlmbr.scene.graph.NodeIndex()

    return nodeList, paths

def generate_mesh_group(scene, sceneManifest, meshDataList, paths):
    # Compute the name of the scene file
    clean_filename = scene.sourceFilename.replace('.', '_')
    mesh_group_name = os.path.basename(clean_filename)

    # make the mesh group
    mesh_group = sceneManifest.add_mesh_group(mesh_group_name)
    mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, clean_filename)) + '}'

    # add all nodes to this mesh group
    for activeMeshIndex in range(len(meshDataList)):
        mesh_name = meshDataList[activeMeshIndex]
        mesh_path = mesh_name.get_path()
        sceneManifest.mesh_group_select_node(mesh_group, mesh_path)

def create_shape_configuration(nodeName):
    import scene_api.physics_data
    
    if(nodeName in ['_foot_','_wrist_']):
        shapeConfiguration = scene_api.physics_data.BoxShapeConfiguration()
        shapeConfiguration.scale = [1.1, 1.1, 1.1]
        shapeConfiguration.dimensions = [2.1, 3.1, 4.1]
        return shapeConfiguration
    else:
        shapeConfiguration = scene_api.physics_data.CapsuleShapeConfiguration()
        shapeConfiguration.scale = [1.0, 1.0, 1.0]
        shapeConfiguration.height = 1.0
        shapeConfiguration.radius = 1.0
        return shapeConfiguration
    
def create_collider_configuration(nodeName):
    import scene_api.physics_data
    
    colliderConfiguration = scene_api.physics_data.ColliderConfiguration()
    colliderConfiguration.Position = [0.1, 0.1, 0.2]
    colliderConfiguration.Rotation = [45.0, 35.0, 25.0]    
    return colliderConfiguration

def generate_physics_nodes(actorPhysicsSetupRule, nodeNameList):
    import scene_api.physics_data
    
    hitDetectionConfig = scene_api.physics_data.CharacterColliderConfiguration()    
    simulatedObjectColliderConfig = scene_api.physics_data.CharacterColliderConfiguration()
    clothConfig = scene_api.physics_data.CharacterColliderConfiguration()
    ragdollConfig = scene_api.physics_data.RagdollConfiguration()    
    
    for nodeName in nodeNameList:
        shapeConfiguration = create_shape_configuration(nodeName)
        colliderConfiguration = create_collider_configuration(nodeName)
        hitDetectionConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
        simulatedObjectColliderConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
        clothConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
        #
        ragdollNode = scene_api.physics_data.RagdollNodeConfiguration()
        ragdollNode.JointConfig.Name = nodeName
        ragdollConfig.add_ragdoll_node_configuration(ragdollNode)
        ragdollConfig.colliders.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
    
    actorPhysicsSetupRule.set_simulated_object_collider_config(simulatedObjectColliderConfig)
    actorPhysicsSetupRule.set_hit_detection_config(hitDetectionConfig)
    actorPhysicsSetupRule.set_cloth_config(clothConfig)
    actorPhysicsSetupRule.set_ragdoll_config(ragdollConfig)

def generate_actor_group(scene, sceneManifest, meshDataList, paths):
    import scene_api.scene_data
    import scene_api.physics_data
    import scene_api.actor_group
    
    # fetch bone data
    validNames = ['_neck_','_pelvis_','_leg_','_knee_','_spine_','_arm_','_clavicle_','_head_','_elbow_','_wrist_']
    graph = scene_api.scene_data.SceneGraph(scene.graph)
    nodeList, allNodePaths = get_node_names(graph, 'BoneData', validList = validNames)

    nodeNameList = []
    for activeMeshIndex, nodeName in enumerate(nodeList):
        nodeNameList.append(nodeName.get_name())

    # add comment    
    commentRule = scene_api.actor_group.CommentRule()
    commentRule.text = str(nodeNameList)
    
    # ActorPhysicsSetupRule
    actorPhysicsSetupRule = scene_api.actor_group.ActorPhysicsSetupRule()
    generate_physics_nodes(actorPhysicsSetupRule, nodeNameList)
    
    # add scale of the Actor rule
    actorScaleRule = scene_api.actor_group.ActorScaleRule()
    actorScaleRule.scaleFactor = 2.0
    
    # add coordinate system rule
    coordinateSystemRule = scene_api.actor_group.CoordinateSystemRule()
    coordinateSystemRule.useAdvancedData = False
    
    # add morph target rule
    morphTargetRule = scene_api.actor_group.MorphTargetRule()
    morphTargetRule.targets.select_targets([nodeNameList[0]], nodeNameList)
    
    # add skeleton optimization rule
    skeletonOptimizationRule = scene_api.actor_group.SkeletonOptimizationRule()
    skeletonOptimizationRule.autoSkeletonLOD = True
    skeletonOptimizationRule.criticalBonesList.select_targets([nodeNameList[0:2]], nodeNameList)
    
    # add LOD rule
    lodRule = scene_api.actor_group.LodRule()
    lodRule0 = lodRule.add_lod_level(0)
    lodRule0.select_targets([nodeNameList[1:4]], nodeNameList)

    actorGroup = scene_api.actor_group.ActorGroup()
    actorGroup.name = os.path.basename(scene.sourceFilename)
    actorGroup.add_rule(actorScaleRule)
    actorGroup.add_rule(coordinateSystemRule)
    actorGroup.add_rule(skeletonOptimizationRule)
    actorGroup.add_rule(morphTargetRule)
    actorGroup.add_rule(lodRule)
    actorGroup.add_rule(actorPhysicsSetupRule)
    actorGroup.add_rule(commentRule)
    sceneManifest.manifest['values'].append(actorGroup.to_dict())
    
def update_manifest(scene):
    import json, uuid, os
    import azlmbr.scene.graph
    import scene_api.scene_data

    graph = scene_api.scene_data.SceneGraph(scene.graph)
    mesh_name_list, all_node_paths = get_node_names(graph, 'MeshData')
    scene_manifest = scene_api.scene_data.SceneManifest()   
    generate_actor_group(scene, scene_manifest, mesh_name_list, all_node_paths)
    generate_mesh_group(scene, scene_manifest, mesh_name_list, all_node_paths)

    # Convert the manifest to a JSON string and return it
    return scene_manifest.export()

sceneJobHandler = None

def on_update_manifest(args):
    try:
        scene = args[0]
        return update_manifest(scene)
    except RuntimeError as err:
        print (f'ERROR - {err}')
        log_exception_traceback()
    except:
        log_exception_traceback()
    finally:
        global sceneJobHandler
        # do not delete or set sceneJobHandler to None, just disconnect from it.
        # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
        # would cause a crash.
        sceneJobHandler.disconnect()

# try to create SceneAPI handler for processing
try:
    import azlmbr.scene

    sceneJobHandler = azlmbr.scene.ScriptBuildingNotificationBusHandler()
    sceneJobHandler.connect()
    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
except:
    sceneJobHandler = None
