import bpy
from bpy.types import Menu, Panel, UIList
from bpy.props import *

from lnx.lightmapper import operators, properties, utility

import lnx.assets
import lnx.utils

if lnx.is_reload(__name__):
    lnx.assets = lnx.reload_module(lnx.assets)
    lnx.utils = lnx.reload_module(lnx.utils)
else:
    lnx.enable_reload(__name__)


class LnxBakeListItem(bpy.types.PropertyGroup):
    obj: PointerProperty(type=bpy.types.Object, description="The object to bake")
    res_x: IntProperty(name="X", description="Texture resolution", default=1024)
    res_y: IntProperty(name="Y", description="Texture resolution", default=1024)

class LNX_UL_BakeList(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        # We could write some code to decide which icon to use here...
        custom_icon = 'OBJECT_DATAMODE'

        # Make sure your code supports all 3 layout types
        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            row = layout.row()
            row.prop(item, "obj", text="", emboss=False, icon=custom_icon)
            col = row.column()
            col.alignment = 'RIGHT'
            col.label(text=str(item.res_x) + 'x' + str(item.res_y))

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label(text="", icon=custom_icon)

class LnxBakeListNewItem(bpy.types.Operator):
    # Add a new item to the list
    bl_idname = "lnx_bakelist.new_item"
    bl_label = "Add a new item"

    def execute(self, context):
        scn = context.scene
        scn.lnx_bakelist.add()
        scn.lnx_bakelist_index = len(scn.lnx_bakelist) - 1
        return{'FINISHED'}


class LnxBakeListDeleteItem(bpy.types.Operator):
    # Delete the selected item from the list
    bl_idname = "lnx_bakelist.delete_item"
    bl_label = "Deletes an item"

    @classmethod
    def poll(self, context):
        """ Enable if there's something in the list """
        scn = context.scene
        return len(scn.lnx_bakelist) > 0

    def execute(self, context):
        scn = context.scene
        list = scn.lnx_bakelist
        index = scn.lnx_bakelist_index

        list.remove(index)

        if index > 0:
            index = index - 1

        scn.lnx_bakelist_index = index
        return{'FINISHED'}

class LnxBakeListMoveItem(bpy.types.Operator):
    # Move an item in the list
    bl_idname = "lnx_bakelist.move_item"
    bl_label = "Move an item in the list"
    direction: EnumProperty(
                items=(
                    ('UP', 'Up', ""),
                    ('DOWN', 'Down', ""),))

    def move_index(self):
        # Move index of an item render queue while clamping it
        obj = bpy.context.scene
        index = obj.lnx_bakelist_index
        list_length = len(obj.lnx_bakelist) - 1
        new_index = 0

        if self.direction == 'UP':
            new_index = index - 1
        elif self.direction == 'DOWN':
            new_index = index + 1

        new_index = max(0, min(new_index, list_length))
        obj.lnx_bakelist.move(index, new_index)
        obj.lnx_bakelist_index = new_index

    def execute(self, context):
        obj = bpy.context.scene
        list = obj.lnx_bakelist
        index = obj.lnx_bakelist_index

        if self.direction == 'DOWN':
            neighbor = index + 1
            self.move_index()

        elif self.direction == 'UP':
            neighbor = index - 1
            self.move_index()
        else:
            return{'CANCELLED'}
        return{'FINISHED'}

class LnxBakeButton(bpy.types.Operator):
    '''Bake textures for listed objects'''
    bl_idname = 'lnx.bake_textures'
    bl_label = 'Bake'

    def execute(self, context):
        scn = context.scene
        if len(scn.lnx_bakelist) == 0:
            return{'FINISHED'}

        self.report({'INFO'}, "Once baked, hit 'Leenkx Bake - Apply' to pack lightmaps")

        # At least one material required for now..
        for o in scn.lnx_bakelist:
            ob = o.obj
            if len(ob.material_slots) == 0:
                if not 'MaterialDefault' in bpy.data.materials:
                    mat = bpy.data.materials.new(name='MaterialDefault')
                    mat.use_nodes = True
                else:
                    mat = bpy.data.materials['MaterialDefault']
                ob.data.materials.append(mat)

        # Single user materials
        for o in scn.lnx_bakelist:
            ob = o.obj
            for slot in ob.material_slots:
                # Temp material already exists
                if slot.material.name.endswith('_temp'):
                    continue
                n = slot.material.name + '_' + ob.name + '_temp'
                if not n in bpy.data.materials:
                    slot.material = slot.material.copy()
                    slot.material.name = n

        # Images for baking
        for o in scn.lnx_bakelist:
            ob = o.obj
            img_name = ob.name + '_baked'
            sc = scn.lnx_bakelist_scale / 100
            rx = o.res_x * sc
            ry = o.res_y * sc
            # Get image
            if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != rx or bpy.data.images[img_name].size[1] != ry:
                img = bpy.data.images.new(img_name, int(rx), int(ry))
                img.name = img_name # Force img_name (in case Blender picked img_name.001)
            else:
                img = bpy.data.images[img_name]
            for slot in ob.material_slots:
                # Add image nodes
                mat = slot.material
                mat.use_nodes = True
                nodes = mat.node_tree.nodes
                if 'Baked Image' in nodes:
                    img_node = nodes['Baked Image']
                else:
                    img_node = nodes.new('ShaderNodeTexImage')
                    img_node.name = 'Baked Image'
                    img_node.location = (100, 100)
                    img_node.image = img
                img_node.select = True
                nodes.active = img_node

        obs = bpy.context.view_layer.objects

        # Unwrap
        active = obs.active
        for o in scn.lnx_bakelist:
            ob = o.obj
            uv_layers = ob.data.uv_layers
            if not 'UVMap_baked' in uv_layers:
                uvmap = uv_layers.new(name='UVMap_baked')
                uv_layers.active_index = len(uv_layers) - 1
                obs.active = ob
                if scn.lnx_bakelist_unwrap == 'Lightmap Pack':
                    bpy.context.view_layer.objects.active = ob
                    ob.select_set(True)
                    bpy.ops.uv.lightmap_pack(PREF_CONTEXT='ALL_FACES')
                else:
                    bpy.ops.object.mode_set(mode='OBJECT')
                    bpy.ops.object.select_all(action='DESELECT')
                    bpy.context.view_layer.objects.active = ob
                    ob.select_set(True)
                    bpy.ops.object.mode_set(mode='EDIT')
                    bpy.ops.mesh.select_all(action='SELECT')
                    bpy.ops.uv.smart_project()
                    bpy.ops.mesh.select_all(action='DESELECT')
                    bpy.ops.object.mode_set(mode='OBJECT')
            else:
                for i in range(0, len(uv_layers)):
                    if uv_layers[i].name == 'UVMap_baked':
                        uv_layers.active_index = i
                        break
        obs.active = active

        # Materials for runtime
        # TODO: use single mat per object
        for o in scn.lnx_bakelist:
            ob = o.obj
            img_name = ob.name + '_baked'
            for slot in ob.material_slots:
                n = slot.material.name[:-5] + '_baked'
                if not n in bpy.data.materials:
                    mat = bpy.data.materials.new(name=n)
                    mat.use_nodes = True
                    mat.use_fake_user = True
                    nodes = mat.node_tree.nodes
                    img_node = nodes.new('ShaderNodeTexImage')
                    img_node.name = 'Baked Image'
                    img_node.location = (100, 100)
                    img_node.image = bpy.data.images[img_name]
                    mat.node_tree.links.new(img_node.outputs[0], nodes['Principled BSDF'].inputs[0])
                else:
                    mat = bpy.data.materials[n]
                    nodes = mat.node_tree.nodes
                    nodes['Baked Image'].image = bpy.data.images[img_name]

        # Bake
        bpy.ops.object.select_all(action='DESELECT')
        for o in scn.lnx_bakelist:
            o.obj.select_set(True)
        obs.active = scn.lnx_bakelist[0].obj
        bpy.ops.object.bake('INVOKE_DEFAULT', type='COMBINED')
        bpy.ops.object.select_all(action='DESELECT')

        return{'FINISHED'}

class LnxBakeApplyButton(bpy.types.Operator):
    '''Pack baked textures and restore materials'''
    bl_idname = 'lnx.bake_apply'
    bl_label = 'Apply'

    def execute(self, context):
        scn = context.scene
        if len(scn.lnx_bakelist) == 0:
            return{'FINISHED'}
        for material in bpy.data.materials:
            if not material.users:
                bpy.data.materials.remove(material)

        # Remove leftover _baked materials for removed objects
        for mat in bpy.data.materials:
            if mat.name.endswith('_baked'):
                has_user = False
                for ob in bpy.data.objects:
                    if ob.type == 'MESH' and mat.name.endswith('_' + ob.name + '_baked'):
                        has_user = True
                        break
                if not has_user:
                    bpy.data.materials.remove(mat, do_unlink=True)
        # Recache lightmaps
        lnx.assets.invalidate_unpacked_data(None, None)
        for o in scn.lnx_bakelist:
            ob = o.obj
            img_name = ob.name + '_baked'
            # Save images
            bpy.data.images[img_name].pack()
            #bpy.data.images[img_name].save()
            for slot in ob.material_slots:
                mat = slot.material
                # Remove temp material
                if mat.name.endswith('_temp'):
                    old = slot.material
                    slot.material = bpy.data.materials[old.name.split('_' + ob.name)[0] + "_" + ob.name + "_baked"]
                    bpy.data.materials.remove(old, do_unlink=True)
        # Restore uv slots
        for o in scn.lnx_bakelist:
            ob = o.obj
            uv_layers = ob.data.uv_layers
            uv_layers.active_index = 0
            uv_layers["UVMap_baked"].active_render = True

        return{'FINISHED'}

class LnxBakeSpecialsMenu(bpy.types.Menu):
    bl_label = "Bake"
    bl_idname = "LNX_MT_BakeListSpecials"

    def draw(self, context):
        layout = self.layout
        layout.operator("lnx.bake_add_all")
        layout.operator("lnx.bake_add_selected")
        layout.operator("lnx.bake_clear_all")
        layout.operator("lnx.bake_remove_baked_materials")

class LnxBakeAddAllButton(bpy.types.Operator):
    '''Fill the list with scene objects'''
    bl_idname = 'lnx.bake_add_all'
    bl_label = 'Add All'

    def execute(self, context):
        scn = context.scene
        scn.lnx_bakelist.clear()
        for ob in scn.objects:
            if ob.type == 'MESH':
                scn.lnx_bakelist.add().obj = ob
        return{'FINISHED'}

class LnxBakeAddSelectedButton(bpy.types.Operator):
    '''Add selected objects to the list'''
    bl_idname = 'lnx.bake_add_selected'
    bl_label = 'Add Selected'

    def contains(self, scn, ob):
        for o in scn.lnx_bakelist:
            if o == ob:
                return True
        return False

    def execute(self, context):
        scn = context.scene
        for ob in context.selected_objects:
            if ob.type == 'MESH' and not self.contains(scn, ob):
                scn.lnx_bakelist.add().obj = ob
        return{'FINISHED'}

class LnxBakeClearAllButton(bpy.types.Operator):
    '''Clear the list'''
    bl_idname = 'lnx.bake_clear_all'
    bl_label = 'Clear'

    def execute(self, context):
        scn = context.scene
        scn.lnx_bakelist.clear()
        return{'FINISHED'}

class LnxBakeRemoveBakedMaterialsButton(bpy.types.Operator):
    '''Clear the list'''
    bl_idname = 'lnx.bake_remove_baked_materials'
    bl_label = 'Remove Baked Materials'

    def execute(self, context):
        for mat in bpy.data.materials:
            if mat.name.endswith('_baked'):
                bpy.data.materials.remove(mat, do_unlink=True)
        return{'FINISHED'}


__REG_CLASSES = (
    LnxBakeListItem,
    LNX_UL_BakeList,
    LnxBakeListNewItem,
    LnxBakeListDeleteItem,
    LnxBakeListMoveItem,
    LnxBakeButton,
    LnxBakeApplyButton,
    LnxBakeSpecialsMenu,
    LnxBakeAddAllButton,
    LnxBakeAddSelectedButton,
    LnxBakeClearAllButton,
    LnxBakeRemoveBakedMaterialsButton
)
__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES)


def register():
    __reg_classes()

    bpy.types.Scene.lnx_bakelist_scale = FloatProperty(
        name="Resolution", description="Resolution scale", subtype='PERCENTAGE',
        default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0
    )
    bpy.types.Scene.lnx_bakelist = CollectionProperty(type=LnxBakeListItem)
    bpy.types.Scene.lnx_bakelist_index = IntProperty(name="Index for my_list", default=0)
    bpy.types.Scene.lnx_bakelist_unwrap = EnumProperty(
        name="UV Unwrap", default='Smart UV Project',
        items=[
            ('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'),
            ('Smart UV Project', 'Smart UV Project', 'Smart UV Project')
        ]
    )

    # Register lightmapper
    bpy.types.Scene.lnx_bakemode = EnumProperty(
        name="Bake mode", default='Static Map',
        items=[
            ('Static Map', 'Static Map', 'Static Map'),
            ('Lightmap', 'Lightmap', 'Lightmap')
        ]
    )

    operators.register()
    properties.register()


def unregister():
    __unreg_classes()

    # Unregister lightmapper
    operators.unregister()
    properties.unregister()