import bpy
import typing, enum
from . import UTIL_functions, UTIL_icons_manager

#region Virtools Groups Define & Help Class

class BBP_PG_virtools_group(bpy.types.PropertyGroup):
    group_name: bpy.props.StringProperty(
        name = "Group Name",
        default = ""
    )

def get_virtools_groups(obj: bpy.types.Object) -> bpy.types.CollectionProperty:
    return obj.virtools_groups

def get_active_virtools_groups(obj: bpy.types.Object) -> int:
    return obj.active_virtools_groups

def set_active_virtools_groups(obj: bpy.types.Object, val: int) -> None:
    obj.active_virtools_groups = val

class VirtoolsGroupsHelper():
    """
    A helper for object's Virtools groups adding, removal and checking.

    All Virtools group operations should be done by this class.
    Do NOT manipulate object's Virtools group properties directly.
    """
    __mSingletonMutex: typing.ClassVar[bool] = False
    __mIsValid: bool
    __mNoChange: bool ##< A bool indicate whether any change happended during lifetime. If no change, skip the writing when exiting.
    __mAssocObj: bpy.types.Object
    __mGroupsSet: set[str]
    
    def __init__(self, assoc: bpy.types.Object):
        self.__mGroupsSet = set()
        self.__mAssocObj = assoc
        self.__mNoChange = True
        
        # check singleton
        if VirtoolsGroupsHelper.__mSingletonMutex:
            self.__mIsValid = False
            raise UTIL_functions.BBPException('VirtoolsGroupsHelper is mutex.')
        
        # set validation and read ballance elements property
        VirtoolsGroupsHelper.__mSingletonMutex = True
        self.__mIsValid = True
        self.__read_from_virtools_groups()
    
    def is_valid(self) -> bool:
        return self.__mIsValid
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        self.dispose()
    
    def dispose(self) -> None:
        if self.is_valid():
            # if have changes,
            # write to ballance elements property and reset validation
            if not self.__mNoChange:
                self.__write_to_virtools_groups()
            self.__mIsValid = False
            VirtoolsGroupsHelper.__mSingletonMutex = False
    
    def __check_valid(self) -> None:
        if not self.is_valid():
            raise UTIL_functions.BBPException('calling invalid VirtoolsGroupsHelper')
    
    def add_group(self, gname: str) -> None:
        self.__check_valid()
        self.__mNoChange = False
        self.__mGroupsSet.add(gname)
    
    def add_groups(self, gnames: typing.Iterable[str]) -> None:
        self.__check_valid()
        self.__mNoChange = False
        self.__mGroupsSet.update(gnames)
    
    def remove_group(self, gname: str) -> None:
        self.__check_valid()
        self.__mNoChange = False
        self.__mGroupsSet.discard(gname)
    
    def remove_groups(self, gnames: typing.Iterable[str]) -> None:
        self.__check_valid()
        self.__mNoChange = False
        for gname in gnames:
            self.__mGroupsSet.discard(gname)
    
    def contain_group(self, gname: str) -> bool:
        self.__check_valid()
        return gname in self.__mGroupsSet
    
    def contain_groups(self, gnames: typing.Iterable[str]) -> bool:
        """
        Check existing intersection between group names and given collection.

        In other words, check whether group name of given paramter is in group names with OR operator.

        @param gnames[in] Iterable group names to check.
        @return return True if the length of the intersection between group names and given group names is not zero.
        """
        self.__check_valid()
        for gname in gnames:
            if gname in self.__mGroupsSet:
                return True
        return False
    
    def intersect_groups(self, gnames: set[str]) -> set[str]:
        self.__check_valid()
        return self.__mGroupsSet.intersection(gnames)
    
    def iterate_groups(self) -> typing.Iterator[str]:
        self.__check_valid()
        return iter(self.__mGroupsSet)

    def clear_groups(self) -> None:
        self.__check_valid()
        self.__mNoChange = False
        self.__mGroupsSet.clear()

    def get_count(self) -> int:
        self.__check_valid()
        return len(self.__mGroupsSet)
    
    def __write_to_virtools_groups(self) -> None:
        groups: bpy.types.CollectionProperty = get_virtools_groups(self.__mAssocObj)
        sel: int = get_active_virtools_groups(self.__mAssocObj)
        groups.clear()
        
        for gname in self.__mGroupsSet:
            item: BBP_PG_virtools_group = groups.add()
            item.group_name = gname
        
        # restore selection if necessary
        if sel >= len(self.__mGroupsSet):
            sel = len(self.__mGroupsSet) - 1
        if sel < 0:
            sel = 0
        set_active_virtools_groups(self.__mAssocObj, sel)
    
    def __read_from_virtools_groups(self) -> None:
        groups: bpy.types.CollectionProperty = get_virtools_groups(self.__mAssocObj)
        self.__mGroupsSet.clear()
        
        item: BBP_PG_virtools_group
        for item in groups:
            self.__mGroupsSet.add(item.group_name)

#endregion

#region Preset Group Names

