import bpy
from bpy_extras.wm_utils.progress_report import ProgressReport
import tempfile, os, typing, re
from . import PROP_preferences, UTIL_ioport_shared
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_icons_manager
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture
from .PyBMap import bmap_wrapper as bmap

# define global tex save opt blender enum prop helper
_g_EnumHelper_CK_TEXTURE_SAVEOPTIONS: UTIL_virtools_types.EnumPropHelper = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS)

class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtoolsFile, UTIL_ioport_shared.ExportParams, UTIL_ioport_shared.VirtoolsParams):
    """Export Virtools File"""
    bl_idname = "bbp.export_virtools"
    bl_label = "Export Virtools File"
    bl_options = {'PRESET'}

    texture_save_opt: bpy.props.EnumProperty(
        name = "Global Texture Save Options",
        description = "Decide how texture saved if texture is specified as Use Global as its Save Options.",
        items = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.generate_items(),
        default = _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.to_selection(UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS.CKTEXTURE_EXTERNAL)
    )

    use_compress: bpy.props.BoolProperty(
        name="Use Compress",
        default = True,
    )

    compress_level: bpy.props.IntProperty(
        name = "Compress Level",
        description = "The ZLib compress level used by Virtools Engine when saving composition.",
        min = 1, max = 9,
        default = 5,
    )

    @classmethod
    def poll(self, context):
        return (
            PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder()
            and bmap.is_bmap_available())
    
    def execute(self, context):
        # check selecting first
        objls: tuple[bpy.types.Object] | None = self.general_get_export_objects()
        if objls is None:
            UTIL_functions.message_box(
                ('No selected target!', ), 
                'Lost Parameters', 
                UTIL_icons_manager.BlenderPresetIcons.Error.value
            )
            return {'CANCELLED'}

        # start exporting
        with UTIL_ioport_shared.ExportEditModeBackup() as editmode_guard:
            _export_virtools(
                self.general_get_filename(),
                self.general_get_vt_encodings(),
                _g_EnumHelper_CK_TEXTURE_SAVEOPTIONS.get_selection(self.texture_save_opt),
                self.use_compress,
                self.compress_level,
                objls
            )

        self.report({'INFO'}, "Virtools File Exporting Finished.")
        return {'FINISHED'}
    
    def draw(self, context):
        layout = self.layout
        layout.label(text = 'Export Target')
        self.draw_export_params(layout.box())

        layout.separator()
        layout.label(text = 'Virtools Params')
        box = layout.box()
        self.draw_virtools_params(box)

        box.separator()
        box.label(text = 'Global Texture Save Option')
        box.prop(self, 'texture_save_opt', text = '')

        box.separator()
        box.prop(self, 'use_compress')
        if self.use_compress:
            box.prop(self, 'compress_level')

_TObj3dPair = tuple[bpy.types.Object, bmap.BM3dObject]
_TMeshPair = tuple[bpy.types.Object, bpy.types.Mesh, bmap.BMMesh]
_TMaterialPair = tuple[bpy.types.Material, bmap.BMMaterial]
_TTexturePair = tuple[bpy.types.Image, bmap.BMTexture]

def _export_virtools(
        file_name_: str, 
        encodings_: tuple[str], 
        texture_save_opt_: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS,
        use_compress_: bool,
        compress_level_: int, 
        export_objects: tuple[bpy.types.Object, ...]
    ) -> None:

    # create temp folder
    with tempfile.TemporaryDirectory() as vt_temp_folder:
        print(f'Virtools Engine Temp: {vt_temp_folder}')

        # create virtools reader context
        with bmap.BMFileWriter(
            vt_temp_folder,
            PROP_preferences.get_raw_preferences().mBallanceTextureFolder,
            encodings_) as writer:

            # prepare progress reporter
            with ProgressReport(wm = bpy.context.window_manager) as progress:
                # prepare 3dobject
                obj3d_crets: tuple[_TObj3dPair, ...] = _prepare_virtools_3dobjects(
                    writer, progress, export_objects)
                # export group and 3dobject by prepared 3dobject
                _export_virtools_groups(writer, progress, obj3d_crets)
                mesh_crets: tuple[_TMeshPair, ...] = _export_virtools_3dobjects(
                    writer, progress, obj3d_crets)
                # export mesh
                material_crets: tuple[_TMaterialPair, ...] = _export_virtools_meshes(
                    writer, progress, mesh_crets)
                # export material
                texture_crets: tuple[_TTexturePair, ...] = _export_virtools_materials(
                    writer, progress, material_crets)
                # export texture
                _export_virtools_textures(writer, progress, vt_temp_folder, texture_crets)

                # save document
                _save_virtools_document(
                    writer, progress, file_name_, texture_save_opt_, use_compress_, compress_level_)

