from asyncore import poll

from .utils_debug import debug_err
from .blender_windows import setStatus
import bpy
from .types.animation.animator import NEEDLE_AnimatorController

from . import panels_components
from . import component_registry, component_utils, utils
from .utils_icons import getPreviewIcon
from .component_utils import hasComponent
from .component_registry import getScheme

def pollObjectPicker(self, object):
    if isinstance(self, ComponentReference):
        requiredComponent = self.typeName
        if requiredComponent != "comp" and len(requiredComponent) > 0:
            hasIt = hasComponent(object, requiredComponent, False)
            return hasIt
    return True

def pollAnimatorControllerPicker(self, object):
    return isinstance(object, NEEDLE_AnimatorController)

def createEnumOptions(self, context, enumType:str, filter:str or None = None):
        # print("Create select method options ------------------")
    options = []

    # iterate the items on the current component
    for prop in self.items():
        # print("KEY", key)
        value = prop[1]
        # if an item is a object reference we want to go through that objects components
        if isinstance(value, bpy.types.Object):
            if enumType != "component":
                options.append(("none", "<none>", "Nothing selected", "VIEWZOOM", 0))
            # print("is object", value)
            if value is None: 
                continue
            # iterate the referenced object components
            for prop in value.items():
                componentFullName = prop[0]
                if componentFullName.startswith("NEEDLE_component") == False:
                    continue
                # check if the object has that component
                if not hasattr(value, componentFullName):
                    continue
                # TODO: filter inactive components
                # isActive = getIsActive(component)
                component = getattr(value, componentFullName)
                isActive = getattr(component, "internal___active")
                if isActive == False:
                    continue

                startIndex = len("NEEDLE_component_")
                componentName = componentFullName[startIndex:len(componentFullName)]
                 
                allSchemes = component_registry.schemes
                if not componentName in allSchemes:
                    continue

                if enumType == "component":
                    # if filter is None: 
                    #     continue
                    if filter != None and filter != "comp":
                        if not componentName.lower().startswith(filter.lower()):
                            continue
                    # print(filter, componentName)
                    id = utils.generateIdFromString(componentName)
                    icon = getPreviewIcon("logo_small_bw")
                    options.append((componentFullName, componentName, "", icon, id))
                    continue
                
                # get the schema for the component
                schema = allSchemes[componentName]
                # iterate the schemes settings
                schemeItems = schema.items()
                if len(schemeItems) == 0:
                    continue
                for prop in schemeItems:
                    memberName = prop[0]
                    if memberName.startswith("internal__"):
                        continue
                    # make sure we only select functions
                    memberValue = prop[1]
                    type = None
                    if "type" in memberValue:
                        type = memberValue.get("type")
                    if not type == "fn":
                        continue
                    # the first argument must be the original method name
                    # thats what we will serialize
                    # methodFullName = componentFullName+ "::" + memberName
                    # args : None or list = memberValue.get("args")
                    methodInfo = component_utils.toMethodInfo(componentFullName, memberName)
                    id = utils.generateIdFromString(methodInfo)
                    displayName = componentName + ": " + memberName
                    icon = "WORDWRAP_OFF"
                    options.append((methodInfo, displayName, "Method " + memberName + " on component " + componentName, icon, id))

    if len(options) == 0:
        options.append(("none", "<none>", "Please assign an object", 0))
    
    return options


def createSelectComponentOptions(self, context):
    return createEnumOptions(self, context, "component", self.typeName)

def createSelectMethodOptions(self, context):
    return createEnumOptions(self, context, "method", None)

def onSelectedComponentChanged(self, context):
    pass



def onSelectedMethodChanged(self, context):
    self.args.clear()
    method = self.method
    componentNameAndMethod = method.split("::")
    if len(componentNameAndMethod) != 2:
        print("Selected method changed but componentName and methodName can not be extracted", method)
        return
    componentName = componentNameAndMethod[0]
    methodName = componentNameAndMethod[1]
    print("Selected method", componentName, methodName)
    componentScheme = getScheme(componentName)
    methodScheme = componentScheme[methodName]
    args = methodScheme.get("args", None)
    if args is None:
        return
    # iterate the arguments declared in the scheme and add them to the method call
    for arg in args:
        argName = arg.get("name", "")
        argType = arg["type"]
        prop = self.args.add()
        prop.argumentName = argName
        prop.argumentType = argType
        pass
    pass

