"""
    Python Library for reading and writing Roblox mesh files.
    Written by something.else on 21/9/2023

    Supported meshes up to version 5.
    Mesh Format documentation by MaximumADHD: https://devforum.roblox.com/t/roblox-mesh-format/326114
"""

import struct
from dataclasses import dataclass
import sys

def debug_print( message : str ) -> None:
    if __name__ == "__main__":
        print(message)

"""
struct FileMeshVertexNormalTexture3d
{
    float vx,vy,vz;
    float nx,ny,nz;
    float tu,tv;

    signed char tx, ty, tz, ts;
    unsigned char  r, g, b, a;
};
"""

@dataclass
class FileMeshVertexNormalTexture3d:
    vx: float
    vy: float
    vz: float
    nx: float
    ny: float
    nz: float
    tu: float
    tv: float
    tx: int
    ty: int
    tz: int
    ts: int
    r: int
    g: int
    b: int
    a: int

    def read_data( self, data : str ):
        if len(data) < 40:
            raise Exception(f"FileMeshVertexNormalTexture3d.read_data: data is too short ({len(data)} bytes)")

        self.vx = struct.unpack("<f", data[0:4])[0]
        self.vy = struct.unpack("<f", data[4:8])[0]
        self.vz = struct.unpack("<f", data[8:12])[0]
        self.nx = struct.unpack("<f", data[12:16])[0]
        self.ny = struct.unpack("<f", data[16:20])[0]
        self.nz = struct.unpack("<f", data[20:24])[0]
        self.tu = struct.unpack("<f", data[24:28])[0]
        self.tv = struct.unpack("<f", data[28:32])[0]
        self.tx = int.from_bytes(data[32:33], "little")
        self.ty = int.from_bytes(data[33:34], "little")
        self.tz = int.from_bytes(data[34:35], "little")
        self.ts = int.from_bytes(data[35:36], "little")
        self.r = int.from_bytes(data[36:37], "little")
        self.g = int.from_bytes(data[37:38], "little")
        self.b = int.from_bytes(data[38:39], "little")
        self.a = int.from_bytes(data[39:40], "little")
    
    def export_data( self ) -> bytearray:
        return bytearray(
            struct.pack("<f", self.vx) +
            struct.pack("<f", self.vy) +
            struct.pack("<f", self.vz) +
            struct.pack("<f", self.nx) +
            struct.pack("<f", self.ny) +
            struct.pack("<f", self.nz) +
            struct.pack("<f", self.tu) +
            struct.pack("<f", self.tv) +
            self.tx.to_bytes(1, "little") +
            self.ty.to_bytes(1, "little") +
            self.tz.to_bytes(1, "little") +
            self.ts.to_bytes(1, "little") +
            self.r.to_bytes(1, "little") +
            self.g.to_bytes(1, "little") +
            self.b.to_bytes(1, "little") +
            self.a.to_bytes(1, "little")
        )

"""
struct FileMeshVertexNormalTexture3dNoRGBA
{
    float vx, vy, vz;
    float nx, ny, nz;
    float tu, tv;

    signed char tx, ty, tz, ts;
};
"""

@dataclass
class FileMeshVertexNormalTexture3dNoRGBA:
    vx: float
    vy: float
    vz: float
    nx: float
    ny: float
    nz: float
    tu: float
    tv: float
    tx: int
    ty: int
    tz: int
    ts: int

    def read_data( self, data : str ):
        if len(data) < 36:
            raise Exception(f"FileMeshVertexNormalTexture3dNoRGBA.read_data: data is too short ({len(data)} bytes)")

        self.vx = struct.unpack("<f", data[0:4])[0]
        self.vy = struct.unpack("<f", data[4:8])[0]
        self.vz = struct.unpack("<f", data[8:12])[0]
        self.nx = struct.unpack("<f", data[12:16])[0]
        self.ny = struct.unpack("<f", data[16:20])[0]
        self.nz = struct.unpack("<f", data[20:24])[0]
        self.tu = struct.unpack("<f", data[24:28])[0]
        self.tv = struct.unpack("<f", data[28:32])[0]
        self.tx = int.from_bytes(data[32:33], "little")
        self.ty = int.from_bytes(data[33:34], "little")
        self.tz = int.from_bytes(data[34:35], "little")
        self.ts = int.from_bytes(data[35:36], "little")
    
    def export_data( self ) -> bytearray:
        return bytearray(
            struct.pack("<f", self.vx) +
            struct.pack("<f", self.vy) +
            struct.pack("<f", self.vz) +
            struct.pack("<f", self.nx) +
            struct.pack("<f", self.ny) +
            struct.pack("<f", self.nz) +
            struct.pack("<f", self.tu) +
            struct.pack("<f", self.tv) +
            self.tx.to_bytes(1, "little") +
            self.ty.to_bytes(1, "little") +
            self.tz.to_bytes(1, "little") +
            self.ts.to_bytes(1, "little")
        )

