import datetime
import typing

from ..utils_debug import debug_log, debug_warn
from ..blender_materials import createMaterialWithTexture
from ..blender_export import reportError, reportWarning
import bpy

if bpy.app.version >= (4, 3, 0):
    from io_scene_gltf2.blender.exp.material.search_node_tree import NodeSocket

elif bpy.app.version >= (4, 1, 0):
    from io_scene_gltf2.blender.exp.material.gltf2_blender_search_node_tree import NodeSocket

if bpy.app.version >= (4, 3, 0):
    from io_scene_gltf2.blender.exp.material.materials import gather_material
    from io_scene_gltf2.blender.exp.material.texture import gather_texture
    from io_scene_gltf2.blender.exp.material.image import gather_image
    from io_scene_gltf2.blender.exp.animation.animations import gather_animations
elif bpy.app.version >= (3, 6, 0):
    from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_materials import gather_material
    from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_texture import gather_texture
    from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_image import gather_image
    from io_scene_gltf2.blender.exp.animation.gltf2_blender_gather_animations import gather_animations
else:
    from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials import gather_material
    from io_scene_gltf2.blender.exp.gltf2_blender_gather_texture import gather_texture
    from io_scene_gltf2.blender.exp.gltf2_blender_gather_image import gather_image
    from io_scene_gltf2.blender.exp.gltf2_blender_gather_animations import gather_animations

if bpy.app.version >= (4, 3, 0):
    from io_scene_gltf2.blender.exp.mesh import gather_mesh
    from io_scene_gltf2.blender.exp.primitives import gather_primitives
else:
    from io_scene_gltf2.blender.exp.gltf2_blender_gather_mesh import gather_mesh
    from io_scene_gltf2.blender.exp.gltf2_blender_gather_primitives import gather_primitives


class DeferredObjectExportInfo:
    def __init__(self, blender_object: bpy.types.Object, export_settings: dict):
        self.blender_object = blender_object
        self.export_settings = export_settings
        
class DeferredResolveInfo:
    """ registered asset to be resolved later (call ComponentExportContext.register with it) - the gltf_object should be added to the extension"""
    def __init__(self, gltf_key:str, path_base:str, gltf_object:object|DeferredObjectExportInfo):
        self.gltf_key = gltf_key
        self.path_base = path_base
        self.gltf_object = gltf_object
        # filled in when the object is registered
        self.target_object = None
        self.target_key = None

class DeferredObjectResolveInfo:
    def __init__(self, blender_object: bpy.types.Object, target_object:object, key:str or int):
        self.blender_object = blender_object
        self.target_object = target_object
        self.target_key = key


