import os
from pathlib import Path
from time import perf_counter

import bpy
from bpy_extras.io_utils import axis_conversion
from mathutils import Quaternion, Vector

# RAGE: forward=Y, up=Z
rage_to_blender = axis_conversion(from_forward='Y', from_up='Z',
                                  to_forward='Y', to_up='Z').to_4x4()
blender_to_rage = rage_to_blender.inverted()

# -------------------- UI --------------------

def update_show_names(self, context):
    show_names = context.scene.opl_importer_settings.show_names
    instance_collection = bpy.data.collections.get('Instances')
    if instance_collection is None: return

    for obj in instance_collection.all_objects:
        obj.show_name = show_names

def update_show_names_missing(self, context):
    show_names_missing = context.scene.opl_importer_settings.show_names_missing
    instance_collection = bpy.data.collections.get('Instances')
    if instance_collection is None: return

    for obj in instance_collection.all_objects:
        if obj.instance_collection is not None: continue
        obj.show_name = show_names_missing

def update_show_axes(self, context):
    show_axes = context.scene.opl_importer_settings.show_axes
    instance_collection = bpy.data.collections.get('Instances')
    if instance_collection is None: return

    for obj in instance_collection.all_objects:
        obj.show_axis = show_axes

def update_show_axes_missing(self, context):
    show_axes_missing = context.scene.opl_importer_settings.show_axes_missing
    instance_collection = bpy.data.collections.get('Instances')
    if instance_collection is None: return

    for obj in instance_collection.all_objects:
        if obj.instance_collection is not None: continue
        obj.show_axis = show_axes_missing

def update_hide_lights(self, context):
    hide_lights = context.scene.opl_importer_settings.hide_lights
    objects_collection = bpy.data.collections.get('Objects')
    if objects_collection is None: return

    for obj in objects_collection.all_objects:
        if obj.type == "LIGHT":
            obj.hide_viewport = hide_lights

class OPLOperatorsSettings(bpy.types.PropertyGroup):
    opl_import_folder_path: bpy.props.StringProperty(subtype="DIR_PATH", name="Import Folder", description="Folder with .opl files you want to import")
#    asset_folder_path: bpy.props.StringProperty(subtype="DIR_PATH")
    opl_export_folder_path: bpy.props.StringProperty(subtype="DIR_PATH", name="Export Folder", description="Folder to export .opl files to")
    show_names: bpy.props.BoolProperty(update=update_show_names)
    show_names_missing: bpy.props.BoolProperty(update=update_show_names_missing)
    show_axes: bpy.props.BoolProperty(update=update_show_axes)
    show_axes_missing: bpy.props.BoolProperty(update=update_show_axes_missing)
    hide_lights: bpy.props.BoolProperty(update=update_hide_lights)
    

class IplItem(bpy.types.PropertyGroup):
    flags: bpy.props.IntProperty(name="Flags", default=0)
    lod_index: bpy.props.IntProperty(name="LOD Index", default=-1)
    unk1: bpy.props.IntProperty(name="UnkInt", default=-1)
    unk2: bpy.props.IntProperty(name="UnkInt2", default=-1)


class IplAttributesPanel(bpy.types.Panel):
    """Creates a Panel in the scene context of the properties editor"""
    bl_label = "Ipl"
    bl_idname = "EMPTY_PT_ipl"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "data"

    @classmethod
    def poll(cls, context):
        return context.object.type == "EMPTY"

    def draw(self, context):
        layout = self.layout
        attr = context.object.attr
        layout.prop(attr, "flags")
        layout.prop(attr, "lod_index")
        layout.prop(attr, "unk1")
        layout.prop(attr, "unk2")


class IplToolsPanel(bpy.types.Panel):
    bl_label = "Panel"
    bl_idname = "VIEW3D_PT_IPL"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "IPL"
    
    def draw(self, context):
        layout = self.layout
        scene = context.scene
        opl_importer_settings = context.scene.opl_importer_settings
        layout.prop(opl_importer_settings, "opl_import_folder_path")
        layout.prop(opl_importer_settings, "opl_export_folder_path")
#        layout.prop(opl_importer_settings, "asset_folder_path", text="Asset folder")
        layout.operator("iv.import_opl")
        layout.operator("iv.export_opl")
        grid = layout.grid_flow(row_major=True, columns=2)
        grid.prop(opl_importer_settings, "show_names", text="Show Names")
        grid.prop(opl_importer_settings, "show_axes", text="Show Axes")
        grid.label(text="Not perfect, but useful")
        grid.separator()
        grid.prop(opl_importer_settings, "show_names_missing", text="Show Names for Missing Models")
        grid.prop(opl_importer_settings, "show_axes_missing", text="Show Axes for Missing Models")
        grid.prop(opl_importer_settings, "hide_lights", text="Hide Lights")


# -------------------- Importer --------------------

def get_objects_by_name(name: str, arr):
    return [obj for obj in arr if name == obj.name.rsplit('.', maxsplit=1)[0]]


