# TODO: currently that is still in __init__

from ..types.timeline.timeline_serializer import serializeTimeline
from .asset_utils import exportAsset
from ..utils_debug import debug_err, debug_warn
from .NEEDLE_components_postprocess import exportPostprocessing, exportExtraComponents, postProcessComponents
from ..panels_object import hasInstancingEnabled
from .reference_resolver import CurrentObject, ReferenceResolver
from .NEEDLE_components_unknown import addUnknownComponent, foreachUnknownComponent, resolveImportedJsonPointersToObjects
from .NEEDLE_components_importer import ComponentImportContext, deserializeComponent
from .NEEDLE_components_export import ComponentExportContext, DeferredResolveInfo
from .extensionbase import ExtensionBase
from ..blender_windows import get_context_window
import bpy
import mathutils
from ..component_types import ComponentReference, getBlenderPropertyKey
from ..panels_components import getComponentKey, getIsActive
from ..component_registry import isMissing
from ..component_utils import addComponentToObject, fromMethodInfo, getComponent, getComponentGuid, getGuid
from ..component_registry import *
from ..blender_export import registerDependency, reportError
from ..lightmapping.lightmapping import getLightmapScaleOffset, isLightmapped

class DontSerialize():
    pass



class NEEDLE_components(ExtensionBase):
    def __init__(self, ext):
        self.ext = ext
        self.extrasObject = None
        pass

    #################################################
    ## EXPORT

    def onBeforeExport(self, exportContext):
        super().onBeforeExport(exportContext)
        # creating an extra components object here in the root export context
        # this is for exporting e.g. menu items and postprocessing settings
        if self.exportContext.isRootExport() and not self.exportContext.exportOnlyComponentsInScene:
            self.extrasObject = bpy.data.objects.new("Needle Extras", None)
            bpy.context.collection.objects.link(self.extrasObject)

    def onAfterExport(self, exportContext):
        # remove the empty object
        if self.extrasObject is not None:
            bpy.data.objects.remove(self.extrasObject)
        pass

    def gather_asset_hook(self, gltf2_asset, export_settings):
        self.nodesCounter = 0
        self.componentExportContext = ComponentExportContext(self.getExportSettings())
        self.referenceResolver = ReferenceResolver(self.componentExportContext)
        self.export_unknown_components = []
        self.__firstNodeExport = True

    def gather_joint_hook(self, gltf2_node, blender_bone, export_settings):
        self.componentExportContext.nodes.append(blender_bone)
        pass
    
    def gather_node_hook(self, gltf2_node, blender_object, node):
        super().gather_node_hook(gltf2_node, blender_object, node)

        # TODO: do this in a later hook, maybe in the scene hook to make sure we have all nodes collected

        self.componentExportContext.nodes.append(blender_object)

        objType = blender_object.type
        nodeId = self.nodesCounter
        self.nodesCounter += 1

        # loop over all known component types
        components = []
        schemes = getSchemes()
        for componentName in getTypes():
            name = componentName
            if isMissing(name):
                continue
            key = getComponentKey(componentName)
            if key in blender_object:
                # check if component is actually active
                blenderComponent = getattr(blender_object, key)
                if not getIsActive(blenderComponent):
                    continue
                # add component
                component = {}
                components.append(component)
                component["name"] = "" # the final value will be written in the end to ensure it's not overriden. But we insert it here already so it's at the start of the json
                component["guid"] = guid = getGuid(blenderComponent) # str(id(blenderComponent))
                component["gameObject"] = {"node": nodeId}
                schema = schemes[componentName]
                print("> Export component", name, guid, "on " + blender_object.name)

                # write properties
                for kvp in schema.items():
                    propertyName = kvp[0]
                    # print(propertyName, schema)
                    if(propertyName.startswith("internal__")):
                        continue

                    scheme = schema[propertyName]
                    propertyType = ""
                    if "type" in scheme:
                        propertyType = scheme.get("type")

                    blender_property_name = getBlenderPropertyKey(propertyName)

                    # check if attribute exists
                    propertyValue = None
                    if hasattr(blenderComponent, blender_property_name):
                        propertyValue = getattr(blenderComponent, blender_property_name)

                    propertyValue = self.serializeProperty(blender_object, component, blenderComponent, scheme, propertyType, propertyName, propertyValue, CurrentObject(component, propertyName))
                    if propertyValue is not None:
                        component[propertyName] = propertyValue

                # we currently identify component types using the name field only
                # this might change in the future when we include namespaces or package information
                component["name"] = str(name)

        for unknown in foreachUnknownComponent(blender_object):
            if unknown is None:
                continue
            unknown_comp = unknown["component"]
            unknown_comp["gameObject"] = {"node": nodeId}
            print("Export unknown component", unknown_comp["name"], unknown_comp)
            # save the components to be resolved later
            self.export_unknown_components.append(unknown)
            components.append(unknown_comp)


        postProcessComponents(self, blender_object, components)

        if blender_object == self.extrasObject:
            exportPostprocessing(blender_object, components)
            exportExtraComponents(self, blender_object, components);

        # add the components extension
        gltf2_node.extensions["NEEDLE_components"] = self.ext.Extension(
            name="NEEDLE_components",
            extension={"builtin_components": components},
            required=False
        )
        pass

    def gather_material_hook(self, gltf2_material, blender_material, export_settings):
        if not blender_material:
            return
        
        self.componentExportContext.register_exported(blender_material, gltf2_material)

        if not blender_material in self.componentExportContext.materials:
            self.componentExportContext.materials.append(blender_material)

    def gather_mesh_hook(self, gltf2_mesh, blender_mesh, blender_object, vertex_groups, modifiers, materials, export_settings):
    # def gather_mesh_hook(self, gltf2_mesh, blender_mesh, blender_object, vertex_groups, modifiers, skip_filter, materials, export_settings):
        if not blender_mesh in self.componentExportContext.meshes:
            self.componentExportContext.meshes.append(blender_mesh)

    def gather_texture_hook(self, gltf2_texture, blender_shader_sockets, export_settings):
        if not gltf2_texture in self.componentExportContext.textures:
            self.componentExportContext.textures.append(gltf2_texture)
        # self.componentExportContext.textures.append(blender_shader_sockets)
        # TODO: what do we need to serialize for a texture? what reference do we reference on import for textures in unknown components
        pass

    # animation callback 3.6
    def gather_actions_hook(self, blender_object, params, export_settings):
        if not hasattr(params, "blender_actions"):
            return
        list = getattr(params, "blender_actions", None)
        for blender_action in list:
            if not blender_action in self.componentExportContext.animations:
                self.componentExportContext.animations.append(blender_action)
        pass
    # animation callback < 3.6
    def gather_animation_hook(self, gltf2_animation, blender_action, blender_object, export_settings):
        if not blender_action in self.componentExportContext.animations:
            self.componentExportContext.animations.append(blender_action)

    def gather_scene_hook(self, gltf2_scene, blender_scene, export_settings):
        self.register_referenced_objects()
        pass

    def gather_gltf_extensions_hook(self, gltf2_plan, export_settings):
        if(len(gltf2_plan.cameras) <= 0):
            debug_warn("No camera found in scene...")
            pass
        self.componentExportContext.finalize(gltf2_plan)
        pass

    def register_referenced_objects(self):
        # resolve pointers of unknown components
        if self.export_unknown_components is not None and len(self.export_unknown_components) > 0:
            for comp in self.export_unknown_components:
                resolveImportedJsonPointersToObjects(self.componentExportContext, comp, self.referenceResolver)
            self.export_unknown_components.clear()


    # core method for serialization component properties
    def serializeProperty(self, blender_object, serializedComponent, blenderComponent, componentScheme, propertyType, propertyName, propertyValue, currentObject:CurrentObject = None) -> object or None:

        if(propertyName.startswith("internal__")):
            return None

        if propertyType == "label":
            return None

        if propertyType == "videoclip" or propertyType == "audioclip" or propertyType == "imagereference":
            if propertyValue is None:
                return None
            relPath = exportAsset(propertyValue, self.exportContext)
            if relPath is None:
                debug_warn("Exporting Asset failed (it may be missing on disc or the embedded file can not be exported?)", propertyType, propertyName)
            return relPath
        
        # see if any of these things are RESOURCES (e.g. materials, meshes or textures) that should be ensured to be exported with the glb
        # TODO does also catch imagereference but shouldn't
        if currentObject is not None:
            # TODO: "texture" types are currently images but should emit a texture in the gltf
            pointerKeyBase = self.referenceResolver.tryGetJsonPointerBase(propertyValue, propertyType)
            if pointerKeyBase is not None:
                try:
                    res = self.referenceResolver.tryResolve(propertyValue, pointerKeyBase, currentObject=currentObject)
                    # MAKE SURE to register the DeferredResolveInfo with the correct object and key (that we serialized) so that we can resolve it later
                    if isinstance(res, DeferredResolveInfo):
                        return res.gltf_object
                except Exception as e:
                    debug_err("Error resolving pointer for property \"" + propertyName + "\"", pointerKeyBase, e)
                    reportError("Error in extension NEEDLE_components: " + propertyName)
                    return None
        
        # handle serializing unknown component type
        # unknown component references will just be stored as a stringified python dictionary containing the guid only
        # e.g. {'guid': '55354453534'}
        if isinstance(propertyValue, str):
            if propertyValue.startswith("{'guid': '"):
                obj = eval(propertyValue)
                return obj

        # check if propertyValue has a serialize method
        serializeCall = getattr(propertyValue, "serialize", None)
        if serializeCall is not None and callable(serializeCall):
            return serializeCall(self)

        if propertyType == "timeline":
            return serializeTimeline(self, blender_object, blenderComponent)
        
        if propertyType == "enum_flag":
            resulting_value = 0
            for enum in propertyValue:
                valueAsInt = int(enum)
                # combine enum bitshifted values (e.g. 1, 4, 16)
                resulting_value = resulting_value | valueAsInt
            return resulting_value
            
        # type evt is a EventList
        if propertyType == "evt":
            calls = []
            if propertyValue is not None:
                callsArray = getattr(propertyValue, "array")
                for call in callsArray:
                    callInfo = getattr(call, "value")
                    # # get the method info and target object and component
                    targetObject = getattr(callInfo, "obj")
                    methodInfo = getattr(callInfo, "method")
                    componentFullNameAndMethod = fromMethodInfo(methodInfo)
                    if(len(componentFullNameAndMethod) < 2):
                        continue
                    targetComponentName = componentFullNameAndMethod[0]
                    targetComponentMethod = componentFullNameAndMethod[1]
                    # # build new call
                    call = {}
                    call["enabled"] = True
                    # here we need to construct the guid of the component
                    call["target"] = getComponentGuid(targetObject, targetComponentName) #"2397247438608"# propertyValue.get("guid")
                    # get the method we want to call
                    call["method"] = targetComponentMethod

                    args = callInfo.args
                    serializedArguments = []
                    for arg in args:
                        res = arg.serialize()
                        if res != None:
                            serializedArguments.append(res)
                    if len(serializedArguments) > 1:
                        call["arguments"] = serializedArguments
                    elif len(serializedArguments) == 1:
                        call["argument"] = serializedArguments[0]

                    # add it to the array :)
                    calls.append(call)
                    # print("Add call", call)
            return { "type" : "EventList", "calls" : calls }   

        if isinstance(propertyValue, bpy.types.Scene):
            res = registerDependency(propertyValue)
            if res is not None:
                return res
            else:
                return None
        
        # check if the type is some generic array
        if isinstance(propertyValue, bpy.types.bpy_struct):
            if hasattr(propertyValue, "internal__array_index"):
                array = getattr(propertyValue, "array")
                res = self.serializeProperty(blender_object, serializedComponent, blenderComponent, componentScheme, propertyType, propertyName, array)
                return res
        
        # check if the type is some collection
        if isinstance(propertyValue, bpy.types.bpy_prop_collection):
            res = []
            # print("EXPORT LIST", propertyName, propertyValue)
            currentListObject = CurrentObject(res, 0)
            for item in propertyValue:
                # if a list item is a primitive it has a value field
                itemValue = getattr(item, "value", None)
                index = len(res)
                currentListObject.key = index
                if itemValue is not None:
                    serializedItem = self.serializeProperty(blender_object, serializedComponent, blenderComponent, componentScheme, propertyType, propertyName, itemValue, currentListObject)
                    if serializedItem == DontSerialize:
                        continue
                    res.append(serializedItem)
                # otherwise we assume the list entry is a complex object and we export all the keys for each entry
                else:
                    keys = item.keys()
                    obj = None
                    for key in keys:
                        try:
                            value = getattr(item, key, None)
                            if value is None:
                                continue
                            curObj = CurrentObject(obj, key)
                            curType = propertyType[key]["type"]
                            serializedProp = self.serializeProperty(blender_object, serializedComponent, blenderComponent, componentScheme, curType, key, value, curObj)
                            if serializedProp == DontSerialize:
                                continue
                            if obj == None: obj = {}
                            obj[key] = serializedProp
                        except Exception as e:
                            debug_err("Error serializing property \"" + key + "\"", e)
                            reportError("Error in extension NEEDLE_components: \"" + key + "\"")
                        pass
                    # we don't want to serialize an object without data (e.g. obj = {})
                    if obj is not None:
                        res.append(obj)
                    pass

            return res

        if isinstance(propertyValue, bpy.types.Object):
            # export as guid if the referenced thing is a blender object
            if currentObject is not None:
                self.componentExportContext.registerNodeReference(propertyValue, currentObject.obj, currentObject.key)
            return {"guid": getGuid(propertyValue)}
        
        elif isinstance(propertyValue, mathutils.Color):
            return {"r": propertyValue.r,
                             "g": propertyValue.g, "b": propertyValue.b}
        
        elif isinstance(propertyValue, mathutils.Vector):
            if len(propertyValue) == 4:
                return {"x": propertyValue.x,
                             "y": propertyValue.y, "z": propertyValue.z, "w": propertyValue.w}
            elif len(propertyValue) == 3:
                return {"x": propertyValue.x,
                             "y": propertyValue.y, "z": propertyValue.z}  
            elif len(propertyValue) == 2:
                return {"x": propertyValue.x,
                             "y": propertyValue.y}  
        elif isinstance(propertyValue, mathutils.Quaternion):
            return {"x": propertyValue.x, "y": propertyValue.y, "z": propertyValue.z, "w": propertyValue.w}
           
        # a animation field - here we just save a reference to the component field and the animation to resolve it when the animations are written out
        # we do it this way to get the correct order / write out the animation pointer correctly
        if propertyType == "anim" or propertyType == "animationclip":
            if hasattr(blenderComponent, propertyName):
                animation = getattr(blenderComponent, propertyName)
                # the object is an array of actions
                if hasattr(animation, "array"):
                    array = getattr(animation, "array")
                    actions = []
                    if len(array) > 0:
                        entry = array[0]
                        value = getattr(entry, "value")
                        actions.append(value)
                    self.registerAnimation(serializedComponent, propertyName, actions)
                    return DontSerialize
                # the object is an action
                if animation is not None and isinstance(animation, bpy.types.Action):
                    # self.ensureAnimationIsExported(blender_object, animation)
                    self.registerAnimation(serializedComponent, propertyName, animation)
            # the property is a single action
            elif hasattr(blender_object, "animation_data") and blender_object.animation_data != None and blender_object.animation_data.action != None:
                action = blender_object.animation_data.action
                # self.ensureAnimationIsExported(blender_object, action)
                self.registerAnimation(serializedComponent, propertyName, action)
            else:
                print("No animation data")   
            return ""
        
        # type fn is a function on a component
        elif propertyType == "fn": 
            # but we dont want to emit/serialize functions in the gltf
            return None
        
        elif propertyType == "enum":
            return int(propertyValue)
         
        # serialize a component reference
        elif propertyType == "comp" or isinstance(propertyValue, ComponentReference):
            # the propertyValue is expected to be a ComponentReference type
            res = {}
            obj = getattr(propertyValue, "obj")
            selectedComponent = getattr(propertyValue, "component")
            guid = getComponentGuid(obj, selectedComponent)
            return { "guid": guid }

        # check type
        elif isinstance(propertyValue, bpy.types.bpy_struct):
            obj = {}
            # print(propertyName, componentScheme)
            for propName in componentScheme.keys():
                if propName.startswith("internal__"):
                    continue
                propScheme = componentScheme[propName]
                if hasattr(propertyValue, propName):
                    value = getattr(propertyValue, propName)
                    propType = ""
                    if "type" in propScheme:
                        propType = propScheme["type"]
                    # TODO: instance should be the instance of the property
                    obj[propName] = self.serializeProperty(blender_object, serializedComponent, obj, propScheme, propType, propName, value, CurrentObject(obj, propName))
            return obj
        return propertyValue



    #################################################
    ## IMPORT
    
    def gather_import_scene_before_hook(self, gltf_scene, blender_scene, gltf):
        self.imported = {}
        self.node_objects = []
        self.material_objects = {}
        self.unknown_components = []
        pass


    def gather_import_node_after_hook(self, vnode, gltf_node, blender_object, gltf):
        self.node_objects.append(blender_object)
        if not gltf_node.extensions:
            return
        if not "NEEDLE_components" in gltf_node.extensions:
            return
        ext = gltf_node.extensions["NEEDLE_components"]
        array = ext["builtin_components"]
        for component in array:
            componentName = component["name"]
            print("> Load component " + componentName)
            componentFullName = addComponentToObject(blender_object, componentName)
            if componentFullName != None:
                self.imported[componentFullName] = { 
                    "component_name" : componentName, 
                    "component_data": component, 
                    "blender_object": blender_object
                    }
            else:
                print("WARN: Component not found", componentName)
                self.unknown_components.append({
                    "component_name": componentName,
                    "component_data": component,
                    "blender_object": blender_object,
                })
        pass


    def gather_import_material_before_hook(self, gltf_material, vertex_color, gltf):
        pass

    def gather_import_material_after_hook(self, gltf_material, vertex_color, blender_mat, gltf):
        data = gltf.data
        gltf_materials = data.materials
        gltf_material_index = gltf_materials.index(gltf_material)
        self.material_objects[gltf_material_index] = blender_mat
        pass


    def gather_import_scene_after_nodes_hook(self, gltf_scene, blender_scene, gltf):
        context = ComponentImportContext(self)
        context.gltf = gltf
        context.nodes = self.node_objects
        context.materials = self.material_objects
        context.resolver = ReferenceResolver(context)

        self.ensureValuesAreCreated(context)

        for unknown in self.unknown_components:
            componentName = unknown["component_name"]
            componentData = unknown["component_data"]
            blenderObject = unknown["blender_object"]
            addUnknownComponent(blenderObject, componentData, context)

        for componentFullName in self.imported:
            value = self.imported[componentFullName]
            context.component_name = value["component_name"]
            context.blender_object = value["blender_object"]
            context.component_data = value["component_data"]
            context.component_schema = getScheme(context.component_name)
            context.component = getComponent(context.blender_object, componentFullName)
            deserializeComponent(context)
        self.imported.clear()
        pass


    def ensureValuesAreCreated(self, context : ComponentImportContext):
        # this could also be done when requested for the first time
        gltf = context.gltf
        data = gltf.data
        materialIndex = 0
        for _gltf_mat in data.materials:
            hasMaterial = materialIndex in context.materials
            if not hasMaterial:
                createMaterial(gltf, materialIndex)
            materialIndex += 1


if bpy.app.version >= (4, 3, 0):
    from io_scene_gltf2.blender.imp.material import BlenderMaterial
else:
    from io_scene_gltf2.blender.imp.gltf2_blender_material import BlenderMaterial
def createMaterial(gltf, index):
    # blender vertex color seems to be 0 by default ?!
    has_vertex_color = False
    BlenderMaterial.create(gltf, index, has_vertex_color)

