import bpy
from .NEEDLE_components_export import ComponentExportContext, DeferredResolveInfo
from .reference_resolver import CurrentObject, FailedToResolve, ReferenceResolver, RegisteredObject
from .NEEDLE_components_importer import ComponentImportContext

class UnknownPointerObject(bpy.types.PropertyGroup):
    # just the name of the field in the script, mainly for debugging
    # fieldName: bpy.props.StringProperty(name="fieldName")
    # the pointer string
    pointer: bpy.props.StringProperty(name="pointer") # e.g. /materials/0
    #used for the resolver to quickly find the right array to find the index in
    pointerKey: bpy.props.StringProperty(name="pointerKey") # e.g. /materials/ ---- 

    obj: bpy.props.PointerProperty(type=bpy.types.Object)
    mat: bpy.props.PointerProperty(type=bpy.types.Material)
    tex: bpy.props.PointerProperty(type=bpy.types.Texture)
    img: bpy.props.PointerProperty(type=bpy.types.Image)
    mesh: bpy.props.PointerProperty(type=bpy.types.Mesh)
    # graph: bpy.props.PointerProperty(type=bpy.types.NodeTree)

    def draw(self, context, layout):
        local_layout = layout.row()
        local_layout.enabled = False
        if self.obj is not None:
            local_layout.prop(self, "obj", text=self.pointer)
        elif self.mat is not None:
            local_layout.prop(self, "mat", text=self.pointer)
        elif self.tex is not None:
            local_layout.prop(self, "tex", text=self.pointer)
        elif self.img is not None:
            local_layout.prop(self, "img", text=self.pointer)
        elif self.mesh is not None:
            local_layout.prop(self, "mesh", text=self.pointer)
        else:
            row = local_layout.row()
            row.label(text=self.pointer)
            row.label(text="<missing>", icon='ERROR')
        pass

    def hasReference(self) -> bool:
        if self.obj is not None:
            return True
        if self.mat is not None:
            return True
        if self.tex is not None:
            return True
        if self.img is not None:
            return True
        if self.mesh is not None:
            return True
        return False

    

# wrapper object for unknown component data
# used to restore the components on export as much as we can
# e.g. we try to resolve the json pointers
class UnknownComponent(bpy.types.PropertyGroup):
    name: bpy.props.StringProperty(name="name")
    # the unknown component as it was serialized in an extension
    # we dont know what it is (at least in blender)
    # we parse it for all json pointers it contains (stored in references in the blender file)
    # to restore it to the new correct pointers if the file gets exported again
    json: bpy.props.StringProperty(name="json")
    # storing the pointer + object in blender
    # this is what is being resolved on export to get the correct json pointer again
    # since the order of things might change on export
    references: bpy.props.CollectionProperty(type=UnknownPointerObject)



def register():
    unregister()
    bpy.utils.register_class(UnknownPointerObject)
    bpy.utils.register_class(UnknownComponent)
    bpy.types.Object.NEEDLE_unknown_components = bpy.props.CollectionProperty(type=UnknownComponent)

def unregister():
    try:
        bpy.utils.unregister_class(UnknownPointerObject)
        bpy.utils.unregister_class(UnknownComponent)
    except:
        pass
    # del bpy.types.Object.NEEDLE_unknown_components


def addObject(prop : UnknownPointerObject, obj):
    name = prop.name
    if obj is None:
        return
    elif isinstance(obj, bpy.types.Object):
        prop.name = "obj:" + obj.name
        prop.obj = obj
    elif isinstance(obj, bpy.types.Material):
        prop.name = "mat:" + obj.name
        prop.mat = obj
    elif isinstance(obj, bpy.types.Texture):
        prop.name = "tex:" + obj.name
        prop.tex = obj
    elif isinstance(obj, bpy.types.Image):
        prop.name = "img:" + obj.name
        prop.img = obj
    elif isinstance(obj, bpy.types.Mesh):
        prop.name = "mesh:" + obj.name
        prop.mesh = obj
    return

def getObject(prop: UnknownPointerObject):
    if(prop.pointer == ""):
        return None
    elif prop.pointer.startswith("/materials/"):
        return prop.mat
    elif prop.pointer.startswith("/textures/"):
        return prop.tex
    elif prop.pointer.startswith("/images/"):
        return prop.img
    elif prop.pointer.startswith("/meshes/"):
        return prop.mesh
    elif prop.pointer.startswith("/nodes/"):
        return prop.obj




