bl_info = {
    "name": "Darkspore BMDL Importer",
    "author": "emd4600",
    "blender": (2, 80, 0),
    "version": (0, 0, 1),
    "location": "File > Import-Export",
    "warning": "",
    "description": "Import Darkspore .bmdl model format.",
    "wiki_url": "https://github.com/emd4600/SporeModder-Blender-Addons#features",
    "tracker_url": "https://github.com/emd4600/SporeModder-Blender-Addons/issues/new",
    "category": "Import-Export"
}

# Important!
# Don't believe anything I've written. My initial supposition is probably wrong.
# The header has some offsets that point to "sections". Those sections are always made with an offset and X number
# of data -- that data is usually about the offset of the next section --. The value of X depends on the data it's
# representing - I don't know if it follow any arbitrary order or there is some kind of section type identifier
# somewhere; this is the main problem I've had reading these files
#
# Usually, there's only one vertex/triangle buffer, which can be structured in multiple meshes; I think this is able to
# import them.
# Some models have multiple vertex and triangle buffers (and vertex format too). I have no idea how these work


import os
import bpy
import struct
from bpy_extras.io_utils import ImportHelper
from bpy_extras.io_utils import unpack_face_list

imported_objects = []

def readByte(file, endian='<'):
    return struct.unpack(endian + 'b', file.read(1))[0]


def readUByte(file, endian='<'):
    return struct.unpack(endian + 'B', file.read(1))[0]


def readShort(file, endian='<'):
    return struct.unpack(endian + 'h', file.read(2))[0]


def readUShort(file, endian='<'):
    return struct.unpack(endian + 'H', file.read(2))[0]


def readInt(file, endian='<'):
    return struct.unpack(endian + 'i', file.read(4))[0]


def readUInt(file, endian='<'):
    return struct.unpack(endian + 'I', file.read(4))[0]

def readFloat(file, endian='<'):
    try:
        print("File position before reading float:", file.tell())
        data = file.read(4)
        print("Data length:", len(data))
        if len(data) == 0:
            return None
        value = struct.unpack(endian + 'f', data)[0]
        print("Read float:", value)
        return value
    except struct.error:
        print("Error reading float at file position:", file.tell())
        raise

def readBoolean(file, endian='<'):
    return struct.unpack(endian + '?', file.read(1))[0]


def readString(file):
    stringBytes = bytearray()
    byte = readUByte(file)
    while byte != 0:
        stringBytes.append(byte)
        byte = readUByte(file)
    return stringBytes.decode('latin-1')


def expect(valueToExpect, expectedValue, errorString, file):
    if valueToExpect != expectedValue:
        if not useWarnings:
            raise NameError(errorString + "\t" + str(file.tell()))
        else:
            bpy.ops.error.message('INVOKE_DEFAULT', type="Error", message=errorString + "\t" + str(file.tell()))


def loadTexture(mesh, textureType, material, file):
    name = BMDLShaderParamString.getParameter(mesh["shaderStringParams"], textureType)
    if name is not None:
        slot = material.texture_slots.add()
        offset = BMDLShaderParamFloat.getParameter(mesh["shaderFloatParams"], "OffsetUV")
        if offset is not None:
            slot.offset.x = offset.values[0]
            slot.offset.y = offset.values[1]
        scale = BMDLShaderParamFloat.getParameter(mesh["shaderFloatParams"], "TileUV")
        if offset is not None:
            slot.scale.x = scale.values[0]
            slot.scale.y = scale.values[1]

        realPath = "%s\\%s.dds" % (os.path.dirname(file.name), name.value.name)
        img = None
        try:
            img = bpy.data.images.load(realPath)
        except:
            print("Couldn't load texture " + realPath)

        tex = bpy.data.textures.new(name.value.name, type='IMAGE')
        tex.image = img
        slot.texture = tex
        slot.texture_coords = 'UV'


