# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 The glTF-Blender-IO authors.

import bpy
import uuid
import numpy as np

from . import gltf2_blender_export_keys
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
from mathutils import Quaternion, Matrix
from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.io.imp.gltf2_io_binary import BinaryData
from io_scene_gltf2.io.com import gltf2_io_constants
from .gltf2_blender_gather_primitive_attributes import array_to_accessor
from io_scene_gltf2.io.exp import gltf2_io_binary_data
from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors

class VExportNode:

    OBJECT = 1
    ARMATURE = 2
    BONE = 3
    LIGHT = 4
    CAMERA = 5
    COLLECTION = 6

    # Parent type, to be set on child regarding its parent
    NO_PARENT = 54
    PARENT_OBJECT = 50
    PARENT_BONE = 51
    PARENT_BONE_RELATIVE = 52
    PARENT_ROOT_BONE = 53
    PARENT_BONE_BONE = 55


    def __init__(self):
        self.children = []
        self.blender_type = None
        self.world_matrix = None
        self.parent_type = None

        self.blender_object = None
        self.blender_bone = None

        self.force_as_empty = False # Used for instancer display

        # Only for bone/bone and object parented to bone
        self.parent_bone_uuid = None

        # Only for bones
        self.use_deform = None

        # Only for armature
        self.bones = {}

        # For deformed object
        self.armature = None # for deformed object and for bone
        self.skin = None

        # glTF
        self.node = None

    def add_child(self, uuid):
        self.children.append(uuid)

    def set_world_matrix(self, matrix):
        self.world_matrix = matrix

    def set_blender_data(self, blender_object, blender_bone):
        self.blender_object = blender_object
        self.blender_bone = blender_bone

    def recursive_display(self, tree, mode):
        if mode == "simple":
            for c in self.children:
                print(self.blender_object.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" )
                tree.nodes[c].recursive_display(tree, mode)

class VExportTree:
    def __init__(self, export_settings):
        self.nodes = {}
        self.roots = []

        self.export_settings = export_settings

        self.tree_troncated = False

    def add_node(self, node):
        self.nodes[node.uuid] = node

    def add_children(self, uuid_parent, uuid_child):
        self.nodes[uuid_parent].add_child(uuid_child)

    def construct(self, blender_scene):
        bpy.context.window.scene = blender_scene
        depsgraph = bpy.context.evaluated_depsgraph_get()

        # Gather parent/children information once, as calling bobj.children is
        #   very expensive operation : takes O(len(bpy.data.objects)) time.
        blender_children = dict()
        for bobj in bpy.data.objects:
            bparent = bobj.parent
            blender_children.setdefault(bobj, [])
            blender_children.setdefault(bparent, []).append(bobj)

        for blender_object in [obj.original for obj in depsgraph.scene_eval.objects if obj.parent is None]:
            self.recursive_node_traverse(blender_object, None, None, Matrix.Identity(4), blender_children)

    def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, blender_children, armature_uuid=None, dupli_world_matrix=None):
        node = VExportNode()
        node.uuid = str(uuid.uuid4())
        node.parent_uuid = parent_uuid
        node.set_blender_data(blender_object, blender_bone)

        # add to parent if needed
        if parent_uuid is not None:
            self.add_children(parent_uuid, node.uuid)
        else:
            self.roots.append(node.uuid)

        # Set blender type
        if blender_bone is not None:
            node.blender_type = VExportNode.BONE
            self.nodes[armature_uuid].bones[blender_bone.name] = node.uuid
            node.use_deform = blender_bone.id_data.data.bones[blender_bone.name].use_deform
        elif blender_object.type == "ARMATURE":
            node.blender_type = VExportNode.ARMATURE
        elif blender_object.type == "CAMERA":
            node.blender_type = VExportNode.CAMERA
        elif blender_object.type == "LIGHT":
            node.blender_type = VExportNode.LIGHT
        elif blender_object.instance_type == "COLLECTION":
            node.blender_type = VExportNode.COLLECTION
        else:
            node.blender_type = VExportNode.OBJECT

        # For meshes with armature modifier (parent is armature), keep armature uuid
        if node.blender_type == VExportNode.OBJECT:
            modifiers = {m.type: m for m in blender_object.modifiers}
            if "ARMATURE" in modifiers and modifiers["ARMATURE"].object is not None:
                if parent_uuid is None or not self.nodes[parent_uuid].blender_type == VExportNode.ARMATURE:
                    # correct workflow is to parent skinned mesh to armature, but ...
                    # all users don't use correct workflow
                    print("WARNING: Armature must be the parent of skinned mesh")
                    print("Armature is selected by its name, but may be false in case of instances")
                    # Search an armature by name, and use the first found
                    # This will be done after all objects are setup
                    node.armature_needed = modifiers["ARMATURE"].object.name
                else:
                    node.armature = parent_uuid

        # For bones, store uuid of armature
        if blender_bone is not None:
            node.armature = armature_uuid

        # for bone/bone parenting, store parent, this will help armature tree management
        if parent_uuid is not None and self.nodes[parent_uuid].blender_type == VExportNode.BONE and node.blender_type == VExportNode.BONE:
            node.parent_bone_uuid = parent_uuid


        # Objects parented to bone
        if parent_uuid is not None and self.nodes[parent_uuid].blender_type == VExportNode.BONE and node.blender_type != VExportNode.BONE:
            node.parent_bone_uuid = parent_uuid

        # World Matrix
        # Store World Matrix for objects
        if dupli_world_matrix is not None:
            node.matrix_world = dupli_world_matrix
        elif node.blender_type in [VExportNode.OBJECT, VExportNode.COLLECTION, VExportNode.ARMATURE, VExportNode.CAMERA, VExportNode.LIGHT]:
            # Matrix World of object is expressed based on collection instance objects are
            # So real world matrix is collection world_matrix @ "world_matrix" of object
            node.matrix_world = parent_coll_matrix_world @ blender_object.matrix_world.copy()
            if node.blender_type == VExportNode.CAMERA and self.export_settings[gltf2_blender_export_keys.CAMERAS]:
                if self.export_settings[gltf2_blender_export_keys.YUP]:
                    correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0))
                else:
                    correction = Matrix.Identity(4).to_quaternion()
                node.matrix_world @= correction.to_matrix().to_4x4()
            elif node.blender_type == VExportNode.LIGHT and self.export_settings[gltf2_blender_export_keys.LIGHTS]:
                if self.export_settings[gltf2_blender_export_keys.YUP]:
                    correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0))
                else:
                    correction = Matrix.Identity(4).to_quaternion()
                node.matrix_world @= correction.to_matrix().to_4x4()
        elif node.blender_type == VExportNode.BONE:
            if self.export_settings['gltf_current_frame'] is True:
                # Use pose bone for TRS
                node.matrix_world = self.nodes[node.armature].matrix_world @ blender_bone.matrix
            else:
                # Use edit bone for TRS --> REST pose will be used
                node.matrix_world = self.nodes[node.armature].matrix_world @ blender_bone.bone.matrix_local
            axis_basis_change = Matrix.Identity(4)
            if self.export_settings[gltf2_blender_export_keys.YUP]:
                axis_basis_change = Matrix(
                    ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
            node.matrix_world = node.matrix_world @ axis_basis_change

        # Force empty ?
        # For duplis, if instancer is not display, we should create an empty
        if blender_object.is_instancer is True and blender_object.show_instancer_for_render is False:
            node.force_as_empty = True

        # Storing this node
        self.add_node(node)

        ###### Manage children ######

        # standard children
        if blender_bone is None and blender_object.is_instancer is False:
            for child_object in blender_children[blender_object]:
                if child_object.parent_bone:
                    # Object parented to bones
                    # Will be manage later
                    continue
                else:
                    # Classic parenting
                    self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world, blender_children)

        # Collections
        if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection:
            for dupli_object in blender_object.instance_collection.all_objects:
                if dupli_object.parent is not None:
                    continue
                self.recursive_node_traverse(dupli_object, None, node.uuid, node.matrix_world, blender_children)

        # Armature : children are bones with no parent
        if blender_object.type == "ARMATURE" and blender_bone is None:
            for b in [b for b in blender_object.pose.bones if b.parent is None]:
                self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, blender_children, node.uuid)

        # Bones
        if blender_object.type == "ARMATURE" and blender_bone is not None:
            for b in blender_bone.children:
                self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, blender_children, armature_uuid)

        # Object parented to bone
        if blender_bone is not None:
            for child_object in [c for c in blender_children[blender_object] if c.parent_bone is not None and c.parent_bone == blender_bone.name]:
                self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world, blender_children)

        # Duplis
        if blender_object.is_instancer is True and blender_object.instance_type != 'COLLECTION':
            depsgraph = bpy.context.evaluated_depsgraph_get()
            for (dupl, mat) in [(dup.object.original, dup.matrix_world.copy()) for dup in depsgraph.object_instances if dup.parent and id(dup.parent.original) == id(blender_object)]:
                self.recursive_node_traverse(dupl, None, node.uuid, parent_coll_matrix_world, blender_children, dupli_world_matrix=mat)

    def get_all_objects(self):
        return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE]

    def get_all_bones(self, uuid): #For armatue Only
        if self.nodes[uuid].blender_type == VExportNode.ARMATURE:
            def recursive_get_all_bones(uuid):
                total = []
                if self.nodes[uuid].blender_type == VExportNode.BONE:
                    total.append(uuid)
                    for child_uuid in self.nodes[uuid].children:
                        total.extend(recursive_get_all_bones(child_uuid))

                return total

            tot = []
            for c_uuid in self.nodes[uuid].children:
                tot.extend(recursive_get_all_bones(c_uuid))
            return tot
        else:
            return []

    def get_all_node_of_type(self, node_type):
        return [n.uuid for n in self.nodes.values() if n.blender_type == node_type]

    def display(self, mode):
        if mode == "simple":
            for n in self.roots:
                print("Root", self.nodes[n].blender_object.name, "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" )
                self.nodes[n].recursive_display(self, mode)


    def filter_tag(self):
        roots = self.roots.copy()
        for r in roots:
            self.recursive_filter_tag(r, None)

    def filter_perform(self):
        roots = self.roots.copy()
        for r in roots:
            self.recursive_filter(r, None) # Root, so no parent

    def filter(self):
        self.filter_tag()
        export_user_extensions('gather_tree_filter_tag_hook', self.export_settings, self)
        self.filter_perform()


    def recursive_filter_tag(self, uuid, parent_keep_tag):
        # parent_keep_tag is for collection instance
        # some properties (selection, visibility, renderability)
        # are defined at collection level, and we need to use these values
        # for all objects of the collection instance.
        # But some properties (camera, lamp ...) are not defined at collection level
        if parent_keep_tag is None:
            self.nodes[uuid].keep_tag = self.node_filter_not_inheritable_is_kept(uuid) and self.node_filter_inheritable_is_kept(uuid)
        elif parent_keep_tag is True:
            self.nodes[uuid].keep_tag = self.node_filter_not_inheritable_is_kept(uuid)
        elif parent_keep_tag is False:
            self.nodes[uuid].keep_tag = False
        else:
            print("This should not happen!")

        for child in self.nodes[uuid].children:
            if self.nodes[uuid].blender_type == VExportNode.COLLECTION:
                self.recursive_filter_tag(child, self.nodes[uuid].keep_tag)
            else:
                self.recursive_filter_tag(child, parent_keep_tag)

    def recursive_filter(self, uuid, parent_kept_uuid):
        children = self.nodes[uuid].children.copy()

        new_parent_kept_uuid = None
        if self.nodes[uuid].keep_tag is False:
            new_parent_kept_uuid = parent_kept_uuid
            # Need to modify tree
            if self.nodes[uuid].parent_uuid is not None:
                self.nodes[self.nodes[uuid].parent_uuid].children.remove(uuid)
            else:
                # Remove from root
                self.roots.remove(uuid)
        else:
            new_parent_kept_uuid = uuid

            # If parent_uuid is not parent_kept_uuid, we need to modify children list of parent_kept_uuid
            if parent_kept_uuid != self.nodes[uuid].parent_uuid and parent_kept_uuid is not None:
                self.tree_troncated = True
                self.nodes[parent_kept_uuid].children.append(uuid)

            # If parent_kept_uuid is None, and parent_uuid was not, add to root list
            if self.nodes[uuid].parent_uuid is not None and parent_kept_uuid is None:
                self.tree_troncated = True
                self.roots.append(uuid)

            # Modify parent uuid
            self.nodes[uuid].parent_uuid = parent_kept_uuid

        for child in children:
            self.recursive_filter(child, new_parent_kept_uuid)


    def node_filter_not_inheritable_is_kept(self, uuid):
        # Export Camera or not
        if self.nodes[uuid].blender_type == VExportNode.CAMERA:
            if self.export_settings[gltf2_blender_export_keys.CAMERAS] is False:
                return False

        # Export Lamp or not
        if self.nodes[uuid].blender_type == VExportNode.LIGHT:
            if self.export_settings[gltf2_blender_export_keys.LIGHTS] is False:
                return False

        # Export deform bones only
        if self.nodes[uuid].blender_type == VExportNode.BONE:
            if self.export_settings['gltf_def_bones'] is True and self.nodes[uuid].use_deform is False:
                # Check if bone has some objected parented to bone. We need to keep it in that case, even if this is not a def bone
                if len([c for c in self.nodes[uuid].children if self.nodes[c].blender_type != VExportNode.BONE]) != 0:
                    return True
                return False

        return True

    def node_filter_inheritable_is_kept(self, uuid):

        if self.export_settings[gltf2_blender_export_keys.SELECTED] and self.nodes[uuid].blender_object.select_get() is False:
            return False

        if self.export_settings[gltf2_blender_export_keys.VISIBLE]:
            # The eye in outliner (object)
            if self.nodes[uuid].blender_object.visible_get() is False:
                return False

            # The screen in outliner (object)
            if self.nodes[uuid].blender_object.hide_viewport is True:
                return False

            # The screen in outliner (collections)
            if all([c.hide_viewport for c in self.nodes[uuid].blender_object.users_collection]):
                return False

        # The camera in outliner (object)
        if self.export_settings[gltf2_blender_export_keys.RENDERABLE]:
            if self.nodes[uuid].blender_object.hide_render is True:
                return False

            # The camera in outliner (collections)
            if all([c.hide_render for c in self.nodes[uuid].blender_object.users_collection]):
                return False

        if self.export_settings[gltf2_blender_export_keys.ACTIVE_COLLECTION]:
            found = any(x == self.nodes[uuid].blender_object for x in bpy.context.collection.all_objects)
            if not found:
                return False

        return True

    def search_missing_armature(self):
        for n in [n for n in self.nodes.values() if hasattr(n, "armature_needed") is True]:
            candidates = [i for i in self.nodes.values() if i.blender_type == VExportNode.ARMATURE and i.blender_object.name == n.armature_needed]
            if len(candidates) > 0:
                n.armature = candidates[0].uuid
            del n.armature_needed

    def add_neutral_bones(self):
        for n in [n for n in self.nodes.values() if n.armature is not None and n.blender_type == VExportNode.OBJECT and hasattr(self.nodes[n.armature], "need_neutral_bone")]: #all skin meshes objects where neutral bone is needed
            # First add a new node

            axis_basis_change = Matrix.Identity(4)
            if self.export_settings[gltf2_blender_export_keys.YUP]:
                axis_basis_change = Matrix(((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))

            trans, rot, sca = axis_basis_change.decompose()
            translation, rotation, scale = (None, None, None)
            if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
                translation = [trans[0], trans[1], trans[2]]
            if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0:
                rotation = [rot[1], rot[2], rot[3], rot[0]]
            if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
                scale = [sca[0], sca[1], sca[2]]
            neutral_bone = gltf2_io.Node(
                            camera=None,
                            children=None,
                            extensions=None,
                            extras=None,
                            matrix=None,
                            mesh=None,
                            name='neutral_bone',
                            rotation=rotation,
                            scale=scale,
                            skin=None,
                            translation=translation,
                            weights=None
                        )
            # Add it to child list of armature
            self.nodes[n.armature].node.children.append(neutral_bone)
            # Add it to joint list
            n.node.skin.joints.append(neutral_bone)

            # Need to add an InverseBindMatrix
            array = BinaryData.decode_accessor_internal(n.node.skin.inverse_bind_matrices)

            axis_basis_change = Matrix.Identity(4)
            if self.export_settings[gltf2_blender_export_keys.YUP]:
                axis_basis_change = Matrix(
                    ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))

            inverse_bind_matrix = (
                axis_basis_change @ self.nodes[n.armature].matrix_world_armature).inverted_safe()

            matrix = []
            for column in range(0, 4):
                for row in range(0, 4):
                        matrix.append(inverse_bind_matrix[row][column])

            array = np.append(array, np.array([matrix]), axis=0)
            binary_data = gltf2_io_binary_data.BinaryData.from_list(array.flatten(), gltf2_io_constants.ComponentType.Float)
            n.node.skin.inverse_bind_matrices = gltf2_blender_gather_accessors.gather_accessor(
                binary_data,
                gltf2_io_constants.ComponentType.Float,
                len(array.flatten()) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4),
                None,
                None,
                gltf2_io_constants.DataType.Mat4,
                self.export_settings
            )
    def get_unused_skins(self):
        from .gltf2_blender_gather_skins import gather_skin
        skins = []
        for n in [n for n in self.nodes.values() if n.blender_type == VExportNode.ARMATURE]:
            if len([m for m in self.nodes.values() if m.keep_tag is True and m.blender_type == VExportNode.OBJECT and m.armature == n.uuid]) == 0:
                skin = gather_skin(n.uuid, self.export_settings)
                skins.append(skin)
        return skins

    def variants_reset_to_original(self):
        # Only if Variants are displayed and exported
        if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False:
            return
        objects = [self.nodes[o].blender_object for o in self.get_all_node_of_type(VExportNode.OBJECT) if self.nodes[o].blender_object.type == "MESH" \
            and self.nodes[o].blender_object.data.get('gltf2_variant_default_materials') is not None]
        for obj in objects:
            # loop on material slots ( primitives )
            for mat_slot_idx, s in enumerate(obj.material_slots):
                # Check if there is a default material for this slot
                for i in obj.data.gltf2_variant_default_materials:
                    if i.material_slot_index == mat_slot_idx:
                        s.material = i.default_material
                        break

            # If not found, keep current material as default