"""
struct FileMeshFace
{
    unsigned int a;
    unsigned int b;
    unsigned int c;
};
"""

@dataclass
class FileMeshFace:
    a: int
    b: int
    c: int

    def read_data( self, data : str ):
        if len(data) < 12:
            raise Exception(f"FileMeshFace.read_data: data is too short ({len(data)} bytes)")

        self.a = int.from_bytes(data[0:4], "little")
        self.b = int.from_bytes(data[4:8], "little")
        self.c = int.from_bytes(data[8:12], "little")
    
    def export_data( self ) -> bytearray:
        return bytearray(
            self.a.to_bytes(4, "little") +
            self.b.to_bytes(4, "little") +
            self.c.to_bytes(4, "little")
        )

"""
struct FileMeshHeader
{
    unsigned short cbSize;
    unsigned char cbVerticesStride;
    unsigned char cbFaceStride;

    unsigned int num_vertices;
    unsigned int num_faces;
};
"""

@dataclass
class FileMeshHeader:
    cbSize: int
    cbVerticesStride: int
    cbFaceStride: int
    num_vertices: int
    num_faces: int

    def read_data( self, data : str ):
        if len(data) < 12:
            raise Exception(f"FileMeshHeader.read_data: data is too short ({len(data)} bytes)")

        self.cbSize = int.from_bytes(data[0:2], "little")
        if self.cbSize != 12:
            raise Exception(f"FileMeshHeader.read_data: invalid cbSize ({self.cbSize})")
        self.cbVerticesStride = int.from_bytes(data[2:3], "little")
        self.cbFaceStride = int.from_bytes(data[3:4], "little")
        self.num_vertices = int.from_bytes(data[4:8], "little")
        self.num_faces = int.from_bytes(data[8:12], "little")

    def export_data(self) -> bytearray:
        return bytearray(
            self.cbSize.to_bytes(2, "little") +
            self.cbVerticesStride.to_bytes(1, "little") +
            self.cbFaceStride.to_bytes(1, "little") +
            self.num_vertices.to_bytes(4, "little") +
            self.num_faces.to_bytes(4, "little")
        )

    def __str__(self) -> str:
        return f"FileMeshHeader(cbSize={self.cbSize}, cbVerticesStride={self.cbVerticesStride}, cbFaceStride={self.cbFaceStride}, num_vertices={self.num_vertices}, num_faces={self.num_faces})"
    
    def __repr__(self) -> str:
        return str(self)
    
"""
struct FileMeshHeaderV3
{
    unsigned short cbSize;
    unsigned char cbVerticesStride;
    unsigned char cbFaceStride;
    unsigned short sizeof_LOD;

    unsigned short numLODs;
    unsigned int num_vertices;
    unsigned int num_faces;
};
"""

@dataclass
class FileMeshHeaderV3:
    cbSize: int
    cbVerticesStride: int
    cbFaceStride: int
    sizeof_LOD: int
    numLODs: int
    num_vertices: int
    num_faces: int

    def read_data( self, data : str ):
        if len(data) < 16:
            raise Exception(f"FileMeshHeaderV3.read_data: data is too short ({len(data)} bytes)")

        self.cbSize = int.from_bytes(data[0:2], "little")
        if self.cbSize != 16:
            raise Exception(f"FileMeshHeaderV3.read_data: invalid cbSize ({self.cbSize})")
        self.cbVerticesStride = int.from_bytes(data[2:3], "little")
        self.cbFaceStride = int.from_bytes(data[3:4], "little")
        self.sizeof_LOD = int.from_bytes(data[4:6], "little")
        self.numLODs = int.from_bytes(data[6:8], "little")
        self.num_vertices = int.from_bytes(data[8:12], "little")
        self.num_faces = int.from_bytes(data[12:16], "little")
    
    def export_data(self) -> bytearray:
        return bytearray(
            self.cbSize.to_bytes(2, "little") +
            self.cbVerticesStride.to_bytes(1, "little") +
            self.cbFaceStride.to_bytes(1, "little") +
            self.sizeof_LOD.to_bytes(2, "little") +
            self.numLODs.to_bytes(2, "little") +
            self.num_vertices.to_bytes(4, "little") +
            self.num_faces.to_bytes(4, "little")
        )

"""
struct FileMeshHeaderV4
{
    unsigned short sizeof_MeshHeader;
    unsigned short lodType;

    unsigned int numVerts;
    unsigned int numFaces;

    unsigned short numLODs;
    unsigned short numBones;

    unsigned int sizeof_boneNamesBuffer;
    unsigned short numSubsets;

    unsigned char numHighQualityLODs;
    unsigned char unused;
};
"""

