import bpy.ops as O
import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh, shutil, glob, uuid
from io import StringIO
from threading  import Thread
from queue import Queue, Empty
from dataclasses import dataclass
from dataclasses import field
from typing import List

###########################################################
###########################################################
# This set of utility functions are courtesy of LorenzWieseke
#
# Modified by Naxela
#   
# https://github.com/Naxela/The_Lightmapper/tree/Lightmap-to-GLB
###########################################################

class Node_Types:
    output_node = 'OUTPUT_MATERIAL'
    ao_node = 'AMBIENT_OCCLUSION'
    image_texture = 'TEX_IMAGE'
    pbr_node = 'BSDF_PRINCIPLED'
    diffuse = 'BSDF_DIFFUSE'
    mapping = 'MAPPING'
    normal_map = 'NORMAL_MAP'
    bump_map = 'BUMP'
    attr_node = 'ATTRIBUTE'

class Shader_Node_Types:
    emission = "ShaderNodeEmission"
    image_texture = "ShaderNodeTexImage"
    mapping = "ShaderNodeMapping"
    normal = "ShaderNodeNormalMap"
    ao = "ShaderNodeAmbientOcclusion"
    uv = "ShaderNodeUVMap"
    mix = "ShaderNodeMixRGB"

def select_object(self,obj):
    C = bpy.context
    try:
        O.object.select_all(action='DESELECT')
        C.view_layer.objects.active = obj
        obj.select_set(True)
    except:
        self.report({'INFO'},"Object not in View Layer")


def select_obj_by_mat(self,mat):
    D = bpy.data
    for obj in D.objects:
        if obj.type == "MESH":
            object_materials = [
                slot.material for slot in obj.material_slots]
            if mat in object_materials:
                select_object(self,obj)


def save_image(image):

    filePath = bpy.data.filepath
    path = os.path.dirname(filePath)

    try:
        os.mkdir(path + "/tex")
    except FileExistsError:
        pass

    try:
        os.mkdir(path + "/tex/" + str(image.size[0]))
    except FileExistsError:
        pass

    if image.file_format == "JPEG" :
        file_ending = ".jpg"
    elif image.file_format == "PNG" :
        file_ending = ".png"
    
    savepath = path + "/tex/" + \
        str(image.size[0]) + "/" + image.name + file_ending

    image.filepath_raw = savepath
    
    image.save()

def get_file_size(filepath):
    size = "Unpack Files"
    try:
        path = bpy.path.abspath(filepath)
        size = os.path.getsize(path)
        size /= 1024
    except:
        if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
            print("error getting file path for " + filepath)
        
    return (size)


def scale_image(image, newSize):
    if (image.org_filepath != ''):
        image.filepath = image.org_filepath

    image.org_filepath = image.filepath
    image.scale(newSize[0], newSize[1])
    save_image(image)


def check_only_one_pbr(self,material):
    check_ok = True
    # get pbr shader
    nodes = material.node_tree.nodes
    pbr_node_type = Node_Types.pbr_node
    pbr_nodes = find_node_by_type(nodes,pbr_node_type)

    # check only one pbr node
    if len(pbr_nodes) == 0:
        self.report({'INFO'}, 'No PBR Shader Found')
        check_ok = False

    if len(pbr_nodes) > 1:
        self.report({'INFO'}, 'More than one PBR Node found ! Clean before Baking.')
        check_ok = False

    return check_ok

# is material already the baked one
def check_is_org_material(self,material):     
    check_ok = True   
    if "_Bake" in material.name:
        self.report({'INFO'}, 'Change back to org. Material')
        check_ok = False
    
    return check_ok


def clean_empty_materials(self):
    for obj in bpy.context.scene.objects:
        for slot in obj.material_slots:
            mat = slot.material
            if mat is None:
                if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
                    print("Removed Empty Materials from " + obj.name)
                bpy.ops.object.select_all(action='DESELECT')
                obj.select_set(True)
                bpy.ops.object.material_slot_remove()

def get_pbr_inputs(pbr_node):

    base_color_input = pbr_node.inputs["Base Color"]
    metallic_input = pbr_node.inputs["Metallic"]
    specular_input = pbr_node.inputs["Specular"]
    roughness_input = pbr_node.inputs["Roughness"]
    normal_input = pbr_node.inputs["Normal"]

    pbr_inputs = {"base_color_input":base_color_input, "metallic_input":metallic_input,"specular_input":specular_input,"roughness_input":roughness_input,"normal_input":normal_input}
    return pbr_inputs    