class MethodCallArgument(bpy.types.PropertyGroup):
    argumentName: bpy.props.StringProperty(name="Name", default="")
    argumentType: bpy.props.StringProperty(name="Type", default="")
    stringValue : bpy.props.StringProperty(name="String", default="")
    boolValue : bpy.props.BoolProperty(name="Bool", default=False)

    def serialize(self):
        if self.argumentType == "string":
            return self.stringValue
        elif self.argumentType == "bool":
            return self.boolValue
        return None

    def draw(self, context, layout, index:int, count:int):
        row = layout.row()
        # we only want to display the argument name if there are multiple arguments
        if count > 1 and len(self.argumentName) > 0:
            row.label(text=self.argumentName)
        if self.argumentType == "string":
            row.prop(self, "stringValue", text="")
        elif self.argumentType == "bool":
            row.prop(self, "boolValue", text="")

# this is used inside a EventList array
class MethodCall(bpy.types.PropertyGroup):
    obj : bpy.props.PointerProperty(name="Object", type=bpy.types.Object, poll=pollObjectPicker)
    method : bpy.props.EnumProperty(name="Method to call", items=createSelectMethodOptions, update=onSelectedMethodChanged)
    args : bpy.props.CollectionProperty(type=MethodCallArgument)

    def draw(self, context, layout):
        row = layout.row()
        row.prop(self, "obj", text="")

        # selection enum
        selection = row.column()
        selection.enabled = self.obj is not None
        selection.prop(self, "method", text="")

        # args
        # args = row.column()
        # args.enabled = self.method is not None
        argsCount = len(self.args)
        for i in range(argsCount):
            arg = self.args[i]
            arg.draw(context, row, i, argsCount)
        pass


# this is used to make a reference to another component
class ComponentReference(bpy.types.PropertyGroup):
    # internal__name: bpy.props.StringProperty(name="Name", default="ComponentReference")
    obj : bpy.props.PointerProperty(name="Object", type=bpy.types.Object, poll=pollObjectPicker)
    component : bpy.props.EnumProperty(name="", items=createSelectComponentOptions, update=onSelectedComponentChanged)
    typeName = ""

    def draw(self, context, layout):
        row = layout.row()
        # draw prefix label
        propName = panels_components.currentDrawContext.currentPropName()
        if panels_components.currentDrawContext.isInList() == False or propName != "value":
            row.label(text=propName)

        row.prop(self, "obj", text="")

        # component selection enum
        componentSelection = row.column()
        componentSelection.enabled = self.obj is not None
        componentSelection.prop(self, "component", text="")
        pass

# this is used to make a reference to another component
# we need to create a new type because we need to be able to filter the components
componentReferenceTypes = {}

def getOrCreateComponentReferenceType(typeName:str) -> type:
    global componentReferenceTypes
    if componentReferenceTypes.get(typeName) is not None:
        return componentReferenceTypes.get(typeName)
    # create a new type
    newType = type(typeName, (ComponentReference,), {})
    # newType.obj.name = "Please select an object with a " + typeName + " component"
    newType.typeName = typeName
    bpy.utils.register_class(newType)
    componentReferenceTypes[typeName] = newType
    return newType

customTypes = {}
debugCustomTypes = False

class CreateComponentContext:
    componentPanelName: str = None
currentComponentContext = None

def _getTypeName(typeName:str or dict):
    if isinstance(typeName, dict):
        # if "name" in typeName:
        #     return typeName["name"]
        keys = typeName.keys()
        name = "_".join(keys)
        return name
    return typeName

