import bpy
from . import UTILS_constants, UTILS_functions, UTILS_virtools_prop, UTILS_icons_manager

class rename_system_props(bpy.types.Operator):
    name_standard: bpy.props.EnumProperty(
        name="Name Standard",
        description="Choose your name standard",
        items=(
            ("YYC", "YYC Tools Chains", "YYC Tools Chains name standard."),
            ("IMENGYU", "Imengyu Ballance", "Auto grouping name standard for Imengyu/Ballance")
            ),
    )

    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self)

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "name_standard")

class BALLANCE_OT_rename_by_group(rename_system_props):
    """Rename object by Virtools groups"""
    bl_idname = "ballance.rename_by_group"
    bl_label = "Rename by Group"
    bl_options = {'UNDO'}

    def execute(self, context):
        _rename_core(_NameStandard.CKGROUP, _NameStandard.cvt_std_from_str_to_int(self.name_standard))
        return {'FINISHED'}

class BALLANCE_OT_convert_name(rename_system_props):
    """Convert name from one name standard to another one."""
    bl_idname = "ballance.convert_name"
    bl_label = "Convert Name"
    bl_options = {'UNDO'}

    dest_name_standard: bpy.props.EnumProperty(
        name="Destination Name Standard",
        description="Choose your name standard",
        items=(
            ("YYC", "YYC Tools Chains", "YYC Tools Chains name standard."),
            ("IMENGYU", "Imengyu Ballance", "Auto grouping name standard for Imengyu/Ballance")
            ),
    )

    def execute(self, context):
        _rename_core(
            _NameStandard.cvt_std_from_str_to_int(self.name_standard),
            _NameStandard.cvt_std_from_str_to_int(self.dest_name_standard))
        return {'FINISHED'}

    # rewrite draw func
    def draw(self, context):
        layout = self.layout
        layout.prop(self, "name_standard")
        layout.prop(self, "dest_name_standard")

class BALLANCE_OT_auto_grouping(rename_system_props):
    """Auto Grouping object according to specific name standard."""
    bl_idname = "ballance.auto_grouping"
    bl_label = "Auto Grouping"
    bl_options = {'UNDO'}

    def execute(self, context):
        _rename_core(_NameStandard.cvt_std_from_str_to_int(self.name_standard), _NameStandard.CKGROUP)
        return {'FINISHED'}

# ========================================== 
# rename misc funcs

class _RenameErrorType():
    ERROR = 0
    WARNING = 1
    INFO = 2

    @staticmethod
    def cvt_err_from_int_to_str(err_t):
        if err_t == _RenameErrorType.ERROR:
            return "ERROR"
        elif err_t == _RenameErrorType.WARNING:
            return "WARNING"
        elif err_t == _RenameErrorType.INFO:
            return "INFO"
        else:
            raise Exception("Unknow error type.")

class _RenameErrorItem():
    def __init__(self, err_t, description):
        self.err_type = err_t
        self.description = description

    def get_presentation(self):
        return "[{}]\t{}".format(_RenameErrorType.cvt_err_from_int_to_str(self.err_type), self.description)

class _RenameErrorReporter():
    def __init__(self):
        self.err_container: list[_RenameErrorItem] = []

    def add_error(self, description):
        self.err_container.append(_RenameErrorItem(_RenameErrorType.ERROR, description))
    def add_warning(self, description):
        self.err_container.append(_RenameErrorItem(_RenameErrorType.WARNING, description))
    def add_info(self, description):
        self.err_container.append(_RenameErrorItem(_RenameErrorType.INFO, description))

    def can_report(self):
        return len(self.err_container) != 0

    def report(self, header):
        print(header)
        for i in self.err_container:
            print('\t' + i.get_presentation())
        
    def clear(self):
        self.err_container.clear()

class _ObjectBasicType():
    COMPONENT = 0

    FLOOR = 1
    RAIL = 2
    WOOD = 3
    STOPPER = 4

    DEPTH_CUBE = 5

    DECORATION = 6

    LEVEL_START = 7
    LEVEL_END = 8
    CHECKPOINT = 9
    RESETPOINT = 10