def find_node_by_type(nodes, node_type):
    nodes_found = [n for n in nodes if n.type == node_type]
    return nodes_found

def find_node_by_type_recusivly(material, note_to_start, node_type, del_nodes_inbetween=False):
    nodes = material.node_tree.nodes
    if note_to_start.type == node_type:
        return note_to_start

    for input in note_to_start.inputs:
        for link in input.links:
            current_node = link.from_node
            if (del_nodes_inbetween and note_to_start.type != Node_Types.normal_map and note_to_start.type != Node_Types.bump_map):
                nodes.remove(note_to_start)
            return find_node_by_type_recusivly(material, current_node, node_type, del_nodes_inbetween)


def find_node_by_name_recusivly(node, idname):
    if node.bl_idname == idname:
        return node

    for input in node.inputs:
        for link in input.links:
            current_node = link.from_node
            return find_node_by_name_recusivly(current_node, idname)

def make_link(material, socket1, socket2):
    links = material.node_tree.links
    links.new(socket1, socket2)


def add_gamma_node(material, pbrInput):
    nodeToPrincipledOutput = pbrInput.links[0].from_socket

    gammaNode = material.node_tree.nodes.new("ShaderNodeGamma")
    gammaNode.inputs[1].default_value = 2.2
    gammaNode.name = "Gamma Bake"

    # link in gamma
    make_link(material, nodeToPrincipledOutput, gammaNode.inputs["Color"])
    make_link(material, gammaNode.outputs["Color"], pbrInput)


def remove_gamma_node(material, pbrInput):
    nodes = material.node_tree.nodes
    gammaNode = nodes.get("Gamma Bake")
    nodeToPrincipledOutput = gammaNode.inputs[0].links[0].from_socket

    make_link(material, nodeToPrincipledOutput, pbrInput)
    material.node_tree.nodes.remove(gammaNode)

def apply_ao_toggle(self,context): 
    all_materials = bpy.data.materials
    ao_toggle = context.scene.toggle_ao
    for mat in all_materials:
        nodes = mat.node_tree.nodes
        ao_node = nodes.get("AO Bake")
        if ao_node is not None:
            if ao_toggle:
                emission_setup(mat,ao_node.outputs["Color"])
            else:
                pbr_node = find_node_by_type(nodes,Node_Types.pbr_node)[0]   
                remove_node(mat,"Emission Bake")
                reconnect_PBR(mat, pbr_node)
        

def emission_setup(material, node_output):
    nodes = material.node_tree.nodes
    emission_node = add_node(material,Shader_Node_Types.emission,"Emission Bake")

    # link emission to whatever goes into current pbrInput
    emission_input = emission_node.inputs[0]
    make_link(material, node_output, emission_input)

    # link emission to materialOutput
    surface_input = nodes.get("Material Output").inputs[0]
    emission_output = emission_node.outputs[0]
    make_link(material, emission_output, surface_input)

def link_pbr_to_output(material,pbr_node):
    nodes = material.node_tree.nodes
    surface_input = nodes.get("Material Output").inputs[0]
    make_link(material,pbr_node.outputs[0],surface_input)

    
def reconnect_PBR(material, pbrNode):
    nodes = material.node_tree.nodes
    pbr_output = pbrNode.outputs[0]
    surface_input = nodes.get("Material Output").inputs[0]
    make_link(material, pbr_output, surface_input)

def mute_all_texture_mappings(material, do_mute):
    nodes = material.node_tree.nodes
    for node in nodes:
        if node.bl_idname == "ShaderNodeMapping":
            node.mute = do_mute

def add_node(material,shader_node_type,node_name):
    nodes = material.node_tree.nodes
    new_node = nodes.get(node_name)
    if new_node is None:
        new_node = nodes.new(shader_node_type)
        new_node.name = node_name
        new_node.label = node_name
    return new_node

def remove_node(material,node_name):
    nodes = material.node_tree.nodes
    node = nodes.get(node_name)
    if node is not None:
        nodes.remove(node)