def _prepare_virtools_3dobjects(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        export_objects: tuple[bpy.types.Object]
        ) -> tuple[_TObj3dPair, ...]:
    # this function only create equvalent entries in virtools engine and do not export anything
    # because _export_virtools_3dobjects() and _export_virtools_groups() are need use the return value of this function

    # create 3dobject hashset and result
    obj3d_crets: list[_TObj3dPair] = []
    obj3d_cret_set: set[bpy.types.Object] = set()
    # start saving
    progress.enter_substeps(len(export_objects), "Creating 3dObjects")

    for obj3d in export_objects:
        if obj3d not in obj3d_cret_set:
            # add into set
            obj3d_cret_set.add(obj3d)
            # create virtools instance
            vtobj3d: bmap.BM3dObject = writer.create_3dobject()
            # add into result list
            obj3d_crets.append((obj3d, vtobj3d))
        
        # step progress no matter whether create new one
        progress.step()

    # leave progress and return
    progress.leave_substeps()
    return tuple(obj3d_crets)

def _export_virtools_groups(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        obj3d_crets: tuple[_TObj3dPair, ...]
        ) -> None:
    # create virtools group
    group_cret_map: dict[str, bmap.BMGroup] = {}
    # start saving
    progress.enter_substeps(len(obj3d_crets), "Saving Groups")

    # create group exporting helper
    group_cret_guard: VirtoolsGroupCreationGuard = VirtoolsGroupCreationGuard(writer)

    for obj3d, vtobj3d in obj3d_crets:
        # open group visitor
        with PROP_virtools_group.VirtoolsGroupsHelper(obj3d) as gp_visitor:
            for gp_name in gp_visitor.iterate_groups():
                # get group or create new group from guard
                vtgroup: bmap.BMGroup | None = group_cret_map.get(gp_name, None)
                if vtgroup is None:
                    # note: no need to set name, guard has set it.
                    vtgroup = group_cret_guard.create_group(gp_name)
                    group_cret_map[gp_name] = vtgroup
                
                # group this object
                vtgroup.add_object(vtobj3d)

        # leave group visitor and step
        progress.step()

    # leave progress and return
    progress.leave_substeps()

def _export_virtools_3dobjects(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        obj3d_crets: tuple[_TObj3dPair, ...]
        ) -> tuple[_TMeshPair, ...]:
    # create virtools mesh
    mesh_crets: list[_TMeshPair] = []
    mesh_cret_map: dict[bpy.types.Mesh, bmap.BMMesh] = {}
    # start saving
    progress.enter_substeps(len(obj3d_crets), "Saving 3dObjects")

    for obj3d, vtobj3d in obj3d_crets:
        # set name
        vtobj3d.set_name(obj3d.name)

        # check mesh
        mesh: bpy.types.Mesh | None = obj3d.data
        if mesh is not None:
            # get existing vt mesh or create new one
            vtmesh: bmap.BMMesh | None = mesh_cret_map.get(mesh, None)
            if vtmesh is None:
                vtmesh = writer.create_mesh()
                mesh_crets.append((obj3d, mesh, vtmesh))
                mesh_cret_map[mesh] = vtmesh

            # assign mesh
            vtobj3d.set_current_mesh(vtmesh)
        else:
            vtobj3d.set_current_mesh(None)

        # set world matrix
        vtmat: UTIL_virtools_types.VxMatrix = UTIL_virtools_types.VxMatrix()
        UTIL_virtools_types.vxmatrix_from_blender(vtmat, obj3d.matrix_world)
        UTIL_virtools_types.vxmatrix_conv_co(vtmat)
        vtobj3d.set_world_matrix(vtmat)

        # set visibility
        vtobj3d.set_visibility(not obj3d.hide_get())

        # step
        progress.step()

    # leave progress and return
    progress.leave_substeps()
    return tuple(mesh_crets)