class _NameStandard():
    CKGROUP = 0
    YYC = 1
    IMENGYU = 2

    @staticmethod
    def cvt_std_from_str_to_int(std_str):
        if std_str == "YYC":
            return _NameStandard.YYC
        elif std_str == "IMENGYU":
            return _NameStandard.IMENGYU
        else:
            raise Exception("Unknow name standard.")

class _NameInfoHelper():
    def __init__(self, _basic_type):
        self.basic_type = _basic_type

    # extra field notes:
    # COMPONENT: 
    #       component_type(string)
    #       sector(int)
    # CHECKPOINT, RESETPOINT:
    #       sector(int)(following Ballance index, checkpoint starts with 1)

def _get_selected_objects():
    return bpy.context.view_layer.active_layer_collection.collection.objects

def _get_sector_from_ckgroup(group_set):
    # this counter is served for stupid 
    # multi-sector-grouping accident.
    counter = 0
    last_matched_sector = ''
    for i in group_set:
        regex_result = UTILS_constants.rename_regexCKGroupSector.match(i)
        if regex_result is not None:
            last_matched_sector = regex_result.group(1)
            counter += 1
            
    if counter != 1:
        return None
    else:
        return last_matched_sector


# ========================================== 
# rename core funcs

# NOTE: the implement of this function are copied from 
# BallanceVirtoolsHelper/bvh/features/mapping/grouping.cpp
# ---
# YYC Tools Chains name standard is Ballance-compatible name standard.
# So this functions also serving for `_get_name_info_from_group` function
# to help get sector field from PC/PR elements. In ordinary call(external call)
# The final error output should be outputed normally. But in the call from 
# `_get_name_info_from_group`, this function should not output any error.
# So parameter `call_internal` is served for this work. In common it is False
# to let function output error str normally. But only set it to True in 
# the call from `_get_name_info_from_group` to disable error output.
def _get_name_info_from_yyc_name(obj_name, err_reporter: _RenameErrorReporter, call_internal = False):
    
    # check component first
    regex_result = UTILS_constants.rename_regexYYCComponent.match(obj_name)
    if regex_result is not None:
        data = _NameInfoHelper(_ObjectBasicType.COMPONENT)
        data.component_type = regex_result.group(1)
        data.sector = int(regex_result.group(2))
        return data

    # check PC PR elements
    regex_result = UTILS_constants.rename_regexYYCPC.match(obj_name)
    if regex_result is not None:
        data = _NameInfoHelper(_ObjectBasicType.CHECKPOINT)
        data.sector = int(regex_result.group(1))
        return data
    regex_result = UTILS_constants.rename_regexYYCPR.match(obj_name)
    if regex_result is not None:
        data = _NameInfoHelper(_ObjectBasicType.RESETPOINT)
        data.sector = int(regex_result.group(1))
        return data

    # check other unique elements
    if obj_name == "PS_FourFlames_01":
        return _NameInfoHelper(_ObjectBasicType.LEVEL_START)
    if obj_name == "PE_Balloon_01":
        return _NameInfoHelper(_ObjectBasicType.LEVEL_END)

    # process floors
    if obj_name.startswith("A_Floor"):
        return _NameInfoHelper(_ObjectBasicType.FLOOR)
    if obj_name.startswith("A_Wood"):
        return _NameInfoHelper(_ObjectBasicType.WOOD)
    if obj_name.startswith("A_Rail"):
        return _NameInfoHelper(_ObjectBasicType.RAIL)
    if obj_name.startswith("A_Stopper"):
        return _NameInfoHelper(_ObjectBasicType.STOPPER)

    # process others
    if obj_name.startswith("DepthCubes"):
        return _NameInfoHelper(_ObjectBasicType.DEPTH_CUBE)
    if obj_name.startswith("D_"):
        return _NameInfoHelper(_ObjectBasicType.DECORATION)

    # only output in external calling
    if not call_internal:
        err_reporter.add_error("Name match lost.")

    return None