def lightmap_to_ao(material,lightmap_node):
        nodes = material.node_tree.nodes
        # -----------------------AO SETUP--------------------#
        # create group data
        gltf_settings = bpy.data.node_groups.get('glTF Settings')
        if gltf_settings is None:
            bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree')
        
        # add group to node tree
        ao_group = nodes.get('glTF Settings')
        if ao_group is None:
            ao_group = nodes.new('ShaderNodeGroup')
            ao_group.name = 'glTF Settings'
            ao_group.node_tree = bpy.data.node_groups['glTF Settings']

        # create group inputs
        if ao_group.inputs.get('Occlusion') is None:
            ao_group.inputs.new('NodeSocketFloat','Occlusion')

        # mulitply to control strength
        mix_node = add_node(material,Shader_Node_Types.mix,"Adjust Lightmap")
        mix_node.blend_type = "MULTIPLY"
        mix_node.inputs["Fac"].default_value = 1
        mix_node.inputs["Color2"].default_value = [3,3,3,1]

        # position node
        ao_group.location = (lightmap_node.location[0]+600,lightmap_node.location[1])
        mix_node.location = (lightmap_node.location[0]+300,lightmap_node.location[1])
    
        make_link(material,lightmap_node.outputs['Color'],mix_node.inputs['Color1'])
        make_link(material,mix_node.outputs['Color'],ao_group.inputs['Occlusion'])


###########################################################
###########################################################
# This utility function is modified from blender_xatlas
# and calls the object without any explicit object context
# thus allowing blender_xatlas to pack from background.
###########################################################
# Code is courtesy of mattedicksoncom
# Modified by Naxela
#
# https://github.com/mattedicksoncom/blender-xatlas/
###########################################################

def gen_safe_name():
    genId = uuid.uuid4().hex
    # genId = "u_" + genId.replace("-","_")
    return "u_" + genId