def getOrCreateCustomType(name, typeName : str or None, isArray:bool, schema : dict or None) -> bpy.types.PropertyGroup:
    # fallback if a custom property has no explicit type defined
    if typeName == None:
        typeName = name
    
    # this is the name we give the new type
    pythonTypeName = _getTypeName(typeName)
    # make sure it's not too long
    if len(pythonTypeName) > 60:
        pythonTypeName = pythonTypeName[0:60]
    # add array postfix
    if isArray and isinstance(pythonTypeName, str):
        pythonTypeName += "[]"

    # get the previously created type if it already exists
    if name in customTypes:
        return customTypes[typeName]
    
    # this is the dict we use to create the custom type
    typeDef = {}

    if(debugCustomTypes): print("Create custom type", typeName, pythonTypeName, schema)

    # arrays need this index property so we combine it into one type
    if isArray:
        entryScheme = None
        if schema:
            entryScheme = schema["type"]
        # the schema must not be a of type string for example
        # e.g. if it's "animationclip" when creating a clips[] list
        # because we just
        if not isinstance(entryScheme, dict):
            entryScheme = None
        collectionType = getOrCreateCustomType(name, typeName, False, entryScheme)
        typeDef["array"] = bpy.props.CollectionProperty(type=collectionType)
        typeDef["internal__array_index"] = bpy.props.IntProperty(name="index", default=0)

    # if a schema is provided and its a dict we use that to build the custom type
    elif isinstance(schema, dict):
        for propName in schema:
            # when building a complex type from a schema where it defines a type name we skip that property
            if propName == "type":
                continue
            if(debugCustomTypes): print("Create custom type property " + propName)
            prop = schema[propName]
            propTypeName = None
            value = None
            if "type" in prop:
                propTypeName = prop["type"]
            if isinstance(prop, dict):
                value = prop.get("value", None)
            else:
                pass
            typeDef[propName] = createProperty(propName, propTypeName, value, prop)
    
    # if the schema is none we create a type that only holds a single value
    elif schema is None:
        prop = createProperty("value", typeName, None, {})
        typeDef["value"] = prop


    # save the type definition for iterating and serialization
    # TODO: remove the need for this - this currently keeps a reference to the _PropertyDeferred type which we dont want, we only want the keys and type info
    # annotations = {}
    # propertyNames = []
    # for propName in typeDef:
    #     propertyNames.append(propName)
    # annotations["internal__propertyNames"] = propertyNames
    typeDef["__annotations__"] = typeDef
    


    # type names must not be longer than 64 chars
    # creating the custom type deriving from our custom PropertyGroup
    customType = type(pythonTypeName, (panels_components.CustomPropertyType,), typeDef)

    # register it and save it for unregistering later
    bpy.utils.register_class(customType)
    customTypes[pythonTypeName] = customType
    # panels_components.customPropertyTypeSchemes[pythonTypeName] = schema
    # print(panels_components.customPropertyTypeSchemes)
    

    # global currentComponentContext
    # if currentComponentContext is not None:
    #     print("Registering custom type", pythonTypeName, "for panel", currentComponentContext.componentPanelName)
    #     customType["bl_parent_id"] = panels_components.getPanelTypeName(currentComponentContext.componentPanelName)

    if(debugCustomTypes): 
        print("Created custom type", pythonTypeName, "\n- TypeDef:", typeDef, "\n- Type:", customType)
    return customType

noDescriptionText = "No description available"

def getDescription(propertyName, propertyDef, default = None):
    if "description" in propertyDef:
        return propertyDef["description"]
    if propertyName == "enabled":
        return "Enable or disable this component"
    if default != None:
        return str(default)
    return propertyDef.get("description", noDescriptionText)

def createEnumFlagOptions(name, options) -> list:

    def parseEnumValue(strValue:str, index:int) -> tuple[str, int]:
        # split by =
        # e.g. the enum options list can contain 
        # - "value=1" in which case we use the value after =
        # - "value" in which case we use the index
        parts = strValue.split("=")
        if len(parts) == 2:
            name = parts[0]
            valueStr = parts[1]
            name = name.strip() # trim
            valueStr = valueStr.strip() # trim
            if valueStr.startswith("0x"):
                value = int(valueStr, 16)
            elif valueStr.__contains__("<<"):
                # bit shift
                parts = valueStr.split("<<")
                value = int(parts[0]) << int(parts[1])
            else:
                value = int(valueStr)
            return (name, value)
        # fallback if just a name is provided
        return (strValue, 1 << index)
    
    items = []
    for i in range(len(options)):
        parsedValue = parseEnumValue(options[i], i)
        value = parsedValue[1]
        displayText = parsedValue[0]
        items.append((str(value), displayText, getDescription(options[i], {})))
    return items