def _export_virtools_meshes(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        mesh_crets: tuple[_TMeshPair, ...]
        ) -> tuple[_TMaterialPair, ...]:
    # create virtools mesh
    material_crets: list[_TMaterialPair] = []
    material_cret_map: dict[bpy.types.Material, bmap.BMMaterial] = {}
    # start saving
    progress.enter_substeps(len(mesh_crets), "Saving Meshes")

    # iterate meshes
    for obj3d, mesh, vtmesh in mesh_crets:
        # we need use temporary mesh function to visit triangulated meshes
        # so we ignore mesh factor and use obj3d to create temp mesh to get data
        # open temp mesh helper
        with UTIL_blender_mesh.TemporaryMesh(obj3d) as tempmesh:
            # sync mesh name, lit mode
            vtmesh.set_name(mesh.name)
            mesh_settings: PROP_virtools_mesh.RawVirtoolsMesh = PROP_virtools_mesh.get_raw_virtools_mesh(mesh)
            vtmesh.set_lit_mode(mesh_settings.mLitMode)

            # sync mesh main data
            # open mesh visitor
            with UTIL_blender_mesh.MeshReader(tempmesh.get_temp_mesh()) as mesh_visitor:
                # construct data provider
                def pos_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]:
                    for v in mesh_visitor.get_vertex_position():
                        UTIL_virtools_types.vxvector3_conv_co(v)
                        yield v
                def nml_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]:
                    for v in mesh_visitor.get_vertex_normal():
                        UTIL_virtools_types.vxvector3_conv_co(v)
                        yield v
                def uv_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector2]:
                    for v in mesh_visitor.get_vertex_uv():
                        UTIL_virtools_types.vxvector2_conv_co(v)
                        yield v

                # construct mtl slot
                def mtl_iterator() -> typing.Iterator[bmap.BMMaterial | None]:
                    for mtl in mesh_visitor.get_material_slot():
                        if mtl is None: yield None
                        else:
                            # get existing one or create new one
                            vtmaterial: bmap.BMMaterial | None = material_cret_map.get(mtl, None)
                            if vtmaterial is None:
                                vtmaterial = writer.create_material()
                                material_crets.append((mtl, vtmaterial))
                                material_cret_map[mtl] = vtmaterial
                            # yield data
                            yield vtmaterial

                def face_idx_iterator(idx_type: int) -> typing.Iterator[UTIL_virtools_types.CKFaceIndices]:
                    data: UTIL_virtools_types.CKFaceIndices = UTIL_virtools_types.CKFaceIndices()
                    for fidx in mesh_visitor.get_face():
                        # swap indices
                        fidx.conv_co()
                        # set data by specific index
                        match(idx_type):
                            case 0:  data.i1, data.i2, data.i3 = fidx.mIndices[0].mPosIdx, fidx.mIndices[1].mPosIdx, fidx.mIndices[2].mPosIdx
                            case 1:  data.i1, data.i2, data.i3 = fidx.mIndices[0].mNmlIdx, fidx.mIndices[1].mNmlIdx, fidx.mIndices[2].mNmlIdx
                            case 2:  data.i1, data.i2, data.i3 = fidx.mIndices[0].mUvIdx, fidx.mIndices[1].mUvIdx, fidx.mIndices[2].mUvIdx
                            case _: raise UTIL_functions.BBPException('invalid index type.')
                        # yield data
                        yield data
                def face_mtl_iterator() -> typing.Iterator[int]:
                    for fidx in mesh_visitor.get_face():
                        yield fidx.mMtlIdx
                
                # create virtools mesh transition
                # and write into mesh
                with bmap.BMMeshTrans() as mesh_trans:
                    # prepare vertices
                    mesh_trans.prepare_vertex(
                        mesh_visitor.get_vertex_position_count(),
                        pos_iterator()
                    )
                    mesh_trans.prepare_normal(
                        mesh_visitor.get_vertex_normal_count(),
                        nml_iterator()
                    )
                    mesh_trans.prepare_uv(
                        mesh_visitor.get_vertex_uv_count(),
                        uv_iterator()
                    )
                    # prepare mtl slots
                    mesh_trans.prepare_mtl_slot(
                        mesh_visitor.get_material_slot_count(),
                        mtl_iterator()
                    )
                    # prepare face
                    mesh_trans.prepare_face(
                        mesh_visitor.get_face_count(),
                        face_idx_iterator(0),
                        face_idx_iterator(1),
                        face_idx_iterator(2),
                        face_mtl_iterator()
                    )

                    # parse to vtmesh
                    mesh_trans.parse(writer, vtmesh)

                # end of mesh trans
            # end of mesh visitor
        # end of temp mesh

        # step
        progress.step()

    # leave progress and return
    progress.leave_substeps()
    return tuple(material_crets)