def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj):

    blender_xatlas = importlib.util.find_spec("blender_xatlas")

    if blender_xatlas is not None:
        import blender_xatlas
    else:
        return 0

    packOptions = bpy.context.scene.pack_tool
    chartOptions = bpy.context.scene.chart_tool

    sharedProperties = bpy.context.scene.shared_properties
    #sharedProperties.unwrapSelection

    context = bpy.context

    #save whatever mode the user was in
    startingMode = bpy.context.object.mode
    selected_objects = bpy.context.selected_objects

    #check something is actually selected
    #external function/operator will select them
    if len(selected_objects) == 0:
        print("Nothing Selected")
        self.report({"WARNING"}, "Nothing Selected, please select Something")
        return {'FINISHED'}

    #store the names of objects to be lightmapped
    rename_dict = dict()
    safe_dict = dict()

    #make sure all the objects have ligthmap uvs
    for obj in selected_objects:
        if obj.type == 'MESH':
            safe_name = gen_safe_name();
            rename_dict[obj.name] = (obj.name,safe_name)
            safe_dict[safe_name] = obj.name
            context.view_layer.objects.active = obj
            if obj.data.users > 1:
                obj.data = obj.data.copy() #make single user copy
            uv_layers = obj.data.uv_layers

            #setup the lightmap uvs
            uvName = "UVMap_Lightmap"
            if sharedProperties.lightmapUVChoiceType == "NAME":
                uvName = sharedProperties.lightmapUVName
            elif sharedProperties.lightmapUVChoiceType == "INDEX":
                if sharedProperties.lightmapUVIndex < len(uv_layers):
                    uvName = uv_layers[sharedProperties.lightmapUVIndex].name

            if not uvName in uv_layers:
                uvmap = uv_layers.new(name=uvName)
                uv_layers.active_index = len(uv_layers) - 1
            else:
                for i in range(0, len(uv_layers)):
                    if uv_layers[i].name == uvName:
                        uv_layers.active_index = i
            obj.select_set(True)

    #save all the current edges
    if sharedProperties.packOnly:
        edgeDict = dict()
        for obj in selected_objects:
            if obj.type == 'MESH':
                tempEdgeDict = dict()
                tempEdgeDict['object'] = obj.name
                tempEdgeDict['edges'] = []
                print(len(obj.data.edges))
                for i in range(0,len(obj.data.edges)):
                    setEdge = obj.data.edges[i]
                    tempEdgeDict['edges'].append(i)
                edgeDict[obj.name] = tempEdgeDict

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY')
    else:
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY')

    bpy.ops.object.mode_set(mode='OBJECT')

    #Create a fake obj export to a string
    #Will strip this down further later
    fakeFile = StringIO()
    blender_xatlas.export_obj_simple.save(
        rename_dict=rename_dict,
        context=bpy.context,
        filepath=fakeFile,
        mainUVChoiceType=sharedProperties.mainUVChoiceType,
        uvIndex=sharedProperties.mainUVIndex,
        uvName=sharedProperties.mainUVName,
        use_selection=True,
        use_animation=False,
        use_mesh_modifiers=True,
        use_edges=True,
        use_smooth_groups=False,
        use_smooth_groups_bitflags=False,
        use_normals=True,
        use_uvs=True,
        use_materials=False,
        use_triangles=False,
        use_nurbs=False,
        use_vertex_groups=False,
        use_blen_objects=True,
        group_by_object=False,
        group_by_material=False,
        keep_vertex_order=False,
    )

    #print just for reference
    # print(fakeFile.getvalue())

    #get the path to xatlas
    #file_path = os.path.dirname(os.path.abspath(__file__))
    scriptsDir = os.path.join(bpy.utils.user_resource('SCRIPTS'), "addons")
    file_path = os.path.join(scriptsDir, "blender_xatlas")
    if platform.system() == "Windows":
        xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender.exe")
    elif platform.system() == "Linux":
        xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender")
        #need to set permissions for the process on linux
        subprocess.Popen(
            'chmod u+x "' + xatlas_path + '"',
            shell=True
        )

    #setup the arguments to be passed to xatlas-------------------
    arguments_string = ""
    for argumentKey in packOptions.__annotations__.keys():
        key_string = str(argumentKey)
        if argumentKey is not None:
            print(getattr(packOptions,key_string))
            attrib = getattr(packOptions,key_string)
            if type(attrib) == bool:
                if attrib == True:
                    arguments_string = arguments_string + " -" + str(argumentKey)
            else:
                arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib)

    for argumentKey in chartOptions.__annotations__.keys():
        if argumentKey is not None:
            key_string = str(argumentKey)
            print(getattr(chartOptions,key_string))
            attrib = getattr(chartOptions,key_string)
            if type(attrib) == bool:
                if attrib == True:
                    arguments_string = arguments_string + " -" + str(argumentKey)
            else:
                arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib)

    #add pack only option
    if sharedProperties.packOnly:
        arguments_string = arguments_string + " -packOnly"

    arguments_string = arguments_string + " -atlasLayout" + " " + sharedProperties.atlasLayout

    print(arguments_string)
    #END setup the arguments to be passed to xatlas-------------------

    #RUN xatlas process
    xatlas_process = subprocess.Popen(
        r'"{}"'.format(xatlas_path) + ' ' + arguments_string,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        shell=True
    )

    print(xatlas_path)

    #shove the fake file in stdin
    stdin = xatlas_process.stdin
    value = bytes(fakeFile.getvalue() + "\n", 'UTF-8') #The \n is needed to end the input properly
    stdin.write(value)
    stdin.flush()

    #Get the output from xatlas
    outObj = ""
    while True:
        output = xatlas_process.stdout.readline()
        if not output:
                break
        outObj = outObj + (output.decode().strip() + "\n")

    #the objects after xatlas processing
    # print(outObj)


    #Setup for reading the output
    @dataclass
    class uvObject:
        obName: string = ""
        uvArray: List[float] = field(default_factory=list)
        faceArray: List[int] = field(default_factory=list)

    convertedObjects = []
    uvArrayComplete = []


    #search through the out put for STARTOBJ
    #then start reading the objects
    obTest = None
    startRead = False
    for line in outObj.splitlines():

        line_split = line.split()

        if not line_split:
            continue

        line_start = line_split[0]  # we compare with this a _lot_
        # print(line_start)
        if line_start == "STARTOBJ":
            print("Start reading the objects----------------------------------------")
            startRead = True
            # obTest = uvObject()

        if startRead:
            #if it's a new obj
            if line_start == 'o':
                #if there is already an object append it
                if obTest is not None:
                    convertedObjects.append(obTest)

                obTest = uvObject() #create new uv object
                obTest.obName = line_split[1]

            if obTest is not None:
                #the uv coords
                if line_start == 'vt':
                    newUv = [float(line_split[1]),float(line_split[2])]
                    obTest.uvArray.append(newUv)
                    uvArrayComplete.append(newUv)

                #the face coords index
                #faces are 1 indexed
                if line_start == 'f':
                    #vert/uv/normal
                    #only need the uvs
                    newFace = [
                        int(line_split[1].split("/")[1]),
                        int(line_split[2].split("/")[1]),
                        int(line_split[3].split("/")[1])
                    ]
                    obTest.faceArray.append(newFace)

    #append the final object
    convertedObjects.append(obTest)
    print(convertedObjects)


    #apply the output-------------------------------------------------------------
    #copy the uvs to the original objects
    # objIndex = 0
    print("Applying the UVs----------------------------------------")
    # print(convertedObjects)
    for importObject in convertedObjects:
        bpy.ops.object.select_all(action='DESELECT')

        obTest = importObject
        obTest.obName = safe_dict[obTest.obName] #probably shouldn't just replace it
        bpy.context.scene.objects[obTest.obName].select_set(True)
        context.view_layer.objects.active = bpy.context.scene.objects[obTest.obName]
        bpy.ops.object.mode_set(mode = 'OBJECT')

        obj = bpy.context.active_object
        me = obj.data
        #convert to bmesh to create the new uvs
        bm = bmesh.new()
        bm.from_mesh(me)

        uv_layer = bm.loops.layers.uv.verify()

        nFaces = len(bm.faces)
        #need to ensure lookup table for some reason?
        if hasattr(bm.faces, "ensure_lookup_table"):
            bm.faces.ensure_lookup_table()

        #loop through the faces
        for faceIndex in range(nFaces):
            faceGroup = obTest.faceArray[faceIndex]

            bm.faces[faceIndex].loops[0][uv_layer].uv = (
                uvArrayComplete[faceGroup[0] - 1][0],
                uvArrayComplete[faceGroup[0] - 1][1])

            bm.faces[faceIndex].loops[1][uv_layer].uv = (
                uvArrayComplete[faceGroup[1] - 1][0],
                uvArrayComplete[faceGroup[1] - 1][1])

            bm.faces[faceIndex].loops[2][uv_layer].uv = (
                uvArrayComplete[faceGroup[2] - 1][0],
                uvArrayComplete[faceGroup[2] - 1][1])

            # objIndex = objIndex + 3

        # print(objIndex)
        #assign the mesh back to the original mesh
        bm.to_mesh(me)
    #END apply the output-------------------------------------------------------------


    #Start setting the quads back again-------------------------------------------------------------
    if sharedProperties.packOnly:
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='DESELECT')
        bpy.ops.object.mode_set(mode='OBJECT')

        for edges in edgeDict:
            edgeList = edgeDict[edges]
            currentObject = bpy.context.scene.objects[edgeList['object']]
            bm = bmesh.new()
            bm.from_mesh(currentObject.data)
            if hasattr(bm.edges, "ensure_lookup_table"):
                bm.edges.ensure_lookup_table()

            #assume that all the triangulated edges come after the original edges
            newEdges = []
            for edge in range(len(edgeList['edges']), len(bm.edges)):
                newEdge = bm.edges[edge]
                newEdge.select = True
                newEdges.append(newEdge)

            bmesh.ops.dissolve_edges(bm, edges=newEdges, use_verts=False, use_face_split=False)
            bpy.ops.object.mode_set(mode='OBJECT')
            bm.to_mesh(currentObject.data)
            bm.free()
            bpy.ops.object.mode_set(mode='EDIT')

    #End setting the quads back again-------------------------------------------------------------

    #select the original objects that were selected
    for objectName in rename_dict:
        if objectName[0] in bpy.context.scene.objects:
            current_object = bpy.context.scene.objects[objectName[0]]
            current_object.select_set(True)
            context.view_layer.objects.active = current_object

    bpy.ops.object.mode_set(mode=startingMode)

    print("Finished Xatlas----------------------------------------")
    return {'FINISHED'}

def transfer_assets(copy, source, destination):
    for filename in glob.glob(os.path.join(source, '*.*')):
        try:
            shutil.copy(filename, destination)
        except shutil.SameFileError:
            pass

def transfer_load():
    load_folder = bpy.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_SceneProperties.tlm_load_folder))
    lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
    print(load_folder)
    print(lightmap_folder)
    transfer_assets(True, load_folder, lightmap_folder)