@dataclass
class FileMeshHeaderV4:
    sizeof_MeshHeader: int
    lodType: int
    numVerts: int
    numFaces: int
    numLODs: int
    numBones: int
    sizeof_boneNamesBuffer: int
    numSubsets: int
    numHighQualityLODs: int
    unused: int

    def read_data( self, data : str ):
        if len(data) < 24:
            raise Exception(f"FileMeshHeaderV4.read_data: data is too short ({len(data)} bytes)")

        self.sizeof_MeshHeader = int.from_bytes(data[0:2], "little")
        if self.sizeof_MeshHeader != 24:
            raise Exception(f"FileMeshHeaderV4.read_data: invalid sizeof_MeshHeader ({self.sizeof_MeshHeader})")
        self.lodType = int.from_bytes(data[2:4], "little")
        self.numVerts = int.from_bytes(data[4:8], "little")
        self.numFaces = int.from_bytes(data[8:12], "little")
        self.numLODs = int.from_bytes(data[12:14], "little")
        self.numBones = int.from_bytes(data[14:16], "little")
        self.sizeof_boneNamesBuffer = int.from_bytes(data[16:20], "little")
        self.numSubsets = int.from_bytes(data[20:22], "little")
        self.numHighQualityLODs = int.from_bytes(data[22:23], "little")
        self.unused = int.from_bytes(data[23:24], "little")

    def export_data(self) -> bytearray:
        return bytearray(
            self.sizeof_MeshHeader.to_bytes(2, "little") +
            self.lodType.to_bytes(2, "little") +
            self.numVerts.to_bytes(4, "little") +
            self.numFaces.to_bytes(4, "little") +
            self.numLODs.to_bytes(2, "little") +
            self.numBones.to_bytes(2, "little") +
            self.sizeof_boneNamesBuffer.to_bytes(4, "little") +
            self.numSubsets.to_bytes(2, "little") +
            self.numHighQualityLODs.to_bytes(1, "little") +
            self.unused.to_bytes(1, "little")
        )

"""
struct Envelope
{
    unsigned char bones[4];
    unsigned char weights[4];
};
"""

@dataclass
class Envelope:
    bones: list[int]
    weights: list[int]

    def read_data( self, data : str ):
        if len(data) < 8:
            raise Exception(f"Envelope.read_data: data is too short ({len(data)} bytes)")

        for i in range(0, 4):
            self.bones.append(int.from_bytes(data[i:i+1], "little"))
            self.weights.append(int.from_bytes(data[i+4:i+5], "little"))

    def export_data(self) -> bytearray:
        return bytearray(
            self.bones[0].to_bytes(1, "little") +
            self.bones[1].to_bytes(1, "little") +
            self.bones[2].to_bytes(1, "little") +
            self.bones[3].to_bytes(1, "little") +
            self.weights[0].to_bytes(1, "little") +
            self.weights[1].to_bytes(1, "little") +
            self.weights[2].to_bytes(1, "little") +
            self.weights[3].to_bytes(1, "little")
        )

"""
struct Bone 
{
    unsigned int boneNameIndex;
    unsigned short parentIndex;
    unsigned short lodParentIndex;
    float culling;

    float r00, r01, r02;
    float r10, r11, r12;
    float r20, r21, r22;

    float x, y, z;
};
"""

@dataclass
class Bone:
    boneNameIndex: int
    parentIndex: int
    lodParentIndex: int
    culling: float
    r00: float
    r01: float
    r02: float
    r10: float
    r11: float
    r12: float
    r20: float
    r21: float
    r22: float
    x: float
    y: float
    z: float

    def read_data( self, data : str ):
        if len(data) < 60:
            raise Exception(f"Bone.read_data: data is too short ({len(data)} bytes)")

        self.boneNameIndex = int.from_bytes(data[0:4], "little")
        self.parentIndex = int.from_bytes(data[4:6], "little")
        self.lodParentIndex = int.from_bytes(data[6:8], "little")
        self.culling = struct.unpack("<f", data[8:12])[0]
        self.r00 = struct.unpack("<f", data[12:16])[0]
        self.r01 = struct.unpack("<f", data[16:20])[0]
        self.r02 = struct.unpack("<f", data[20:24])[0]
        self.r10 = struct.unpack("<f", data[24:28])[0]
        self.r11 = struct.unpack("<f", data[28:32])[0]
        self.r12 = struct.unpack("<f", data[32:36])[0]
        self.r20 = struct.unpack("<f", data[36:40])[0]
        self.r21 = struct.unpack("<f", data[40:44])[0]
        self.r22 = struct.unpack("<f", data[44:48])[0]
        self.x = struct.unpack("<f", data[48:52])[0]
        self.y = struct.unpack("<f", data[52:56])[0]
        self.z = struct.unpack("<f", data[56:60])[0]
    
    def export_data(self) -> bytearray:
        return bytearray(
            self.boneNameIndex.to_bytes(4, "little") +
            self.parentIndex.to_bytes(2, "little") +
            self.lodParentIndex.to_bytes(2, "little") +
            struct.pack("<f", self.culling) +
            struct.pack("<f", self.r00) +
            struct.pack("<f", self.r01) +
            struct.pack("<f", self.r02) +
            struct.pack("<f", self.r10) +
            struct.pack("<f", self.r11) +
            struct.pack("<f", self.r12) +
            struct.pack("<f", self.r20) +
            struct.pack("<f", self.r21) +
            struct.pack("<f", self.r22) +
            struct.pack("<f", self.x) +
            struct.pack("<f", self.y) +
            struct.pack("<f", self.z)
        )