def _export_virtools_materials(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        material_crets: tuple[_TMaterialPair, ...]
        ) -> tuple[_TTexturePair, ...]:
    # create virtools mesh
    texture_crets: list[_TTexturePair] = []
    texture_cret_map: dict[bpy.types.Image, bmap.BMTexture] = {}
    # start saving
    progress.enter_substeps(len(material_crets), "Saving Materials")

    for mtl, vtmaterial in material_crets:
        # set name
        vtmaterial.set_name(mtl.name)

        # get raw mtl
        rawmtl: PROP_virtools_material.RawVirtoolsMaterial = PROP_virtools_material.get_raw_virtools_material(mtl)

        # apply vt material
        vtmaterial.set_diffuse(rawmtl.mDiffuse)
        vtmaterial.set_ambient(rawmtl.mAmbient)
        vtmaterial.set_specular(rawmtl.mSpecular)
        vtmaterial.set_emissive(rawmtl.mEmissive)
        vtmaterial.set_specular_power(rawmtl.mSpecularPower)

        # apply assoc texture
        if rawmtl.mTexture is not None:
            # create or get new one vt texture
            vttexture: bmap.BMTexture | None = texture_cret_map.get(rawmtl.mTexture, None)
            if vttexture is None:
                vttexture = writer.create_texture()
                texture_cret_map[rawmtl.mTexture] = vttexture
                texture_crets.append((rawmtl.mTexture, vttexture))
            # assign texture
            vtmaterial.set_texture(vttexture)
        else:
            vtmaterial.set_texture(None)
        
        vtmaterial.set_texture_border_color(rawmtl.mTextureBorderColor)

        vtmaterial.set_texture_blend_mode(rawmtl.mTextureBlendMode)
        vtmaterial.set_texture_min_mode(rawmtl.mTextureMinMode)
        vtmaterial.set_texture_mag_mode(rawmtl.mTextureMagMode)
        vtmaterial.set_texture_address_mode(rawmtl.mTextureAddressMode)

        vtmaterial.set_source_blend(rawmtl.mSourceBlend)
        vtmaterial.set_dest_blend(rawmtl.mDestBlend)
        vtmaterial.set_fill_mode(rawmtl.mFillMode)
        vtmaterial.set_shade_mode(rawmtl.mShadeMode)

        vtmaterial.set_alpha_test_enabled(rawmtl.mEnableAlphaTest)
        vtmaterial.set_alpha_blend_enabled(rawmtl.mEnableAlphaBlend)
        vtmaterial.set_perspective_correction_enabled(rawmtl.mEnablePerspectiveCorrection)
        vtmaterial.set_z_write_enabled(rawmtl.mEnableZWrite)
        vtmaterial.set_two_sided_enabled(rawmtl.mEnableTwoSided)

        vtmaterial.set_alpha_ref(rawmtl.mAlphaRef)
        vtmaterial.set_alpha_func(rawmtl.mAlphaFunc)
        vtmaterial.set_z_func(rawmtl.mZFunc)

        # step
        progress.step()

    # leave progress and return
    progress.leave_substeps()
    return tuple(texture_crets)