def onPropertyChanged(self, context, name):
    componentName = "NEEDLE_component_" + self.bl_label
    newValue = getattr(self, name)
    # apply to all selected objects
    for obj in context.selected_objects:
        if obj is None:
            continue
        if obj == self:
            continue
        try:
            component = getattr(obj, componentName)
            if component is None:
                continue
            current_value = getattr(component, name, "NOTFOUND")
            if current_value == "NOTFOUND":
                continue
            if current_value == newValue:
                continue
            print("Set property " + name + " to " + str(newValue) + " on " + obj.name)
            setattr(component, name, newValue)
        except Exception as e:
            debug_err(e)
    pass

# TODO: remove componentDef from here - we only want to return one property here always!
def createProperty(name, typeName : str, default, propertyDef):
    
    allowList = True
    propertyChangedLambda = lambda self, context: onPropertyChanged(self, context, name)

    if typeName == "vec2" or typeName == "vec3" or typeName == "vec4":
        allowList = False
    
    # https://docs.blender.org/api/current/bpy.props.html
    if allowList and isinstance(default, list):
        # print("--------------------\nCreate array property", name, typeName, default, propertyDef)
        # https://blender.stackexchange.com/questions/119267/default-value-for-bpy-props-collectionproperty
        # collection = bpy.props.CollectionProperty(type=itemType)
        # componentDef[name] = collection
        # componentDef["internal__" + name + "_index"] = bpy.props.IntProperty()
        array = getOrCreateCustomType(name, typeName, True, propertyDef)
        return bpy.props.PointerProperty(type=array)

    if typeName == "int":
        if default == None:
            default = 0
        return bpy.props.IntProperty(name=name, default=default)
    if typeName == "float":
        if default == None:
            default = 0.0
        # infinity
        min = propertyDef.get("min", -float("inf"))
        max = propertyDef.get("max", float("inf"))
        return bpy.props.FloatProperty(name=name, default=default, description=getDescription(name, propertyDef), min=min, max=max, update=propertyChangedLambda)
    if typeName == "string":
        if default == None:
            default = ""
        return bpy.props.StringProperty(name=name, default=default, description=getDescription(name, propertyDef))
    if typeName == "bool":
        if default == None:
            default = False
        return bpy.props.BoolProperty(name=name, default=default, description=getDescription(name, propertyDef), update=propertyChangedLambda)
    if typeName == "col":
        if default == None:
            default = (0.5, 0.5, 0.5)
        return bpy.props.FloatVectorProperty(name=name, default=default, subtype="COLOR", min=0.0, max=1.0, soft_min = 0.0, soft_max = 1.0, step = 1, update=propertyChangedLambda)
    if typeName == "vec2":
        if default == None:
            default = (0.0, 0.0)
        return bpy.props.FloatVectorProperty(name=name, default=default, subtype="XYZ", size=2, update=propertyChangedLambda)
    if typeName == "vec3":
        if default == None:
            default = (0.0, 0.0, 0.0)
        return bpy.props.FloatVectorProperty(name=name, default=default, subtype="XYZ", size=3, update=propertyChangedLambda)
    if typeName == "vec4":
        if default == None:
            default = (0.0, 0.0, 0.0, 0.0)
        return bpy.props.FloatVectorProperty(name=name, default=default, subtype="XYZ", size=4, update=propertyChangedLambda)
    if typeName == "quat":
        if default == None:
            default = (0.0, 0.0, 0.0, 0.0)
        return bpy.props.FloatVectorProperty(name=name, size=4, subtype="QUATERNION")
    if typeName == "obj" or typeName == "object3d":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Object, poll=pollObjectPicker, description=getDescription(name, propertyDef), update=propertyChangedLambda)
    if typeName == "assetreference":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Scene, poll=pollObjectPicker, description=getDescription(name, propertyDef), update=propertyChangedLambda)
    if typeName == "tex" or typeName == "texture":
        # TODO: we must export textures as gltfTexture and not image
        return bpy.props.PointerProperty(name=name, type=bpy.types.Image, poll=pollObjectPicker, description=getDescription(name, propertyDef), update=propertyChangedLambda)
    if typeName == "img" or typeName == "image":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Image, poll=pollObjectPicker, description=getDescription(name, propertyDef), update=propertyChangedLambda) 
    if typeName == "mat" or typeName == "material":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Material, poll=pollObjectPicker, description=getDescription(name, propertyDef), update=propertyChangedLambda)
    if typeName == "mesh":
        if default == None or default == "Mesh":
            default = bpy.types.Mesh
        return bpy.props.PointerProperty(name=name, type=bpy.types.Mesh, poll=pollObjectPicker, description=getDescription(name, propertyDef), update=propertyChangedLambda)

    if typeName == "enum" or typeName == "enum_flag":
        options = propertyDef["options"]
        items = []
        index = 0
        isFlag = typeName == "enum_flag"
        if isFlag:
            items = createEnumFlagOptions(name, options)
            return bpy.props.EnumProperty(items=items, name=name, options={"ENUM_FLAG"}, description=getDescription(name, propertyDef), update=propertyChangedLambda)
        else:
            for option in options:
                if len(option) > 0:
                    items.append((str(index), option, "", index))
                index += 1
            defaultValue = propertyDef.get("value", 0)
            return bpy.props.EnumProperty(items=items, name=name, default=defaultValue, description=getDescription(name, propertyDef), update=propertyChangedLambda)
    if typeName == "evt":
        callsArray = getOrCreateCustomType(name, "fn_call", True, propertyDef) 
        return bpy.props.PointerProperty(type=callsArray)
    #     # componentDef[name] = bpy.props.PointerProperty(name=name, type=bpy.types.Object, poll=pollObjectPicker, description=getDescription(name, propertyDef, "Select an object on which you want to call a method when \"" + name + "\" is triggered"))
    #     # componentDef[name + "_method"] = bpy.props.EnumProperty(name=name + " method", items=createSelectMethodOptions)
    #     # TODO: this we need to refactor to be a EventList type (propably dynamically generated too)
    #     # create MethodCall property
    #     # componentDef[name] = bpy.props.PointerProperty(name=name, type=EventList)
        return None
    if typeName == "fn_call":
        return bpy.props.PointerProperty(name=name, type=MethodCall)
    if typeName == "comp":
        componentReferenceType = getOrCreateComponentReferenceType(typeName)
        prop = bpy.props.PointerProperty(name=name, type=componentReferenceType, update=propertyChangedLambda)
        return prop
    if typeName == "fn":
        return bpy.props.StringProperty(name=name, default="Select a method to call", update=propertyChangedLambda)
    if typeName == "anim" or typeName == "animationclip":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Action, update=propertyChangedLambda)
    if typeName == "animatorcontroller":
        return bpy.props.PointerProperty(name=name, type=bpy.types.NodeTree, poll=pollAnimatorControllerPicker, description=getDescription(name, propertyDef, "Select an animator controller"))
    if typeName == "videoclip":
        return bpy.props.PointerProperty(name=name, type=bpy.types.MovieClip, update=propertyChangedLambda)
    if typeName == "audioclip":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Sound, update=propertyChangedLambda)
    if typeName == "imagereference":
        return bpy.props.PointerProperty(name=name, type=bpy.types.Image, update=propertyChangedLambda)

    if(debugCustomTypes):
        print("-------------------- Custom property")

    buildCustomType = typeName is None or typeName == "timeline"
    
    # create complex custom type
    if buildCustomType:
        customType = getOrCreateCustomType(name, typeName, False, propertyDef)
        if customType != None:
            return bpy.props.PointerProperty(name=name, type=customType)
    
    if typeName is None:
        typeName = "Unknown"
    else:
        if not isinstance(typeName, str):
            typeName = "INVALID:" + str(typeName)
            print("Typename is not a string", typeName)
        else:
            # test if there exists a component scheme for this unknown type name
            # in which case we want to render a component picker
            scheme = component_registry.getScheme(typeName)
            if scheme is not None:
                componentReferenceType = getOrCreateComponentReferenceType(typeName)
                prop = bpy.props.PointerProperty(name=name, type=componentReferenceType)
                return prop

    notFoundProp = bpy.props.StringProperty(name=name, default="Type \"" + typeName + "\" not found / generated")
    return notFoundProp