"""
struct MeshSubset
{
    unsigned int facesBegin;
    unsigned int facesLength;

    unsigned int vertsBegin;
    unsigned int vertsLength;

    unsigned int numBoneIndicies;
    unsigned short boneIndicies[26];
};
"""

@dataclass
class MeshSubset:
    facesBegin: int
    facesLength: int
    vertsBegin: int
    vertsLength: int
    numBoneIndicies: int
    boneIndicies: list[int]

    def read_data( self, data : str ):
        if len(data) < 72:
            raise Exception(f"MeshSubset.read_data: data is too short ({len(data)} bytes)")

        self.facesBegin = int.from_bytes(data[0:4], "little")
        self.facesLength = int.from_bytes(data[4:8], "little")
        self.vertsBegin = int.from_bytes(data[8:12], "little")
        self.vertsLength = int.from_bytes(data[12:16], "little")
        self.numBoneIndicies = int.from_bytes(data[16:20], "little")
        for i in range(0, 26):
            self.boneIndicies.append(int.from_bytes(data[20+i:21+i], "little"))
        
    def export_data(self) -> bytearray:
        subsetData : bytearray = bytearray(
            self.facesBegin.to_bytes(4, "little") +
            self.facesLength.to_bytes(4, "little") +
            self.vertsBegin.to_bytes(4, "little") +
            self.vertsLength.to_bytes(4, "little") +
            self.numBoneIndicies.to_bytes(4, "little")
        )
        for i in range(0, 26):
            subsetData += self.boneIndicies[i].to_bytes(1, "little")

        return subsetData

"""
struct FileMeshHeaderV5
{
    unsigned short sizeof_MeshHeader;
    unsigned short lodType;

    unsigned int numVerts;
    unsigned int numFaces;

    unsigned short numLODs;
    unsigned short numBones;

    unsigned int sizeof_boneNamesBuffer;
    unsigned short numSubsets;

    unsigned char numHighQualityLODs;
    unsigned char unusedPadding;

    unsigned int facsDataFormat;
    unsigned int facsDataSize;
};
"""

@dataclass
class FileMeshHeaderV5:
    sizeof_MeshHeader: int
    lodType: int
    numVerts: int
    numFaces: int
    numLODs: int
    numBones: int
    sizeof_boneNamesBuffer: int
    numSubsets: int
    numHighQualityLODs: int
    unusedPadding: int
    facsDataFormat: int
    facsDataSize: int

    def read_data( self, data : str ):
        if len(data) < 32:
            raise Exception(f"FileMeshHeaderV5.read_data: data is too short ({len(data)} bytes)")

        self.sizeof_MeshHeader = int.from_bytes(data[0:2], "little")
        if self.sizeof_MeshHeader != 32:
            raise Exception(f"FileMeshHeaderV5.read_data: invalid sizeof_MeshHeader ({self.sizeof_MeshHeader})")
        self.lodType = int.from_bytes(data[2:4], "little")
        self.numVerts = int.from_bytes(data[4:8], "little")
        self.numFaces = int.from_bytes(data[8:12], "little")
        self.numLODs = int.from_bytes(data[12:14], "little")
        self.numBones = int.from_bytes(data[14:16], "little")
        self.sizeof_boneNamesBuffer = int.from_bytes(data[16:20], "little")
        self.numSubsets = int.from_bytes(data[20:22], "little")
        self.numHighQualityLODs = int.from_bytes(data[22:23], "little")
        self.unusedPadding = int.from_bytes(data[23:24], "little")
        self.facsDataFormat = int.from_bytes(data[24:28], "little")
        self.facsDataSize = int.from_bytes(data[28:32], "little")
    
    def export_data(self) -> bytearray:
        return bytearray(
            self.sizeof_MeshHeader.to_bytes(2, "little") +
            self.lodType.to_bytes(2, "little") +
            self.numVerts.to_bytes(4, "little") +
            self.numFaces.to_bytes(4, "little") +
            self.numLODs.to_bytes(2, "little") +
            self.numBones.to_bytes(2, "little") +
            self.sizeof_boneNamesBuffer.to_bytes(4, "little") +
            self.numSubsets.to_bytes(2, "little") +
            self.numHighQualityLODs.to_bytes(1, "little") +
            self.unusedPadding.to_bytes(1, "little") +
            self.facsDataFormat.to_bytes(4, "little") +
            self.facsDataSize.to_bytes(4, "little")
        )