def _get_name_info_from_imengyu_name(obj_name, err_reporter: _RenameErrorReporter):

    # check component first
    regex_result = UTILS_constants.rename_regexImengyuComponent.match(obj_name)
    if regex_result is not None:
        data = _NameInfoHelper(_ObjectBasicType.COMPONENT)
        data.component_type = regex_result.group(1)
        data.sector = int(regex_result.group(2))
        return data

    # check PC PR elements
    regex_result = UTILS_constants.rename_regexImengyuPCRComp.match(obj_name)
    if regex_result is not None:
        eles_name = regex_result.group(1)
        if eles_name == 'PC_CheckPoint':
            data = _NameInfoHelper(_ObjectBasicType.CHECKPOINT)
        elif eles_name == 'PR_ResetPoint':
            data = _NameInfoHelper(_ObjectBasicType.RESETPOINT)
        data.sector = int(regex_result.group(2))
        return data

    # check other unique elements
    if obj_name == "PS_LevelStart":
        return _NameInfoHelper(_ObjectBasicType.LEVEL_START)
    if obj_name == "PE_LevelEnd":
        return _NameInfoHelper(_ObjectBasicType.LEVEL_END)

    # process floors
    if obj_name.startswith("S_Floors"):
        return _NameInfoHelper(_ObjectBasicType.FLOOR)
    if obj_name.startswith("S_FloorWoods"):
        return _NameInfoHelper(_ObjectBasicType.WOOD)
    if obj_name.startswith("S_FloorRails"):
        return _NameInfoHelper(_ObjectBasicType.RAIL)
    if obj_name.startswith("S_FloorStopper"):
        return _NameInfoHelper(_ObjectBasicType.STOPPER)

    # process others
    if obj_name.startswith("DepthTestCubes"):
        return _NameInfoHelper(_ObjectBasicType.DEPTH_CUBE)
    if obj_name.startswith("O_"):
        return _NameInfoHelper(_ObjectBasicType.DECORATION)

    err_reporter.add_error("Name match lost.")
    return None

def _get_name_info_from_group(obj, err_reporter: _RenameErrorReporter):
    group_list = UTILS_virtools_prop.get_virtools_group_data(obj)
    if len(group_list) == 0:
        # name it as a decoration
        return _NameInfoHelper(_ObjectBasicType.DECORATION)

    group_set = set(group_list)

    # try to filter unique elements first
    set_result = UTILS_constants.rename_uniqueComponentsGroupName.intersection(group_set)
    if len(set_result) == 1:
        # get it
        gotten_group_name = (list(set_result))[0]
        if gotten_group_name == 'PS_Levelstart':
            return _NameInfoHelper(_ObjectBasicType.LEVEL_START)
        elif gotten_group_name == 'PE_Levelende':
            return _NameInfoHelper(_ObjectBasicType.LEVEL_END)
        elif gotten_group_name == 'PC_Checkpoints' or gotten_group_name == 'PR_Resetpoints':
            # these type's data should be gotten from its name
            # use _get_name_info_from_yyc_name to get it
            # _get_name_info_from_yyc_name is Ballance-compatible name standard
            data = _get_name_info_from_yyc_name(obj.name, err_reporter, call_internal=True)
            if data is None:
                err_reporter.add_error("PC_Checkpoints or PR_Resetpoints detected. But couldn't get sector from name.")
                return None
            if data.basic_type != _ObjectBasicType.CHECKPOINT and data.basic_type != _ObjectBasicType.RESETPOINT:
                # check whether it is checkpoint or resetpoint
                # if not, it mean that we got error data from name
                # return None instead
                err_reporter.add_error("PC_Checkpoints or PR_Resetpoints detected. But name is illegal.")
                return None
            # otherwise return data
            return data
        else:
            err_reporter.add_error("The match of Unique Component lost.")
            return None
    elif len(set_result) != 0:
        # must be a weird grouping, report it
        err_reporter.add_error("A Multi-grouping Unique Component.")
        return None

    # distinguish normal elements
    set_result = UTILS_constants.rename_normalComponentsGroupName.intersection(group_set)
    if len(set_result) == 1:
        # get it
        # now try get its sector
        gotten_elements = (tuple(set_result))[0]
        gotten_sector = _get_sector_from_ckgroup(group_set)
        if gotten_sector is None:
            # fail to get sector
            err_reporter.add_error("Component detected. But couldn't get sector from CKGroup data.")
            return None
        
        data = _NameInfoHelper(_ObjectBasicType.COMPONENT)
        data.component_type = gotten_elements
        data.sector = int(gotten_sector)
        return data
    elif len(set_result) != 0:
        # must be a weird grouping, report it
        err_reporter.add_error("A Multi-grouping Component.")
        return None

    # distinguish road
    if 'Phys_FloorRails' in group_set:
        # rail
        return _NameInfoHelper(_ObjectBasicType.RAIL)
    elif 'Phys_Floors' in group_set:
        # distinguish it between Floor and Wood
        floor_result =UTILS_constants.rename_floorGroupTester.intersection(group_set)
        rail_result = UTILS_constants.rename_woodGroupTester.intersection(group_set)
        if len(floor_result) > 0 and len(rail_result) == 0:
            return _NameInfoHelper(_ObjectBasicType.FLOOR)
        elif len(floor_result) == 0 and len(rail_result) > 0:
            return _NameInfoHelper(_ObjectBasicType.WOOD)
        else:
            err_reporter.add_warning("Can't distinguish object between Floors and Rails. Suppose it is Floors.")
            return _NameInfoHelper(_ObjectBasicType.FLOOR)
    elif 'Phys_FloorStopper' in group_set:
        return _NameInfoHelper(_ObjectBasicType.STOPPER)
    elif 'DepthTestCubes' in group_set:
        return _NameInfoHelper(_ObjectBasicType.DEPTH_CUBE)

    # no matched
    err_reporter.add_error("Group match lost.")
    return None