# recursively parse object to find all json pointers
# get the objects they are pointing to and store them in the blender file
def import_recursive_collect_data(prop : UnknownComponent, model, context : ComponentImportContext, currentObject:CurrentObject = None):

    if isinstance(model, dict):
        currentObject = CurrentObject(model, "")
        for key in model:
            value = model[key]
            currentObject.key = key

            # the guid will not change, just serialize it
            if key == "guid" and isinstance(value, str): 
                continue

            elif key == "node" and isinstance(value, int):
                if value >= 0 and value < len(context.nodes):
                    pointer = "/nodes/" + str(value)
                    for existing in prop.references:
                        if existing.pointer == pointer:
                            return
                    blender_object = context.nodes[value]
                    newPointer = prop.references.add()
                    newPointer.name = blender_object.name
                    newPointer.pointer = pointer
                    newPointer.pointerKey = "/nodes/"
                    addObject(newPointer, blender_object)
                pass

            import_recursive_collect_data(prop, value, context, currentObject)

    elif isinstance(model, list):
        currentObject = CurrentObject(model, 0)
        for i in range(len(model)):
            item = model[i]
            currentObject.key = i
            import_recursive_collect_data(prop, item, context, currentObject)
    
    elif isinstance(model, str):
        # dont add it again if it already exists
        for existing in prop.references:
            if existing.pointer == model:
                return
        res = context.resolver.tryResolve(model)
        if res == FailedToResolve:
            print("ERR: Failed to resolve json pointer: " + model)
            newPointer = prop.references.add()
            newPointer.name = "Missing"
            newPointer.pointer = model
            newPointer.pointerKey = ""
            # if currentObject:
            #     currentValue = currentObject.obj[currentObject.key]
            #     currentObject.obj[currentObject.key] = "missing:" + currentValue
            return
        if isinstance(res, RegisteredObject):
            newPointer = prop.references.add()
            newPointer.name = res.blender_object.name
            newPointer.pointer = model
            newPointer.pointerKey = res.key
            addObject(newPointer, res.blender_object)
        else:
            print("Could not resolve json pointer: " + model)


def addUnknownComponent(blender_object, component_model, context):
    """invoked after import of the scene to collect references to objects, materials, textures... in unknown components\n
    we do this so we can on export make sure they are added to the glb again and also pointers and guids/ids are updated"""
    col = blender_object.NEEDLE_unknown_components
    prop = col.add()
    prop.name = component_model["name"]
    prop.json = str(component_model)
    import_recursive_collect_data(prop, component_model, context)
    return

def foreachUnknownComponent(blender_object):
    """invoked on export to add unknown components to the glb"""
    for entry in blender_object.NEEDLE_unknown_components:
        jsonStr = entry.json
        # create a python object
        obj = eval(jsonStr)
        yield {
            "component" : obj,
            "unknown_data" : entry
        }
    return None


def resolveImportedJsonPointersToObjects(ext : ComponentExportContext, unknown : any, pointerResolver : ReferenceResolver):
    """ invoke after the scene has been processed to assign used references as glb_objects (e.g. a gltf material type) so they are exported\n
    we will also register those assignments to resolve them as json pointers at the very end of the export process
    """
    comp = unknown["component"]
    data = unknown["unknown_data"]
    hasSomethingToResolve = len(data.references) > 0
    if hasSomethingToResolve:
        recursively_update_resources(ext, None, comp, data, pointerResolver)
    pass

def recursively_update_resources(ctx : ComponentExportContext, key: None or str or int, value : any, unknown_data : UnknownComponent, pointerResolver : ReferenceResolver, currentObject:CurrentObject = None):

    if not currentObject is None:
        if key == "node" and isinstance(value, int):
            ctx.registerNodeReference(ctx.nodes[value], currentObject.obj, currentObject.key)
            return

     # OBJECT
    if isinstance(value, dict):
        currentObject = CurrentObject(value, "")
        for objKey in value:
            currentObject.key = objKey
            currentValue = value[objKey]
            res = recursively_update_resources(ctx, objKey, currentValue, unknown_data, pointerResolver, currentObject=currentObject)
            if res == FailedToResolve:
                # TODO: handle failed to resolve - we need to add it to the exported json now...
                # this can happen if a material is only referenced by a component and not used by any mesh
                print("ERR: Failed to resolve json pointer: " + currentValue)
                value[objKey] = None
            elif isinstance(res, str):
                print("> Resolved pointer " + currentValue + " to " + res)
                value[objKey] = res
            elif isinstance(res, DeferredResolveInfo):
                print("> Defer  pointer for " + currentValue + " in " + str(objKey))
                value[objKey] = res.gltf_object

    # LIST
    elif isinstance(value, list):
        currentObject = CurrentObject(value, 0)
        for i in range(len(value)):
            item = value[i]
            currentObject.key = i
            res = recursively_update_resources(ctx, i, item, unknown_data, pointerResolver, currentObject)
            if res == FailedToResolve:
                # TODO: see above
                print("ERR: Failed to resolve json pointer: " + item + " in list " + str(i))
                value[i] = None
            elif isinstance(res, str):                # 
                print("> Resolved pointer " + item + " to " + res + " in list at " + str(i))
                value[i] = res
            elif isinstance(res, DeferredResolveInfo):
                print("> Defer pointer for " + res.gltf_object.name + " [" + str(i) + "]")
                value[i] = res.gltf_object
    
    # POINTER ? maybe!
    elif isinstance(value, str):
        if value.startswith("/"):
            for ref in unknown_data.references:
                if ref.pointer == value:
                    if isinstance(ref, UnknownPointerObject):
                        # TODO: this should be done for known objects as well
                        if ref.hasReference() == False:
                            print("WARN: removing unknown/missing reference " + value)
                            if currentObject:
                                currentObject.write(None)
                            return None
                    obj = getObject(ref)
                    res = pointerResolver.tryResolve(obj, ref.pointerKey, currentObject)
                    return res


        