@dataclass
class FileMeshData:
    vnts: list[FileMeshVertexNormalTexture3d]
    faces: list[FileMeshFace]
    header : FileMeshHeader | FileMeshHeaderV3 | FileMeshHeaderV4 | FileMeshHeaderV5 
    LODs : list[int]
    bones : list[Bone]
    boneNames : str
    meshSubsets : list[MeshSubset]
    full_faces : list[FileMeshFace]
    envelopes : list[Envelope]

def read_data( data : str, offset : int, size : int ) -> str:
    """Reads a string of data from a given offset and size."""
    
    if len(data) < offset + size:
        raise Exception(f"read_data: offset is out of bounds (offset={offset}, size={size})")
    return data[offset:offset+size]

def get_mesh_version( data : bytearray ) -> float:
    """Gets the version of the mesh file. Throws an exception if the version is not supported."""
    
    if len(data) < 12:
        raise Exception(f"get_mesh_version: data is too short ({len(data)} bytes)")
    if not data[0:8] == b"version ":
        raise Exception(f"get_mesh_version: invalid mesh header ({data[0:8]})")

    if data[0:12] == b"version 1.00":
        return 1.0
    elif data[0:12] == b"version 1.01":
        return 1.1
    elif data[0:12] == b"version 2.00":
        return 2.0
    elif data[0:12] == b"version 3.00":
        return 3.0
    elif data[0:12] == b"version 3.01":
        return 3.1
    elif data[0:12] == b"version 4.00":
        return 4.0
    elif data[0:12] == b"version 4.01":
        return 4.1
    elif data[0:12] == b"version 5.00":
        return 5.0
    elif data[0:12] == b"version 5.01":
        return 5.1
    else:
        raise Exception(f"get_mesh_version: unsupported mesh version ({data[0:12]})")

def read_mesh_v1( data : bytearray, offset : int, scale : int = 0.5, invertUV : bool = True) -> FileMeshData:
    data = data.decode("ASCII")

    meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], [])
    numFaces : int = int(data.split("\n")[1])
    debug_print(f"read_mesh_v1: numFaces={numFaces}")

    # [0.551563,-0.0944613,0.0862401] we need to find every vector3 in the file
    # Each vert has 3 vector3 values so we need to find 3 vector3 values for each vert
    startingIndex = data.find("[")
    allVectors : list[str] = data[startingIndex:].split("]")
    for i in range(0, len(allVectors)):
        vector : str = allVectors[i].strip()
        vector = vector.replace("[", "").replace("]", "")
        if vector == "":
            del allVectors[i]
            continue

        vector_floats : list[float] = [float(x) for x in vector.split(",")]

        if len(vector_floats) != 3:
            raise Exception(f"read_mesh_v1: invalid vector3 ({vector})")
        #debug_print(f"read_mesh_v1: allVectors[{i}]={vector_floats}")
        allVectors[i] = vector_floats
    if len(allVectors) != numFaces * 9:
        raise Exception(f"read_mesh_v1: invalid number of verticies ({len(allVectors)}), expected {numFaces * 9}")

    for i in range(0, len(allVectors), 3):
        vertPos : list[float] = allVectors[ i ]
        vertNorm : list[float] = allVectors[ i + 1 ]
        vertUV : list[float] = allVectors[ i + 2 ]
        
        meshData.vnts.append(FileMeshVertexNormalTexture3d(
            vertPos[0] * scale, vertPos[1] * scale, vertPos[2] * scale,
            vertNorm[0], vertNorm[1], vertNorm[2],
            # Version 1.0 has the UVs inverted, it was only fixed in 1.1
            vertUV[0], float(( 1 - vertUV[1] ) if invertUV else vertUV[1]), int(vertUV[2]),

            0, 0, 0, 0, 0, 0, 0 
        ))

        debug_print(f"read_mesh_v1: vnts[{i // 3}]={meshData.vnts[i // 3]}")

    for i in range(0, numFaces):
        meshData.faces.append(FileMeshFace(i * 3, i * 3 + 1, i * 3 + 2))
        debug_print(f"read_mesh_v1: faces[{i}]={meshData.faces[i]}")

    debug_print(f"read_mesh_v1: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully")
    return meshData