class VirtoolsGroupsPreset(enum.Enum):
    Sector_01 = "Sector_01"
    Sector_02 = "Sector_02"
    Sector_03 = "Sector_03"
    Sector_04 = "Sector_04"
    Sector_05 = "Sector_05"
    Sector_06 = "Sector_06"
    Sector_07 = "Sector_07"
    Sector_08 = "Sector_08"
    
    P_Extra_Life = "P_Extra_Life"
    P_Extra_Point = "P_Extra_Point"
    P_Trafo_Paper = "P_Trafo_Paper"
    P_Trafo_Stone = "P_Trafo_Stone"
    P_Trafo_Wood = "P_Trafo_Wood"
    P_Ball_Paper = "P_Ball_Paper"
    P_Ball_Stone = "P_Ball_Stone"
    P_Ball_Wood = "P_Ball_Wood"
    P_Box = "P_Box"
    P_Dome = "P_Dome"
    P_Modul_01 = "P_Modul_01"
    P_Modul_03 = "P_Modul_03"
    P_Modul_08 = "P_Modul_08"
    P_Modul_17 = "P_Modul_17"
    P_Modul_18 = "P_Modul_18"
    P_Modul_19 = "P_Modul_19"
    P_Modul_25 = "P_Modul_25"
    P_Modul_26 = "P_Modul_26"
    P_Modul_29 = "P_Modul_29"
    P_Modul_30 = "P_Modul_30"
    P_Modul_34 = "P_Modul_34"
    P_Modul_37 = "P_Modul_37"
    P_Modul_41 = "P_Modul_41"
    
    PS_Levelstart = "PS_Levelstart"
    PE_Levelende = "PE_Levelende"
    PC_Checkpoints = "PC_Checkpoints"
    PR_Resetpoints = "PR_Resetpoints"
    
    Sound_HitID_01 = "Sound_HitID_01"
    Sound_RollID_01 = "Sound_RollID_01"
    Sound_HitID_02 = "Sound_HitID_02"
    Sound_RollID_02 = "Sound_RollID_02"
    Sound_HitID_03 = "Sound_HitID_03"
    Sound_RollID_03 = "Sound_RollID_03"
    
    DepthTestCubes = "DepthTestCubes"
    
    Phys_Floors = "Phys_Floors"
    Phys_FloorRails = "Phys_FloorRails"
    Phys_FloorStopper = "Phys_FloorStopper"
    
    Shadow = "Shadow"

_g_VtGrpPresetValues: tuple[str] = tuple(map(lambda x: x.value, VirtoolsGroupsPreset))

## Some of group names are not matched with icon name
#  So we create a convertion map to convert them.
_g_GroupIconNameConvMap: dict[str, str] = {
    "PS_Levelstart": "PS_FourFlames",
    "PE_Levelende": "PE_Balloon",
    "PC_Checkpoints": "PC_TwoFlames",
    "PR_Resetpoints": "PR_Resetpoint",

    "Sound_HitID_01": "SoundID_01",
    "Sound_RollID_01": "SoundID_01",
    "Sound_HitID_02": "SoundID_02",
    "Sound_RollID_02": "SoundID_02",
    "Sound_HitID_03": "SoundID_03",
    "Sound_RollID_03": "SoundID_03"
}
def _get_group_icon_by_name(gp_name: str) -> int:
    # try converting group name
    # if not found, return self
    gp_name = _g_GroupIconNameConvMap.get(gp_name, gp_name)
    
    # get from extra group icon first
    value: int | None = UTIL_icons_manager.get_group_icon(gp_name)
    if value is not None: return value

    # if failed, get from component. if still failed, return empty icon
    value = UTIL_icons_manager.get_component_icon(gp_name)
    if value is not None: return value
    else: return UTIL_icons_manager.get_empty_icon()
# blender group name prop helper
_g_EnumHelper_Group: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
    VirtoolsGroupsPreset,
    lambda x: x.value,  # member is string self
    lambda x: VirtoolsGroupsPreset(x),   # convert directly because it is StrEnum.
    lambda x: x.value,
    lambda _: '',
    lambda x: _get_group_icon_by_name(x.value)
)

class SharedGroupNameInputProperties():
    group_name_source: bpy.props.EnumProperty(
        name = "Group Name Source",
        items = (
            ('DEFINED', "Predefined", "Pre-defined group name."),
            ('CUSTOM', "Custom", "User specified group name."),
        ),
    )
    
    preset_group_name: bpy.props.EnumProperty(
        name = "Group Name",
        description = "Pick vanilla Ballance group name.",
        items = _g_EnumHelper_Group.generate_items(),
    )
    
    custom_group_name: bpy.props.StringProperty(
        name = "Custom Group Name",
        description = "Input your custom group name.",
        default = "",
    )
    
    def draw_group_name_input(self, layout: bpy.types.UILayout) -> None:
        layout.prop(self, 'group_name_source', expand = True)
        if (self.group_name_source == 'CUSTOM'):
            layout.prop(self, 'custom_group_name')
        else:
            layout.prop(self, 'preset_group_name')
    
    def general_get_group_name(self) -> str:
        if self.group_name_source == 'CUSTOM':
            return self.custom_group_name
        else:
            return _g_EnumHelper_Group.get_selection(self.preset_group_name).value