# class MyAddonProperties(bpy.types.PropertyGroup):
#     my_prop_1 = bpy.props.IntProperty()
#     my_prop_2 = bpy.props.IntProperty()
#     my_prop_3 = bpy.props.IntProperty()



def createTypeFromSchema(schema):
    componentName = schema["name"]
    data = {}
    data["bl_label"] = componentName
    data["bl_idname"] = "OBJECT_PT_" + componentName.lower()
    obj= {}
    data["__annotations__"] = obj
    obj["internal___active"] = bpy.props.BoolProperty(default=False)
    obj["guid"] = bpy.props.StringProperty(default="")
    obj[panels_components.getBlenderObjectPropertyKey()] = bpy.props.PointerProperty(type=bpy.types.Object)

    global currentComponentContext
    if currentComponentContext is None:
        currentComponentContext = CreateComponentContext()
    currentComponentContext.componentPanelName = componentName
    try:
        if "blender" in schema:
            blender = schema["blender"]
            if "description" in blender:
                data["description"] = blender["description"]
        for key in schema:
            # ignore some magic fields for which we dont want to create properties
            if key != "name" and key != "blender" and key.startswith("internal__") == False:
                try:
                    propertyDef = schema[key]
                    propertyType = None
                    if "type" in propertyDef:
                        propertyType = propertyDef["type"]
                    if isinstance(propertyType, dict):
                        propertyType = componentName + "_" + key
                    componentProperty = createProperty(key, propertyType, propertyDef.get("value", None), propertyDef) 
                    if componentProperty != None:
                        key = getBlenderPropertyKey(key)
                        obj[key] = componentProperty
                except Exception as e:
                    debug_err("Error creating property " + key + " for component " + componentName + ": " + str(e))
                    pass
                # add method to select for events and additional infos
        # print(data)
        newType = type("COMPONENT_" + componentName, (bpy.types.PropertyGroup,), data)
        bpy.utils.register_class(newType)
        return {"type": newType, "schema": data}
    except Exception as e:
        debug_err("Error creating component " + componentName + ": ", e)
        # raise e
    finally:
        currentComponentContext = None