def read_mesh_v2( data : bytearray, offset : int ) -> FileMeshData:
    meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], [])
    meshHeader : FileMeshHeader = FileMeshHeader(0, 0, 0, 0, 0)
    meshHeader.read_data(read_data(data, offset, 12))
    offset += 12

    debug_print(f"read_mesh_v2: meshHeader={meshHeader}")
    if meshHeader.num_vertices == 0 or meshHeader.num_faces == 0:
        raise Exception(f"read_mesh_v2: empty mesh")
    meshData.header = meshHeader
    isRGBAMissing = meshHeader.cbVerticesStride == 36
    if isRGBAMissing:
        for i in range(0, meshHeader.num_vertices):
            meshData.vnts.append(FileMeshVertexNormalTexture3dNoRGBA(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
            meshData.vnts[i].read_data(read_data(data, offset + i * 36, 36))

            debug_print(f"read_mesh_v2: vnts[{i}]={meshData.vnts[i]}")
    else:
        for i in range(0, meshHeader.num_vertices):
            meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
            meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40))

            debug_print(f"read_mesh_v2: vnts[{i}]={meshData.vnts[i]}")
    offset += meshHeader.num_vertices * meshHeader.cbVerticesStride
    
    for i in range(0, meshHeader.num_faces):
        meshData.faces.append(FileMeshFace(0, 0, 0))
        meshData.faces[i].read_data(read_data(data, offset + i * 12, 12))

        debug_print(f"read_mesh_v2: faces[{i}]={meshData.faces[i]}")
    
    offset += meshHeader.num_faces * meshHeader.cbFaceStride

    if offset != len(data):
        raise Exception(f"read_mesh_v2: unexpected data at end of file ({len(data) - offset} bytes)")
    
    debug_print(f"read_mesh_v2: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully")
    return meshData

def read_mesh_v3( data : bytearray, offset : int ) -> FileMeshData:
    meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], [])
    meshHeader : FileMeshHeaderV3 = FileMeshHeaderV3(0, 0, 0, 0, 0, 0, 0)
    meshHeader.read_data(read_data(data, offset, 16))
    offset += 16

    debug_print(f"read_mesh_v3: meshHeader={meshHeader}")
    if meshHeader.num_vertices == 0 or meshHeader.num_faces == 0:
        raise Exception(f"read_mesh_v3: empty mesh")
    meshData.header = meshHeader
    for i in range(0, meshHeader.num_vertices):
        meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
        meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40))

        debug_print(f"read_mesh_v3: vnts[{i}]={meshData.vnts[i]}")
    offset += meshHeader.num_vertices * meshHeader.cbVerticesStride

    for i in range(0, meshHeader.num_faces):
        meshData.faces.append(FileMeshFace(0, 0, 0))
        meshData.faces[i].read_data(read_data(data, offset + i * 12, 12))

        debug_print(f"read_mesh_v3: faces[{i}]={meshData.faces[i]}")
    offset += meshHeader.num_faces * meshHeader.cbFaceStride

    # LODs ( sizeof_LOD [ unsigned int ] * numLODs )
    meshLODs : list[int] = []
    for i in range(0, meshHeader.numLODs):
        meshLODs.append(int.from_bytes(read_data(data, offset + i * 4, 4), "little"))
        debug_print(f"read_mesh_v3: meshLODs[{i}]={meshLODs[i]}")
    offset += meshHeader.numLODs * 4

    # We only keep the first LOD in the mesh data
    if len(meshLODs) > 1:
        meshData.full_faces = meshData.faces
        meshData.faces = meshData.faces[0:meshLODs[1]]
        debug_print(f"read_mesh_v3: only keeping {meshLODs[1]}/{meshHeader.num_faces} faces")
    meshData.LODs = meshLODs

    if offset != len(data):
        raise Exception(f"read_mesh_v3: unexpected data at end of file ({len(data) - offset} bytes)")
    
    debug_print(f"read_mesh_v3: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully")
    return meshData