def import_opl(opl_folder: Path):
    if 'Instances' not in bpy.data.collections:
        collection = bpy.data.collections.new('Instances')
        bpy.context.scene.collection.children.link(collection)
    instance_collection = bpy.data.collections['Instances']

    if 'Objects' not in bpy.data.collections:
        collection = bpy.data.collections.new('Objects')
        bpy.context.scene.collection.children.link(collection)
    objects_collection = bpy.data.collections['Objects']

    for obj in objects_collection.objects:
        name = obj.name
        if not (name.endswith('.odr') or name.endswith('.oft')): continue
        name = obj.name.rsplit('.', maxsplit=1)[0]
        collection = objects_collection.children.get(name)
        if collection is None:
            collection = bpy.data.collections.new(name)
            objects_collection.children.link(collection)
        objects_collection.objects.unlink(obj)
        collection.objects.link(obj)
        for sub in obj.children_recursive:
            #print(sub)
            collection.objects.link(sub)

    ipl_end = bpy.context.scene.get("ipl_end", {})
    start = perf_counter()
    for opl in opl_folder.glob('*.opl'):
        #print(opl)
        with open(opl, encoding='utf8') as f:
            datalines = f.readlines()
    #    i = 0
        opl_collection_name = opl.stem
        opl_collection = instance_collection.children.get(opl_collection_name)
        if opl_collection is None:
            opl_collection = bpy.data.collections.new(opl_collection_name)
            instance_collection.children.link(opl_collection)
        
        for index, line in enumerate(datalines):
            if line.startswith('#'): continue
            if line.startswith('end'): break
            data = line.split(', ')
            if len(data) < 2: continue
            
            model_name = data[7].casefold()
            empty_name = f"{index}_{model_name}"
            
    #        assert model_name not in objects_collection
            
            loc = [float(x) for x in data[:3]]
            location = rage_to_blender @ Vector(loc)
            rot = [float(x) for x in data[3:7]]
            rot = (rage_to_blender @ Quaternion([rot[3], rot[0], rot[1], rot[2]]).inverted().to_matrix().to_4x4()).to_euler()
            
            empty = get_objects_by_name(empty_name, opl_collection.all_objects)
            if empty:
                assert len(empty) == 1
                empty = empty[0]
            else:
                empty = bpy.data.objects.new(empty_name, None)
                opl_collection.objects.link(empty)
            
            
            empty.location = location
            empty.rotation_euler = rot
            empty.empty_display_size = 0
            
            empty.attr.flags = int(data[8])
            empty.attr.lod_index = int(data[9])
            empty.attr.unk1 = int(data[10])
            empty.attr.unk2 = int(data[11])
            
            empty.instance_type = 'COLLECTION'
            empty.instance_collection = objects_collection.children.get(model_name)
    #        empty.show_axis = True
    #        if i == 2:
    #            break
    #        i+= 1
            

        ipl_end[opl.stem] = ''.join(datalines[index+1:])
    bpy.context.scene["ipl_end"] = ipl_end
        
    print("TOTAL TIME: ", perf_counter()-start, "sec")
    
class OplImporterButton(bpy.types.Operator):
    bl_idname = "iv.import_opl"
    bl_label = "Import"

    @classmethod
    def poll(cls, context):
        path = context.scene.opl_importer_settings.opl_import_folder_path
        return path != '' and os.path.exists(path)

    def execute(self, context):
        import_opl(Path(context.scene.opl_importer_settings.opl_import_folder_path))
        return {'FINISHED'}

# -------------------- Exporter --------------------

def format_float(f: float) -> str:
    # get rid of -0s
    if f == 0:
        f = 0
    # Limit to 8 decimals
    s = f"{f:.8G}"  # 'g' automatically switches to scientific notation if needed
    return s

def to_csv(tupl) -> str:
    return ', '.join(format_float(elem) for elem in tupl)

def export_opl(export_folder: Path):
    instance_collection = bpy.data.collections.get('Instances')
    if instance_collection is None: return

    for collection in instance_collection.children:
        with open(export_folder / (collection.name + ".opl"), 'w') as writer:
            writer.writelines(("# GTA IV Binary Placement File\n", "# Generated with clueless' opl_import_export script\n", "version 3\n" "inst\n"))
            for obj in collection.objects:
                loc = to_csv(blender_to_rage @ obj.location)
                quat = tuple(round(i, 8) for i in (blender_to_rage @ obj.rotation_euler.to_matrix().to_4x4()).to_quaternion().inverted())
                rot = to_csv((quat[1], quat[2], quat[3], quat[0]))
                model_name = obj.name.split('_', maxsplit=1)[1].rsplit('.', maxsplit=1)[0]
                attr = obj.attr
                flags = str(attr.flags)
                lod_index = str(attr.lod_index)
                unk1 = str(attr.unk1)
                unk2 = str(attr.unk2)
                writer.write(', '.join((loc, rot, model_name, flags, lod_index, unk1, unk2)))
                writer.write('\n')
            writer.write("end\n")
            writer.write(bpy.context.scene["ipl_end"].get(collection.name, ''))
    


class OplExporterButton(bpy.types.Operator):
    bl_idname = "iv.export_opl"
    bl_label = "Export"

    @classmethod
    def poll(cls, context):
        path = context.scene.opl_importer_settings.opl_export_folder_path
        return path != '' and os.path.exists(path)

    def execute(self, context):
        export_opl(Path(context.scene.opl_importer_settings.opl_export_folder_path))
        return {'FINISHED'}

#

classes = (
    # UI
    OPLOperatorsSettings,
    IplItem,
    IplAttributesPanel,
    IplToolsPanel,
    # Importer
    OplImporterButton,
    # Exporter
    OplExporterButton,
)
def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    bpy.types.Scene.opl_importer_settings = bpy.props.PointerProperty(type=OPLOperatorsSettings)
    bpy.types.Object.attr = bpy.props.PointerProperty(type=IplItem)

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)

    del bpy.types.Object.attr
    del bpy.types.Scene.opl_importer_settings

if __name__ == "__main__":
    register()