def _export_virtools_textures(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        vt_temp_folder: str,
        texture_crets: tuple[_TTexturePair, ...]
        ) -> None:
    # start saving
    progress.enter_substeps(len(texture_crets), "Saving Textures")

    for tex, vttexture in texture_crets:
        # set name
        vttexture.set_name(tex.name)

        # set texture cfg
        rawtex: PROP_virtools_texture.RawVirtoolsTexture = PROP_virtools_texture.get_raw_virtools_texture(tex)
        vttexture.set_save_options(rawtex.mSaveOptions)
        vttexture.set_video_format(rawtex.mVideoFormat)

        # save core texture
        # load ballance textures to vt engine from external ref path
        # load other textures to vt engine from temp folder.
        # no need to distinguish save options
        try_filepath: str | None = UTIL_ballance_texture.get_ballance_texture_filename(
            UTIL_ballance_texture.get_texture_filepath(tex))
        if try_filepath is None:
            # non-ballance file, save in temp and change file path to point to it.
            try_filepath = UTIL_ballance_texture.generate_other_texture_save_path(tex, vt_temp_folder)
            UTIL_ballance_texture.save_other_texture(tex, try_filepath)
        # load into vt engine
        vttexture.load_image(try_filepath)

        # step
        progress.step()

    # leave progress and return
    progress.leave_substeps()

def _save_virtools_document(
        writer: bmap.BMFileWriter,
        progress: ProgressReport,
        file_name: str,
        texture_save_opt: UTIL_virtools_types.CK_TEXTURE_SAVEOPTIONS,
        use_compress: bool,
        compress_level: int
        ) -> None:
    
    progress.enter_substeps(1, "Saving Document")
    writer.save(file_name, texture_save_opt, use_compress, compress_level)
    progress.step()
    progress.leave_substeps()
    
class VirtoolsGroupCreationGuard():
    """
    This class is designed for ensure that the created sector group is successive.

    Due to the design of Ballance, Ballance rely on checking the existance of Sector_XX to get how many sectors this map have.
    Thus if there are no component in a sector, it still need to create a empty Sector_XX group, otherwise the game will crash
    or be ended at a accident sector.
    
    This class hook the operation of Virtools group creation and check all Sector group creation.
    Create essential group to make Sector_XX group successive.
    Thus all group creation in this module should be passed by this class.
    """
    cRegexGroupSector: typing.ClassVar[re.Pattern] = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')

    @staticmethod
    def __get_group_index(group_name: str) -> int | None:
        """
        Return the sector index of group name if it is. Otherwise None.
        """
        regex_result = VirtoolsGroupCreationGuard.cRegexGroupSector.match(group_name)
        if regex_result is not None:
            return int(regex_result.group(1))
        else:
            return None
        
    @staticmethod
    def __get_group_name(group_index: int) -> str:
        """
        Output Sector group name by given sector index
        """
        if group_index == 9:
           return 'Sector_9'
        else:
            return f'Sector_{group_index:0>2d}'

    __mWriter: bmap.BMFileWriter
    __mSectors: list[bmap.BMGroup]

    def __init__(self, assoc_writer: bmap.BMFileWriter):
        self.__mWriter = assoc_writer
        self.__mSectors = []

    def create_group(self, group_name: str) -> bmap.BMGroup:
        """
        The hooked group creation function.

        Please note the passed group name argument is just for name checking.
        This function will set group name for you, not like BMFileWriter.create_group() operated.
        """
        # check whether it is sector group
        # note: the return sector index is 1 based, not 0
        sector_idx: int | None = VirtoolsGroupCreationGuard.__get_group_index(group_name)
        # if it is regular group, return normal creation
        if sector_idx is None:
            gp: bmap.BMGroup = self.__mWriter.create_group()
            gp.set_name(group_name)
            return gp
        
        # get from sector cahce list
        # enlarge sector cache list if it is not fulfilled given sector index
        while sector_idx > len(self.__mSectors):
            gp: bmap.BMGroup = self.__mWriter.create_group()
            self.__mSectors.append(gp)
            gp.set_name(self.__get_group_name(len(self.__mSectors)))
        # return ordered sector from sector caches
        return self.__mSectors[sector_idx - 1]

def register() -> None:
    bpy.utils.register_class(BBP_OT_export_virtools)

def unregister() -> None:
    bpy.utils.unregister_class(BBP_OT_export_virtools)