def importBMDL(file):

    # The order in which sections appear. The question is, do they really appear in that order or is this specified by
    # something else?
    sectionTypes = [BMDLSectionBounds, BMDLSectionHash, BMDLSectionInt, BMDLSectionInt, BMDLSectionName,
                    BMDLSectionObject]

    sectionVariables = ["bounds", "hash", "unkInt1", "unkInt2", "name",
                        "shader",
                        # Extra names, put here to show in the log
                        "vertexFormatOffset", "vertexBufferOffset", "meshInfo", "shaderName"]

    meshSectionVariables = ["int1", "int2", "int3", "bounds2", "objectInfo", "shaderFloatParams", "shaderStringParams",
                            "bounds", "unkInt", "firstIndex", "indicesCount"]

    # int1 -> offset to shader float parameters
    # int2 -> offset to shader float parameters values
    # int3 -> offset to shader string parameters

    # a mesh is int1, int2, int3, bounds2 (sometimes without floats), objectInfo

    # For some weird reason, it's like some sections are meant to use the previous section data,
    # even if they are not related
    orderedSections = []
    sections = {}
    meshes = []
    imported_objects = []
    objects = []
    vertexFormat = None
    try:

        header = BMDLHeader()
        header.read(file)

        count = min(header.offsetsCount, len(sectionTypes))

        for i in range(0, count):
            file.seek(header.headerSize + header.offsets[i])
            if sectionTypes[i] == BMDLSectionObject:
                sections[sectionVariables[i]] = sectionTypes[i](file, header.headerSize)
            else:
                sections[sectionVariables[i]] = sectionTypes[i](file)

            orderedSections.append(sections[sectionVariables[i]])

        offsetInd = count

        # Read meshes

        for i in range(0, sections["hash"].count):
            mesh = {}

            file.seek(header.headerSize + header.offsets[offsetInd])
            orderedSections.append(BMDLSectionInt(file))
            mesh["int1"] = orderedSections[-1]
            offsetInd += 1

            file.seek(header.headerSize + header.offsets[offsetInd])
            orderedSections.append(BMDLSectionInt(file))
            mesh["int2"] = orderedSections[-1]
            offsetInd += 1

            file.seek(header.headerSize + header.offsets[offsetInd])
            orderedSections.append(BMDLSectionInt(file))
            mesh["int3"] = orderedSections[-1]
            offsetInd += 1

            file.seek(header.headerSize + header.offsets[offsetInd])
            if sections["hash"].count == 1:
                orderedSections.append(BMDLSectionBounds2(file))
                mesh["bounds2"] = orderedSections[-1]
            else:
                orderedSections.append(readInt(file))
                mesh["bounds2"] = orderedSections[-1]
            offsetInd += 1

            file.seek(header.headerSize + header.offsets[offsetInd])
            orderedSections.append(BMDLSectionObject(file, header.headerSize))
            mesh["objectInfo"] = orderedSections[-1]
            offsetInd += 1

            meshes.append(mesh)

        file.seek(header.headerSize + header.offsets[offsetInd])
        orderedSections.append(BMDLSectionOffset(file))
        sections["vertexFormatOffset"] = orderedSections[-1]
        offsetInd += 1

        file.seek(header.headerSize + header.offsets[offsetInd])
        orderedSections.append(BMDLSectionOffset(file))
        sections["vertexBufferOffset"] = orderedSections[-1]
        offsetInd += 1

        file.seek(header.headerSize + header.offsets[offsetInd])
        orderedSections.append(BMDLSectionMesh(file))
        sections["meshInfo"] = orderedSections[-1]
        offsetInd += 1

        file.seek(header.headerSize + header.offsets[offsetInd])
        orderedSections.append(BMDLSectionSimpleName(file))
        sections["shaderName"] = orderedSections[-1]
        offsetInd += 1

        # We read the section parameters

        for mesh in meshes:
            shaderProperties = []
            numParams = orderedSections[orderedSections.index(mesh["int1"])-1].count
            print("numParams: " + str(numParams))
            print(header.offsets[offsetInd])
            for i in range(0, numParams):
                file.seek(header.headerSize + header.offsets[offsetInd])
                shaderProperties.append(BMDLShaderParamFloat(file, header.headerSize))
                offsetInd += 1

            # The next offset points to the shader parameters values
            address = header.headerSize + mesh["int2"].dataOffset
            for shaderParam in shaderProperties:
                shaderParam.readValues(file, address)
                print(shaderParam.name + "\t" + str(shaderParam.values))

            mesh["shaderFloatParams"] = shaderProperties

            shaderStringParams = []
            # is it always 8 ?
            # Just a guess, unkInt4 has the textures count and unkInt5 the vertex channels count
            for i in range(0, mesh["int2"].unk + mesh["int3"].unk):
                file.seek(header.headerSize + header.offsets[offsetInd+1])
                value = BMDLShaderParamString(file, header.headerSize)

                file.seek(header.headerSize + header.offsets[offsetInd])
                shaderStringParam = BMDLShaderParamString(file, header.headerSize, value)

                offsetInd += 2

                shaderStringParams.append(shaderStringParam)
                print(shaderStringParam.name + "\t" + str(shaderStringParam.value.name))

            mesh["shaderStringParams"] = shaderStringParams

        # Here we read the model data

        vertices = []
        triangles = []

        file.seek(header.headerSize + sections["vertexFormatOffset"].dataOffset)
        vertexFormat = BMDLVertexFormat(file)

        file.seek(header.headerSize + sections["vertexBufferOffset"].dataOffset)
        for i in range(0, sections["meshInfo"].vertexCount):
            vertex = BMDLVertex()
            vertex.read(file, vertexFormat)
            vertices.append(vertex)

        file.seek(header.headerSize + sections["meshInfo"].dataOffset)
        for i in range(0, sections["meshInfo"].triangleCount):
            triangles.append((readUShort(file), readUShort(file), readUShort(file)))

        file.seek(header.headerSize + sections["shaderName"].dataOffset)

        for mesh in meshes:
            bounds = []
            for i in range(0, 8):
                bounds.append(readFloat(file))
            mesh["bounds"] = bounds
            mesh["unkInt"] = readInt(file)  # ?
            mesh["firstIndex"] = readInt(file)
            mesh["indicesCount"] = readInt(file)

    finally:
        pass
        # Write log
        debugFile = open("C:\\BMDL\\" + os.path.basename(file.name) + ".txt", "w")
        try:
            for s in range(len(sectionVariables)):
                debugFile.write(sectionVariables[s] + ":\t" + str(sections[sectionVariables[s]]) + "\n")
        
            for mesh in meshes:
                for s in meshSectionVariables:
                    if s in mesh:
                        debugFile.write("mesh " + s + ":\t" + str(mesh[s]) + "\n")
        
            if vertexFormat is not None:
                debugFile.write("vertexFormat:\t" + str(vertexFormat) + "\n")
        
        finally:
            debugFile.close()

    # Add data to Blender

    filename = os.path.splitext(os.path.basename(file.name))[0]

    m = bpy.data.meshes.new(filename)
    obj = bpy.data.objects.new(filename, m)

    imported_objects.append(obj)

    bpy.context.collection.objects.link(obj)
    bpy.context.view_layer.objects.active = obj

    # Add vertices and faces (triangles)
    verts = [vertex.pos for vertex in vertices]
    faces = triangles
    m.from_pydata(verts, [], faces)

    # Add UV coordinates
    uv_layer = m.uv_layers.new()
    for i, polygon in enumerate(m.polygons):
        for j in range(len(polygon.loop_indices)):
            loop_index = polygon.loop_indices[j]
            uv_layer.data[loop_index].uv = vertices[triangles[i][j]].uv

    if BMDLVertex.readColor in vertexFormat.fmt:
        colorLayer = m.vertex_colors.new(name="Col")

        m.update()

        for t in range(0, sections["meshInfo"].triangleCount):
            for i in range(0, 3):
                colorLayer.data[t*3 + i].color = BMDLVertex.decodeColor(vertices[triangles[t][i]].color)

    m.validate()
    obj.update_tag()
    bpy.context.view_layer.update()

    for mesh in meshes:
        material = bpy.data.materials.new(mesh["objectInfo"].name)
        
        # Set the material properties
        diffuseColor = BMDLShaderParamFloat.getParameter(mesh["shaderFloatParams"], "DiffuseTint")
        material.diffuse_color = diffuseColor.values[0:4] if diffuseColor is not None else (1, 1, 1, 1)
        material.use_nodes = True
        
        # Create a principled BSDF node and connect it to the material output
        material_output = material.node_tree.nodes.get('Material Output')
        principled_node = material.node_tree.nodes.new('ShaderNodeBsdfPrincipled')
        material.node_tree.links.new(principled_node.outputs['BSDF'], material_output.inputs['Surface'])
        
        # Set other material properties as desired
        
        # Assign the material to the mesh
        m.materials.append(material)
        mesh["material"] = material
        
    bpy.ops.object.mode_set(mode='OBJECT')
    for mesh in meshes:
        print(mesh["material"].name)
        print(bpy.data.materials.find(mesh["material"].name))
        print("firstTri: " + str(mesh["firstIndex"]//3))
        print("triCount: " + str(mesh["indicesCount"]//3))
        print(range(mesh["firstIndex"]//3, mesh["firstIndex"]//3 + mesh["indicesCount"]//3))
        for t in range(mesh["firstIndex"]//3, mesh["firstIndex"]//3 + mesh["indicesCount"]//3):
            # print(bpy.data.materials.find(mesh["material"].name))
            m.polygons[t].material_index = m.materials.find(mesh["material"].name)

    m.update()

    return {'FINISHED'}

class BMDLHeader:
    def __init__(self):
        self.dataSize = 0
        self.headerSize = 0
        self.unk = 4  # usually 4, sometimes 16
        self.offsetsCount = 0
        self.offsets = []  # offsets to what?

    def read(self, file):
        expect(readInt(file), 1, "H001", file)
        expect(readInt(file), 0x6C646D62, "H002", file)  # 'BMDL'
        expect(readInt(file), 2, "H003", file)

        self.headerSize = readInt(file)
        self.dataSize = readInt(file)
        self.unk = readInt(file)
        expect(readInt(file), 0, "H004", file)
        self.offsetsCount = readInt(file)
        self.offsets = struct.unpack('<' + str(self.offsetsCount) + "I", file.read(self.offsetsCount * 4))

        expect(self.headerSize + self.dataSize, os.path.getsize(file.name), "H005", file)


class BMDLSectionBounds():
    def __init__(self, file):
        self.dataOffset = readInt(file)
        file.read(12)  # 0
        self.bounds = []
        # 8 floats ?
        for _ in range(0, 8):
            self.bounds.append(readFloat(file))

    def __str__(self):
        return "BMDLSectionBounds [dataOffset=%d, bounds=%s]" % (self.dataOffset, str(self.bounds))


# An offset, the file hash and an int ?
class BMDLSectionHash:
    def __init__(self, file):
        # Offset to the name
        self.dataOffset = readInt(file)
        self.hash = readUInt(file)
        self.count = readInt(file)

    def __str__(self):
        return "BMDLSectionHash [dataOffset=%d, hash=%x, count=%d]" % (self.dataOffset, self.hash, self.count)


# Just an offset and an int
class BMDLSectionInt:
    def __init__(self, file):
        # Offset to a section with a hash
        # Offset to 8 floats, the same as in BMDLSectionBounds
        self.dataOffset = readInt(file)
        self.unk = readInt(file)

    def __str__(self):
        return "BMDLSectionInt [dataOffset=%d, unk=%d]" % (self.dataOffset, self.unk)


# Offset, padding and the file name
class BMDLSectionName:
    def __init__(self, file):
        self.dataOffset = readInt(file)
        file.read(12)  # 0
        self.name = readString(file)

    def __str__(self):
        return "BMDLSectionName [dataOffset=%d, name=%s]" % (self.dataOffset, self.name)


# Offset, hash, number, number
class BMDLSectionObject:
    def __init__(self, file, baseOffset=0):
        # Offset to string
        self.nameOffset = readInt(file)
        self.hash = readUInt(file)
        self.unk = readInt(file)
        self.count = readInt(file)
        file.seek(baseOffset + self.nameOffset)
        self.name = readString(file)

    def __str__(self):
        return "BMDLSectionObject [nameOffset=%d, hash=%x, unk=%d, count=%d, name=%s]" % \
               (self.nameOffset, self.hash, self.unk, self.count, self.name)


class BMDLSectionBounds2:
    def __init__(self, file):
        self.dataOffset = readInt(file)
        self.bounds = []
        # 8 floats ?
        for _ in range(0, 8):
            self.bounds.append(readFloat(file))

    def __str__(self):
        return "BMDLSectionBounds2 [dataOffset=%d, bounds=%s]" % (self.dataOffset, str(self.bounds))


class BMDLSectionOffset:
    def __init__(self, file):
        # offset to vertex format ?
        # offset to vertex buffer ?
        self.dataOffset = readInt(file)

    def __str__(self):
        return "BMDLSectionOffset [dataOffset=%d]" % self.dataOffset


class BMDLSectionMesh:
    def __init__(self, file):
        # offset to triangle buffer
        self.dataOffset = readInt(file)
        self.vertexCount = readInt(file)
        self.indicesCount = readInt(file)
        self.triangleCount = self.indicesCount // 3
        self.bounds = []
        # 8 floats ?
        for _ in range(0, 8):
            self.bounds.append(readFloat(file))

        self.unk1 = readInt(file)
        self.unk2 = readInt(file)

    def __str__(self):
        return "BMDLSectionMesh [dataOffset=%d, vertexCount=%s, indicesCount=%d, triangleCount=%d, bounds=%s, " \
               "unk1=%d, unk2=%d]" % (self.dataOffset, self.vertexCount, self.indicesCount, self.triangleCount,
                                      str(self.bounds), self.unk1, self.unk2)


class BMDLSectionSimpleName:
    def __init__(self, file):
        # offset to bounds again
        self.dataOffset = readInt(file)
        # shader name ?
        self.name = readString(file)

    def __str__(self):
        return "BMDLSectionSimpleName [dataOffset=%d, name=%s]" % (self.dataOffset, self.name)


class BMDLShaderParamFloat:
    def __init__(self, file, baseOffset):
        self.nameOffset = readInt(file)
        self.hash = readInt(file)
        self.dataIndex = readInt(file)
        self.dataLength = readInt(file)  # in dwords
        self.values = []

        file.seek(baseOffset + self.nameOffset)
        self.name = readString(file)

    def readValues(self, file, address):
        self.values = []
        file.seek(address)
        while True:
            value = readFloat(file, endian='<')  # Add endian parameter
            if value is None:
                break
            self.values.append(value)

    def __str__(self):
        return "BMDLSectionMesh [dataOffset=%d, vertexCount=%s, indicesCount=%d, triangleCount=%d, bounds=%s, " \
            "unk1=%d, unk2=%d]" % (self.dataOffset, self.vertexCount, self.indicesCount, self.triangleCount,
                                    str(self.bounds), self.unk1, self.unk2)

    @staticmethod
    def getParameter(parameters, name):
        for param in parameters:
            if param.name == name:
                return param
        return None


class BMDLShaderParamString:
    def __init__(self, file, baseOffset, value=None):
        self.nameOffset = readInt(file)
        self.hash = readInt(file)
        file.seek(baseOffset + self.nameOffset)
        self.name = readString(file)
        self.value = value

    def __str__(self):
        return "BMDLShaderParamString [nameOffset=%d, hash=%x, name=%s, value=\n\t%s]" % \
               (self.nameOffset, self.hash, self.name, str(self.value))

    @staticmethod
    def getParameter(parameters, name):
        print(name)
        for param in parameters:
            print(param.name)
            if param.name == name:
                return param
        return None

class BMDLVertex:
    def __init__(self):
        # size: 28 bytes
        self.pos = None
        # what about this?
        self.normal = None
        self.tangent = None
        self.uv = None
        self.color = None

    def read(self, file, vertexFormat):
        for fmt in vertexFormat.fmt:
            fmt(self, file)

    def readPosition(self, file):
        self.pos = [readFloat(file), readFloat(file), readFloat(file)]

    def readNormal(self, file):
        self.normal = readInt(file)

    def readTangent(self, file):
        self.tangent = readInt(file)

    def readUV(self, file):
        self.uv = [readFloat(file), 0 - readFloat(file)]

    def readColor(self, file):
        self.color = readInt(file)

    @staticmethod
    def decodeColor(color):
        if isinstance(color, int):
            # Handle 4-component color
            return [
                ((color & 0xFF0000) >> 16) / 255,
                ((color & 0xFF00) >> 8) / 255,
                (color & 0xFF) / 255,
                1.0  # Set alpha to 1.0
            ]
        elif isinstance(color, tuple) and len(color) == 3:
            # Handle 3-component color
            return [
                color[0] / 255,
                color[1] / 255,
                color[2] / 255,
                1.0  # Set alpha to 1.0
            ]
        else:
            raise ValueError("Invalid color format: {}".format(color))


class BMDLVertexFormat:
    methods = {
        0: BMDLVertex.readPosition,
        1: BMDLVertex.readNormal,
        2: BMDLVertex.readTangent,
        4: BMDLVertex.readUV,
        5: BMDLVertex.readColor
    }

    def __init__(self, file):
        self.fmt = []

        num = readShort(file)
        while num != 0x00FF:
            readShort(file)  # offset
            readShort(file)  # unk
            self.fmt.append(self.methods[readShort(file)])
            num = readShort(file)

    def __str__(self):
        return "BMDLVertexFormat %s" % str(self.fmt)


class ImportBMDL(bpy.types.Operator, ImportHelper):
    bl_idname = "import_my_format.bmdl"
    bl_label = "Import BMDL"
    bl_options = {'REGISTER', 'UNDO'}

    filename_ext = ".bmdl"
    filter_glob: bpy.props.StringProperty(default="*.bmdl", options={'HIDDEN'})

    def execute(self, context):
        file = open(self.filepath, 'br')
        result = {'CANCELLED'}
        try:
            result = importBMDL(file)
        finally:
            file.close()

        return result


def bmdlImporter_menu_func(self, context):
    self.layout.operator(ImportBMDL.bl_idname, text="Darkspore BMDL Model (.bmdl)")


def register():
    bpy.utils.register_class(ImportBMDL)
    bpy.types.TOPBAR_MT_file_import.append(bmdlImporter_menu_func)


def unregister():
    bpy.utils.unregister_class(ImportBMDL)
    bpy.types.TOPBAR_MT_file_import.remove(bmdlImporter_menu_func)

if __name__ == "__main__":
    register()