#endregion

#region Display Panel and Simple Operator

class BBP_UL_virtools_groups(bpy.types.UIList):
    def draw_item(self, context, layout: bpy.types.UILayout, data, item: BBP_PG_virtools_group, icon, active_data, active_propname):
        layout.label(text = item.group_name, translate = False, icon_value = _get_group_icon_by_name(item.group_name))

class BBP_OT_add_virtools_group(bpy.types.Operator, SharedGroupNameInputProperties):
    """Add a Virtools Group for Active Object."""
    bl_idname = "bbp.add_virtools_groups"
    bl_label = "Add to Virtools Groups"
    bl_options = {'UNDO'}
    
    @classmethod
    def poll(self, context: bpy.types.Context):
        return context.object is not None
    
    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self)
    
    def execute(self, context):
        # add group
        with VirtoolsGroupsHelper(context.object) as hlp:
            hlp.add_group(self.general_get_group_name())
        return {'FINISHED'}
    
    def draw(self, context):
        self.draw_group_name_input(self.layout)

class BBP_OT_rm_virtools_group(bpy.types.Operator):
    """Remove a Virtools Group for Active Object."""
    bl_idname = "bbp.rm_virtools_groups"
    bl_label = "Remove from Virtools Groups"
    bl_options = {'UNDO'}
    
    ## This class is slightly unique.
    #  Because we need get user selected group name first.
    #  Then pass it to helper.
    
    @classmethod
    def poll(self, context: bpy.types.Context):
        if context.object is None:
            return False
        
        obj = context.object
        gp = get_virtools_groups(obj)
        active_gp = get_active_virtools_groups(obj)
        return active_gp >= 0 and active_gp < len(gp)
    
    def execute(self, context):
        # get selected group name first
        obj = context.object
        item: BBP_PG_virtools_group = get_virtools_groups(obj)[get_active_virtools_groups(obj)]
        gname: str = item.group_name
        # then delete it
        with VirtoolsGroupsHelper(obj) as hlp:
            hlp.remove_group(gname)
        
        return {'FINISHED'}

class BBP_OT_clear_virtools_groups(bpy.types.Operator):
    """Clear All Virtools Group for Active Object."""
    bl_idname = "bbp.clear_virtools_groups"
    bl_label = "Clear Virtools Groups"
    bl_options = {'UNDO'}
    
    @classmethod
    def poll(self, context: bpy.types.Context):
        return context.object is not None
    
    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_confirm(self, event)
    
    def execute(self, context):
        with VirtoolsGroupsHelper(context.object) as hlp:
            hlp.clear_groups()
        return {'FINISHED'}

class BBP_PT_virtools_groups(bpy.types.Panel):
    """Show Virtools Groups Properties."""
    bl_label = "Virtools Groups"
    bl_idname = "BBP_PT_virtools_groups"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"
    
    @classmethod
    def poll(cls, context):
        return context.object is not None
    
    def draw(self, context):
        layout = self.layout
        target = bpy.context.active_object
        
        row = layout.row()
        row.template_list(
            "BBP_UL_virtools_groups", "", 
            target, "virtools_groups",
            target, "active_virtools_groups",
            rows = 6,
            maxrows = 6,
        )
        
        col = row.column(align=True)
        col.operator(BBP_OT_add_virtools_group.bl_idname, icon='ADD', text="")
        col.operator(BBP_OT_rm_virtools_group.bl_idname, icon='REMOVE', text="")
        col.separator()
        col.operator(BBP_OT_clear_virtools_groups.bl_idname, icon='TRASH', text="")

#endregion

def register() -> None:
    # register all classes
    bpy.utils.register_class(BBP_PG_virtools_group)
    bpy.utils.register_class(BBP_UL_virtools_groups)
    bpy.utils.register_class(BBP_OT_add_virtools_group)
    bpy.utils.register_class(BBP_OT_rm_virtools_group)
    bpy.utils.register_class(BBP_OT_clear_virtools_groups)
    bpy.utils.register_class(BBP_PT_virtools_groups)
    
    # add into scene metadata
    bpy.types.Object.virtools_groups = bpy.props.CollectionProperty(type = BBP_PG_virtools_group)
    bpy.types.Object.active_virtools_groups = bpy.props.IntProperty()

def unregister() -> None:
    # del from scene metadata
    del bpy.types.Scene.active_virtools_groups
    del bpy.types.Scene.virtools_groups
    
    bpy.utils.unregister_class(BBP_PT_virtools_groups)
    bpy.utils.unregister_class(BBP_OT_clear_virtools_groups)
    bpy.utils.unregister_class(BBP_OT_rm_virtools_group)
    bpy.utils.unregister_class(BBP_OT_add_virtools_group)
    bpy.utils.unregister_class(BBP_UL_virtools_groups)
    bpy.utils.unregister_class(BBP_PG_virtools_group)