class ComponentExportContext:
    def __init__(self, exportSettings):
        self.exportSettings = exportSettings
        self.nodes = []
        self.materials = []
        self.meshes = []
        self.textures = []
        self.images = []
        self.animations = []
        self.defaultMaterialForExportingImage : bpy.types.Material or None = None

        self.__defered_export_objects : list[DeferredObjectExportInfo] = []
        """list of objects to be exported at the end of the export process so we know if the default blender exporter has exported them already"""

        self.__deferred_resolve : list[DeferredResolveInfo] = []  
        """^ contains a list of DeferredResolveInfo objects"""
        self.__deferred_node_references : list[DeferredObjectResolveInfo] = []
        """^ contains a info """

        self.previously_exported_assets = {}
        """register blender_asset -> gltf_asset here to avoid exporting the same asset multiple times"""

    def register_exported(self, asset, gltf_object):
        gltf_key = ""
        # if it's an material
        if isinstance(asset, bpy.types.Material):
            gltf_key = "materials"
        # if it's an image
        elif isinstance(asset, bpy.types.Image):
            gltf_key = "images"
        # if it's an texture
        elif isinstance(asset, bpy.types.Texture):
            gltf_key = "textures"
        # if it's an mesh
        elif isinstance(asset, bpy.types.Mesh):
            gltf_key = "meshes"
        if gltf_key == "":
            debug_warn("WARN: unknown asset type for registration: " + str(gltf_object))
            return False
        self.__register_exported(asset, gltf_key, gltf_object)
        return

    def __register_exported(self, asset, gltf_key, gltf_object):
        """
        Register the exported object to avoid exporting it multiple times
        gltf_key is e.g. "materials" or "textures"
        """
        dict = None

        # get the concrete datablock if this is bpy_struct
        if asset.is_evaluated:
            asset = asset.original

        if asset not in self.previously_exported_assets:
            dict = self.previously_exported_assets[asset] = {}
        else:
            dict = self.previously_exported_assets[asset]
        dict[gltf_key] = gltf_object
        pass

    def __try_get_exported(self, asset, gltf_key):
        
        if asset not in self.previously_exported_assets:
            print("INFO: asset not found in previously_exported_assets: " + asset.name)
            return None
        
        # get the concrete datablock if this is bpy_struct
        if asset.is_evaluated:
            asset = asset.original

        dict = self.previously_exported_assets[asset]
        if dict is None:
            return None
        if gltf_key not in dict:
            return None
        return dict[gltf_key]

    # if an object is not exported but referenced by a json pointer we want to call the gltf exporter method to generate the gltf-structure
    def exportAsset(self, asset, gltf_key, allowDeferExport:bool = True) -> object | DeferredObjectExportInfo | None:
        """
        asset: the blender object to export
        gltf_key: the key in the gltf structure where the object should be added
        allowDeferExport: if true the object will be exported during the finalize method and this method will return a DeferredObjectExportInfo if possible
        """

        if asset is None:
            print("ERR: try_export_missing_object: asset is None")
            return False
        
        if gltf_key is None:
            print("ERR: try_export_missing_object: gltf_key is None")
            return False
        
        # check if we already exported this asset:
        prev_exported = self.__try_get_exported(asset, gltf_key)
        if prev_exported:
            print("> Resolved previously exported \"" + gltf_key + "\" asset: " + asset.name)
            return prev_exported
        # TODO: ^ this does not cover cases where the regular glTF exporter has exported an asset already (e.g. a material)
        
        export_settings = self.exportSettings
        # if we pass in the same export settings we get the previously resolved asset per gather_X hook
        cloned_export_settings = export_settings.copy()
        cloned_export_settings["timestamp"] = datetime.datetime.now()

        # https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py#L48
        if isinstance(asset, bpy.types.Material):

            # defer export for materials because default blender exporter might export them
            # this will be resolved in the finalize method
            if allowDeferExport:
                return DeferredObjectExportInfo(asset, cloned_export_settings);

            # material export works but seems to produce wrong results / colors in some cases.
            if bpy.app.version >= (4, 0, 0):
                mat = gather_material(asset, cloned_export_settings)[0]
            else:
                mat = gather_material(asset, None, cloned_export_settings)
            self.__register_exported(asset, gltf_key, mat)
            return mat

        # https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py#L28
        # https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py#L31
        elif isinstance(asset, bpy.types.Image) or isinstance(asset, bpy.types.Texture):

            # defer export for textures and images because default blender exporter might export them
            # this will be resolved in the finalize method
            if allowDeferExport:
                return DeferredObjectExportInfo(asset, cloned_export_settings);

            # make sure we have material and the image is in the material
            if self.defaultMaterialForExportingImage is None:
                if "NEEDLE_defaultMaterialForExportingImage" in bpy.data.materials:
                    self.defaultMaterialForExportingImage = bpy.data.materials["NEEDLE_defaultMaterialForExportingImage"]
                else:
                    self.defaultMaterialForExportingImage = createMaterialWithTexture("NEEDLE_defaultMaterialForExportingImage", asset)
            # assign image
            self.defaultMaterialForExportingImage.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (1,1,1,1)
            self.defaultMaterialForExportingImage.node_tree.nodes["Image Texture"].image = asset
            if bpy.app.version >= (4, 1, 0):
                blender_shader_sockets : typing.Tuple[bpy.types.NodeSocket] = (NodeSocket(self.defaultMaterialForExportingImage.node_tree.nodes["Image Texture"].outputs["Color"], self.defaultMaterialForExportingImage),)
            else:
                blender_shader_sockets : typing.Tuple[bpy.types.NodeSocket] = (self.defaultMaterialForExportingImage.node_tree.nodes["Image Texture"].outputs["Color"],)

            if gltf_key == "textures" or isinstance(asset, bpy.types.Texture):
                if bpy.app.version >= (4, 0, 0):
                    res = gather_texture(blender_shader_sockets, False, cloned_export_settings)
                else:
                    res = gather_texture(blender_shader_sockets, export_settings=cloned_export_settings)
                gltf_texture = res[0]
                self.__register_exported(asset, gltf_key, gltf_texture)
                if not gltf_texture:
                    debug_warn("WARN: could not export texture for " + asset.name)
                    return None
                if gltf_texture.name is None:
                    gltf_texture.name = asset.name
                return gltf_texture
            
            if bpy.app.version >= (4, 0, 0):
                res = gather_image(blender_shader_sockets, None, cloned_export_settings)
            else:
                res = gather_image(blender_shader_sockets, export_settings)
            self.__register_exported(asset, gltf_key, res[0])
            return res[0]
            
        # https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py#L55
        # primtiive: https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py#L58
        elif isinstance(asset, bpy.types.Mesh):
            # TODO: implement for referenced mesh export support
            debug_log("WARN: exporting referenced mesh is not implemented")
            return

        # https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
        elif isinstance(asset, bpy.types.Action):
            # animations are actually already exported via a workaround :)
            pass

        # https://github.com/KhronosGroup/glTF-Blender-IO/blob/f877995da23c10b8190b9a33ea5f32690485e82d/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py#L36
        elif isinstance(asset, bpy.types.Node):
            # we should not need to register node references here (and it might cause problems as long as we serialize the components in the gather_nodes callback)
            pass

        debug_warn("Try_export_missing_object: unknown asset type: " + str(asset))
        reportWarning("try_export_missing_object: unknown asset type: " + str(asset))
        return False

    def register(self, asset : str | DeferredResolveInfo, object, key):
        # if the instance is a json pointer already we dont need to resolve it later
        if isinstance(asset, DeferredResolveInfo):
            asset.target_object = object
            asset.target_key = key
            self.__deferred_resolve.append(asset)
        pass

    def registerNodeReference(self, blender_object, object, key):
        deferred = DeferredObjectResolveInfo(blender_object, object, key)
        self.__deferred_node_references.append(deferred)
        pass
    
    def finalize(self, gltf):

        # resolve references
        for info in self.__deferred_resolve:
            try:
                # if the object is a DeferredObjectExportInfo we need to export it first
                if isinstance(info.gltf_object, DeferredObjectExportInfo):
                    asset = self.exportAsset(info.gltf_object.blender_object, info.gltf_key, False)
                    info.gltf_object = asset

                gltf_key = info.gltf_key

                # remove the old entry because it will be resolved below
                # and if it can't be resolved we don't want to serialize a DeferredObject
                del info.target_object[info.target_key]
                found = False

                if hasattr(gltf, gltf_key):
                    array = getattr(gltf, gltf_key)
                    # get index of the object in the array
                    for i in range(len(array)):
                        obj = array[i]
                        if info.gltf_object == obj:
                            found = True
                            path = info.path_base + str(i)
                            info.target_object[info.target_key] = path
                            name = hasattr(obj, "name") and obj.name or ""
                            print("< Resolved pointer " + path + " | " + name + "")
                            break
                if found == False:
                    print("ERR: could not resolve " + gltf_key + ": " + info.gltf_object.name)
                    reportWarning("WARN: could not resolve " + gltf_key + ": " + info.gltf_object.name)
            except Exception as e:
                print("ERR: finalizing deferred reference " + str(e))
                reportError("ERR: finalizing deferred reference " + str(e))

        # resolve node references
        try:
            for info in self.__deferred_node_references:
                blender_object = info.blender_object
                object = info.target_object
                key = info.target_key
                if blender_object is None:
                    print("ERR: missing object - can not resolve...")
                    reportError("ERR: missing object - can not resolve...")
                    continue
                resolved = False
                for i in range(len(self.nodes)):
                    node = self.nodes[i]
                    if node == blender_object:
                        # now do we want to store this as a json pointer, a node index only or our custom object?
                        # json pointers work universally, the custom format should be deprecated
                        pointer = "/nodes/" + str(i)
                        print("< Resolved pointer " + pointer + " to object " + blender_object.name + " in '" + str(key) + "'")
                        object[key] = pointer
                        resolved = True
                if resolved == False:
                    print("ERR: could not resolve node for " + blender_object.name)
                    reportError("ERR: could not resolve node for " + blender_object.name)

        except Exception as e:
            print("ERR: finalizing deferred node references " + str(e))
            reportError("ERR: finalizing deferred node references " + str(e))

        
        # remove material
        if self.defaultMaterialForExportingImage is not None:
            bpy.data.materials.remove(self.defaultMaterialForExportingImage)
            self.defaultMaterialForExportingImage = None

    