def read_mesh_v4( data : bytearray, offset : int ) -> FileMeshData:
    meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], [])
    meshHeader : FileMeshHeaderV4 = FileMeshHeaderV4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    meshHeader.read_data(read_data(data, offset, 24))
    offset += 24

    debug_print(f"read_mesh_v4: meshHeader={meshHeader}")
    if meshHeader.numVerts == 0 or meshHeader.numFaces == 0:
        raise Exception(f"read_mesh_v4: empty mesh")
    meshData.header = meshHeader
    for i in range(0, meshHeader.numVerts):
        meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 127, 127, 127, 127, 127))
        meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40))

        debug_print(f"read_mesh_v4: vnts[{i}]={meshData.vnts[i]}")
    offset += meshHeader.numVerts * 40

    if meshHeader.numBones > 0:
        for i in range(0, meshHeader.numVerts):
            meshData.envelopes.append(Envelope([], []))
            meshData.envelopes[i].read_data(read_data(data, offset + i * 8, 8))

            debug_print(f"read_mesh_v4: envelopes[{i}]={meshData.envelopes[i]}")
        offset += meshHeader.numVerts * 8

    for i in range(0, meshHeader.numFaces):
        meshData.faces.append(FileMeshFace(0, 0, 0))
        meshData.faces[i].read_data(read_data(data, offset + i * 12, 12))

        debug_print(f"read_mesh_v4: faces[{i}]={meshData.faces[i]}")
    offset += meshHeader.numFaces * 12

    # LODs ( sizeof_LOD [ unsigned int ] * numLODs )
    meshLODs : list[int] = []
    for i in range(0, meshHeader.numLODs):
        meshLODs.append(int.from_bytes(read_data(data, offset + i * 4, 4), "little"))
        debug_print(f"read_mesh_v4: meshLODs[{i}]={meshLODs[i]}")
    offset += meshHeader.numLODs * 4

    if len(meshLODs) > 1:
        meshData.full_faces = meshData.faces
        meshData.faces = meshData.faces[0:meshLODs[1]]
        debug_print(f"read_mesh_v4: only keeping {meshLODs[1]}/{meshHeader.numFaces} faces")
    meshData.LODs = meshLODs

    if meshHeader.numBones > 0:
        for i in range(0, meshHeader.numBones):
            meshData.bones.append(Bone(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -127, -127, -127, 0, 0))
            meshData.bones[i].read_data(read_data(data, offset + i * 60, 60))

            debug_print(f"read_mesh_v4: bones[{i}]={meshData.bones[i]}")
        offset += meshHeader.numBones * 60

    boneNames : str = read_data(data, offset, meshHeader.sizeof_boneNamesBuffer).decode("utf-8")
    offset += meshHeader.sizeof_boneNamesBuffer
    debug_print(f"read_mesh_v4: boneNames={boneNames}")

    meshSubsets : list[MeshSubset] = []
    for i in range(0, meshHeader.numSubsets):
        meshSubsets.append(MeshSubset(0, 0, 0, 0, 0, []))
        meshSubsets[i].read_data(read_data(data, offset + i * 72, 72))

        debug_print(f"read_mesh_v4: meshSubsets[{i}]={meshSubsets[i]}")
    meshData.meshSubsets = meshSubsets
    offset += meshHeader.numSubsets * 72
    offset += meshHeader.unused

    if offset != len(data):
        raise Exception(f"read_mesh_v4: unexpected data at end of file ({len(data) - offset} bytes)")
    
    debug_print(f"read_mesh_v4: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully")
    return meshData

def read_mesh_v5( data : bytearray, offset : int ) -> FileMeshData:
    meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], [])
    meshHeader : FileMeshHeaderV5 = FileMeshHeaderV5(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    meshHeader.read_data(read_data(data, offset, 32))
    offset += 32

    debug_print(f"read_mesh_v5: meshHeader={meshHeader}")
    if meshHeader.numVerts == 0 or meshHeader.numFaces == 0:
        raise Exception(f"read_mesh_v5: empty mesh")
    meshData.header = meshHeader
    for i in range(0, meshHeader.numVerts):
        meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 127, 127, 127, 127, 127))
        meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40))

        debug_print(f"read_mesh_v5: vnts[{i}]={meshData.vnts[i]}")
    offset += meshHeader.numVerts * 40

    if meshHeader.numBones > 0:
        for i in range(0, meshHeader.numVerts):
            temp = Envelope([], [])
            temp.read_data(read_data(data, offset + i * 8, 8))
        offset += meshHeader.numVerts * 8

    for i in range(0, meshHeader.numFaces):
        meshData.faces.append(FileMeshFace(0, 0, 0))
        meshData.faces[i].read_data(read_data(data, offset + i * 12, 12))

        debug_print(f"read_mesh_v5: faces[{i}]={meshData.faces[i]}")
    offset += meshHeader.numFaces * 12

    # LODs ( sizeof_LOD [ unsigned int ] * numLODs )
    meshLODs : list[int] = []
    for i in range(0, meshHeader.numLODs):
        meshLODs.append(int.from_bytes(read_data(data, offset + i * 4, 4), "little"))
        debug_print(f"read_mesh_v5: meshLODs[{i}]={meshLODs[i]}")
    offset += meshHeader.numLODs * 4

    if len(meshLODs) > 1:
        meshData.full_faces = meshData.faces
        meshData.faces = meshData.faces[0:meshLODs[1]]
        debug_print(f"read_mesh_v5: only keeping {meshLODs[1]}/{meshHeader.numFaces} faces")
    meshData.LODs = meshLODs

    if meshHeader.numBones > 0:
        for i in range(0, meshHeader.numBones):
            meshData.bones.append(Bone(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -127, -127, -127, 0, 0))
            meshData.bones[i].read_data(read_data(data, offset + i * 60, 60))

            debug_print(f"read_mesh_v5: bones[{i}]={meshData.bones[i]}")
        offset += meshHeader.numBones * 60

    boneNames : str = read_data(data, offset, meshHeader.sizeof_boneNamesBuffer).decode("utf-8")
    offset += meshHeader.sizeof_boneNamesBuffer
    debug_print(f"read_mesh_v5: boneNames={boneNames}")

    meshSubsets : list[MeshSubset] = []
    for i in range(0, meshHeader.numSubsets):
        meshSubsets.append(MeshSubset(0, 0, 0, 0, 0, []))
        meshSubsets[i].read_data(read_data(data, offset + i * 72, 72))

        debug_print(f"read_mesh_v5: meshSubsets[{i}]={meshSubsets[i]}")
    meshData.meshSubsets = meshSubsets
    offset += meshHeader.numSubsets * 72
    offset += meshHeader.unusedPadding

    offset += meshHeader.facsDataSize # No way I am doing allat

    if offset != len(data):
        raise Exception(f"read_mesh_v5: unexpected data at end of file ({len(data) - offset} bytes)")

    debug_print(f"read_mesh_v5: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully")
    return meshData