def _set_for_yyc_name(obj, name_info, err_reporter: _RenameErrorReporter):
    basic_type = name_info.basic_type
    if basic_type == _ObjectBasicType.DECORATION:
        obj.name = "D_"

    elif basic_type == _ObjectBasicType.LEVEL_START:
        obj.name = "PS_FourFlames_01"
    elif basic_type == _ObjectBasicType.LEVEL_END:
        obj.name = "PE_Balloon_01"
    elif basic_type == _ObjectBasicType.RESETPOINT:
        obj.name = "PR_Resetpoint_{:0>2d}".format(name_info.sector)
    elif basic_type == _ObjectBasicType.CHECKPOINT:
        obj.name = "PC_TwoFlames_{:0>2d}".format(name_info.sector)

    elif basic_type == _ObjectBasicType.DEPTH_CUBE:
        obj.name = "DepthCubes_"

    elif basic_type == _ObjectBasicType.FLOOR:
        obj.name = "A_Floor_"
    elif basic_type == _ObjectBasicType.WOOD:
        obj.name = "A_Wood_"
    elif basic_type == _ObjectBasicType.RAIL:
        obj.name = "A_Rail_"
    elif basic_type == _ObjectBasicType.STOPPER:
        obj.name = "A_Stopper_"
    
    elif basic_type == _ObjectBasicType.COMPONENT:
        obj.name = "{}_{:0>2d}_".format(name_info.component_type, name_info.sector)
    

def _set_for_imengyu_name(obj, name_info, err_reporter: _RenameErrorReporter):
    basic_type = name_info.basic_type
    if basic_type == _ObjectBasicType.DECORATION:
        obj.name = "O_"

    elif basic_type == _ObjectBasicType.LEVEL_START:
        obj.name = "PS_LevelStart"
    elif basic_type == _ObjectBasicType.LEVEL_END:
        obj.name = "PE_LevelEnd"
    elif basic_type == _ObjectBasicType.RESETPOINT:
        obj.name = "PR_ResetPoint:{:d}".format(name_info.sector)
    elif basic_type == _ObjectBasicType.CHECKPOINT:
        obj.name = "PC_CheckPoint:{:d}".format(name_info.sector + 1)

    elif basic_type == _ObjectBasicType.DEPTH_CUBE:
        obj.name = "DepthTestCubes"

    elif basic_type == _ObjectBasicType.FLOOR:
        obj.name = "S_Floors"
    elif basic_type == _ObjectBasicType.WOOD:
        obj.name = "S_FloorWoods"
    elif basic_type == _ObjectBasicType.RAIL:
        obj.name = "S_FloorRails"
    elif basic_type == _ObjectBasicType.STOPPER:
        obj.name = "S_FloorStopper"
    
    elif basic_type == _ObjectBasicType.COMPONENT:
        obj.name = "{}:{}:{:d}".format(name_info.component_type, obj.name.replace(":", "_"), name_info.sector)
    