# Blender properties must not start with an underscore
def getBlenderPropertyKey(name):
    if name.startswith("_"):
        return "%" + name
    return name

def register():
    bpy.app.timers.register(generateComponentTypesFromSchemes, first_interval=.1)

def unregister():
    panels_components.unregisterPanelTypes()
    unregisterCustomTypes()

def unregisterCustomTypes():
    for key in customTypes:
        type = customTypes[key]
        bpy.utils.unregister_class(type)
        panels_components.customPropertyTypeSchemes.pop(key, None)
    customTypes.clear()

    for key in componentReferenceTypes:
        type = componentReferenceTypes[key]
        bpy.utils.unregister_class(type)
    componentReferenceTypes.clear()

def generateComponentTypesFromSchemes():
    panels_components.unregisterPanelTypes()
    unregisterCustomTypes()

    print("###############################################################")
    import time
    formattedTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    print("Generating component types from schemes: " + formattedTime)

    component_registry.loadSchemes()

    for key in component_registry.schemes:
        schema = component_registry.schemes[key]
        schema["name"] = key
        schema["internal__displayName"] = key
        # every schema can have a blender object that can contain internal information
        # it is just for UI purposes
        if(schema.get("blender", None) != None):
            blender = schema.get("blender")
            if "tag" in blender and blender["tag"] == "experimental":
                schema["internal__displayName"] += " (experimental)"
                pass

        typeInfo = createTypeFromSchema(schema)
        component_registry.types[key] = typeInfo["type"] 
        typeProperty = bpy.props.PointerProperty(type=typeInfo["type"])

        fullName : str = "NEEDLE_component_" + key
        setattr(bpy.types.Object, fullName, typeProperty)
        # bpy.types.Object.components[key] = myType["type"]()
        # bpy.types.Object.component_schema = myType["schema"]
        
        # register component panel
        panels_components.createPanelType(key, schema, fullName, typeInfo)
        # ui_info =  { 
        #     "bl_label": schema["internal__displayName"],
        #     "bl_description": "test",
        #     "bl_context" : "object",
        # }
        
        # if key == "Camera":
        #     ui_info["bl_context"] = "data"

        # panelType = type("COMPONENT_PANEL_PT_" + schema["name"], (NEEDLE_PT_ComponentPanelBase,), ui_info)
        # panelTypes.append(panelType)
        # panelType.key = ptr
        # panelType.scheme = typeInfo["schema"]
        # bpy.utils.register_class(panelType)
    
    try:
        bpy.utils.unregister_class(panels_components.NEEDLE_PT_ComponentPanelBase) # the base panel should never be visible directly
    except RuntimeError as e:
        # setStatus(e)
        pass

    panels_components.detectMissingComponents()