def export_mesh_v2( meshData : FileMeshData ) -> bytearray:
    if meshData is None:
        raise Exception(f"export_mesh_v2: meshData is None")
    if len(meshData.vnts) == 0 or len(meshData.faces) == 0:
        raise Exception(f"export_mesh_v2: meshData is empty")

    finalmesh : bytearray = b""
    finalmesh += b"version 2.00\n"
    finalmesh += FileMeshHeader(
        12,
        36 if len(meshData.vnts) > 0 and isinstance(meshData.vnts[0], FileMeshVertexNormalTexture3dNoRGBA) else 40,
        12,
        len(meshData.vnts),
        len(meshData.faces)
    ).export_data()

    for i in range(0, len(meshData.vnts)):
        finalmesh += meshData.vnts[i].export_data()

    for i in range(0, len(meshData.faces)):
        finalmesh += meshData.faces[i].export_data()

    return finalmesh

def export_mesh_v3( meshData : FileMeshData ) -> bytearray:
    if meshData is None:
        raise Exception(f"export_mesh_v3: meshData is None")
    if len(meshData.vnts) == 0 or len(meshData.faces) == 0:
        raise Exception(f"export_mesh_v3: meshData is empty")

    finalmesh : bytearray = b""
    finalmesh += b"version 3.00\n"
    finalmesh += FileMeshHeaderV3(
        16,
        40,
        12,
        4,
        len(meshData.LODs),
        len(meshData.vnts),
        len(meshData.full_faces) if len(meshData.LODs) > 1 and len(meshData.full_faces) > len(meshData.faces) else len(meshData.faces),
    ).export_data()

    for i in range(0, len(meshData.vnts)):
        finalmesh += meshData.vnts[i].export_data()

    if len(meshData.LODs) > 1 and len(meshData.full_faces) > len(meshData.faces):
        for i in range(0, len(meshData.full_faces)):
            finalmesh += meshData.full_faces[i].export_data()
    else:
        for i in range(0, len(meshData.faces)):
            finalmesh += meshData.faces[i].export_data()

    for i in range(0, len(meshData.LODs)):
        finalmesh += meshData.LODs[i].to_bytes(4, "little")

    return finalmesh

def read_mesh_data( data : bytearray ) -> FileMeshData:
    meshVersion = get_mesh_version(data)
    startingOffset = data.find(b"\n")
    debug_print(f"meshVersion={meshVersion}, startingOffset={startingOffset}")

    if meshVersion == 1.0:
        meshData : FileMeshData = read_mesh_v1(data, startingOffset + 1, 0.5)
    elif meshVersion == 1.1:
        meshData : FileMeshData = read_mesh_v1(data, startingOffset + 1, 1.0, False)
    elif meshVersion == 2.0:
        meshData : FileMeshData = read_mesh_v2(data, startingOffset + 1)
    elif meshVersion == 3.0 or meshVersion == 3.1:
        meshData : FileMeshData = read_mesh_v3(data, startingOffset + 1)
    elif meshVersion == 4.0 or meshVersion == 4.1:
        meshData : FileMeshData = read_mesh_v4(data, startingOffset + 1)
    elif meshVersion == 5.0 or meshVersion == 5.1:
        meshData : FileMeshData = read_mesh_v5(data, startingOffset + 1)
    else:
        raise Exception(f"read_mesh_data: unsupported mesh version ({meshVersion})")
    return meshData

if __name__ == "__main__":
    arguments = sys.argv[1:]
    if len(arguments) < 1:
        debug_print("Usage: RBXMesh.py <mesh file location>")
        exit(1)
    
    meshFile = open(arguments[0], "rb")
    meshData = meshFile.read()
    meshFile.close()

    meshData = read_mesh_data(meshData)

    if len(arguments) > 1:
        if arguments[1] == "2.0":
            with open(f"{arguments[0]}.v2", "wb") as f:
                f.write(export_mesh_v2(meshData))
        elif arguments[1] == "3.0":
            with open(f"{arguments[0]}.v3", "wb") as f:
                f.write(export_mesh_v3(meshData))