# NOTE: the implement of this function are copied from 
# BallanceVirtoolsHelper/bvh/features/mapping/grouping.cpp
def _set_for_group(obj, name_info, err_reporter: _RenameErrorReporter):
    gps = []
    basic_type = name_info.basic_type

    if basic_type == _ObjectBasicType.DECORATION:
        # decoration do not need grouping
        pass

    elif basic_type == _ObjectBasicType.LEVEL_START:
        gps.append("PS_Levelstart")
    elif basic_type == _ObjectBasicType.LEVEL_END:
        gps.append("PE_Levelende")
    elif basic_type == _ObjectBasicType.RESETPOINT:
        gps.append("PC_Checkpoints")
    elif basic_type == _ObjectBasicType.CHECKPOINT:
        gps.append("PR_Resetpoints")

    elif basic_type == _ObjectBasicType.DEPTH_CUBE:
        gps.append("DepthTestCubes")

    elif basic_type == _ObjectBasicType.FLOOR:
        gps.append("Phys_Floors")
        gps.append("Sound_HitID_01")
        gps.append("Sound_RollID_01")
        gps.append("Shadow")
    elif basic_type == _ObjectBasicType.WOOD:
        gps.append("Phys_FloorRails")
        gps.append("Sound_HitID_03")
        gps.append("Sound_RollID_03")
    elif basic_type == _ObjectBasicType.RAIL:
        gps.append("Phys_Floors")
        gps.append("Sound_HitID_02")
        gps.append("Sound_RollID_02")
        gps.append("Shadow")
    elif basic_type == _ObjectBasicType.STOPPER:
        gps.append("Phys_FloorStopper")
    
    elif basic_type == _ObjectBasicType.COMPONENT:
        gps.append(name_info.component_type)

        # set compabitility for 999 sector loader
        if (name_info.sector == 9):
            gps.append("Sector_9")
        else:
            gps.append("Sector_{:0>2d}".format(name_info.sector))
    

    # apply to custom property
    UTILS_virtools_prop.fill_virtools_group_data(obj, tuple(gps))

# ========================================== 
# assemble funcs

def _get_data(obj, standard, err_reporter: _RenameErrorReporter):
    if standard == _NameStandard.YYC:
        return _get_name_info_from_yyc_name(obj.name, err_reporter)
    elif standard == _NameStandard.IMENGYU:
        return _get_name_info_from_imengyu_name(obj.name, err_reporter)
    elif standard == _NameStandard.CKGROUP:
        return _get_name_info_from_group(obj, err_reporter)
    else:
        raise Exception("Unknow standard")

def _set_data(obj, name_info, standard, err_reporter: _RenameErrorReporter):
    if standard == _NameStandard.YYC:
        return _set_for_yyc_name(obj, name_info, err_reporter)
    elif standard == _NameStandard.IMENGYU:
        return _set_for_imengyu_name(obj, name_info, err_reporter)
    elif standard == _NameStandard.CKGROUP:
        return _set_for_group(obj, name_info, err_reporter)
    else:
        raise Exception("Unknow standard")

def _rename_core(source_std, dest_std):
    if source_std == dest_std:
        # if source == dest
        # we do not to do anything
        return

    # create fail counter and error reporter
    failed_obj_counter = 0
    all_obj_counter = 0
    err_reporter = _RenameErrorReporter()

    print('============')
    print('Rename System Report')
    print('------------')
    for obj in _get_selected_objects():
        # set counter and name
        all_obj_counter += 1
        old_name = new_name = obj.name
        # get data
        info = _get_data(obj, source_std, err_reporter)
        
        # do operation according to whether getting data successfully
        if info is None:
            failed_obj_counter += 1
        else:
            _set_data(obj, info, dest_std, err_reporter)
            # refresh obj name
            new_name = obj.name

        # report result
        if err_reporter.can_report():
            if new_name == old_name:
                report_header = 'For object "{}"'.format(new_name)
            else:
                report_header = 'For object "{}" (Old name: "{}")'.format(new_name, old_name)

            err_reporter.report(report_header)
        # clear report
        err_reporter.clear()
        

    print('------------')
    print('All / Failed - {} / {}'.format(all_obj_counter, failed_obj_counter))
    print('============')

    UTILS_functions.show_message_box(
        ('Rename system report',
        'View console to get more detail',
        'All: {}'.format(all_obj_counter),
        'Failed: {}'.format(failed_obj_counter)),
        "Info", UTILS_icons_manager.blender_error_icon
    )