# import bpy
# from bpy.props import FloatProperty, StringProperty, PointerProperty
# from bpy.types import PropertyGroup

# # TODO: load dynamically at runtime from a JSON file!
# bpy.propertyGroupLayouts = {
#     "Health": [
#         {"name": "current", "type": "float"}
#     ]
# }
# bpy.samplePropertyGroups = {}


# class SamplePanel(bpy.types.Panel):
#     bl_label = "Sample Panel"
#     bl_space_type = 'PROPERTIES'
#     bl_region_type = 'WINDOW'
#     bl_context = "object"

#     def draw(self, context):
#         layout = self.layout
#         obj = context.object

#         # use our layout definition to dynamically create our panel items
#         for groupName, attributeDefinitions in bpy.propertyGroupLayouts.items():
#             # get the instance of our group
#             # dynamic equivalent of `obj.samplePropertyGroup` from before
#             propertyGroup = getattr(obj, groupName)

#             # start laying this group out
#             col = layout.column()
#             col.label(text=groupName)
#             # col.label(text=propertyGroup["current"])

#             # loop through all the attributes and show them
#             for attributeDefinition in attributeDefinitions:
#                 col.prop(propertyGroup, attributeDefinition["name"])

#             # draw a separation between groups
#             layout.separator()


# def register():
#     # register the panel class
#     # bpy.utils.register_class(SamplePanel)

#     # iterate over our list of property groups
#     for groupName, attributeDefinitions in bpy.propertyGroupLayouts.items():
#         # build the attribute dictionary for this group
#         attributes = {}
#         for attributeDefinition in attributeDefinitions:
#             attType = attributeDefinition['type']
#             attName = attributeDefinition['name']
#             print(attName, attType)
#             if attType == 'float':
#                 attributes[attName] = FloatProperty(name=attName.title())
#             elif attType == 'string':
#                 attributes[attName] = StringProperty(name=attName.title())
#             else:
#                 raise TypeError('Unsupported type (%s) for %s on %s!' %
#                                 (attType, attName, groupName))
#         # now build the property group class
#         propertyGroupClass = type(groupName, (PropertyGroup,), attributes)

#         # register it with Blender

#         print("REGISTER", propertyGroupClass, groupName, attributes)

#         bpy.utils.register_class(propertyGroupClass)

#         # apply it to all Objects
#         setattr(bpy.types.Object, groupName, PointerProperty(type=propertyGroupClass))

#         # store it for later
#         bpy.samplePropertyGroups[groupName] = propertyGroupClass


# def unregister():
#     # unregister the panel class
#     # bpy.utils.unregister_class(SamplePanel123)

#     # unregister our components
#     try:
#         for key, value in bpy.samplePropertyGroups.items():
#             delattr(bpy.types.Object, key)
#             bpy.utils.unregister_class(value)
#     except UnboundLocalError:
#         pass
#     bpy.samplePropertyGroups = {}
