import math

from .AnimationClip import AABB, PackedFloatVector, PackedIntVector
from .NamedObject import NamedObject
from .Texture2D import StreamingInfo
from ..helpers.ResourceReader import get_resource_data
from ..math import Matrix4x4, Vector3
from ..streams import EndianBinaryWriter
from ..enums import GfxPrimitiveType
import struct
from enum import IntEnum
from ..export import MeshExporter


class MinMaxAABB:
    def __init__(self, reader):
        self.m_Min = reader.read_vector3()
        self.m_Max = reader.read_vector3()

    def save(self, writer):
        writer.write_vector3(self.m_Min)
        writer.write_vector3(self.m_Max)


class CompressedMesh:
    def __init__(self, reader):
        version = reader.version
        self.m_Vertices = PackedFloatVector(reader)
        self.m_UV = PackedFloatVector(reader)
        if version[0] < 5:  # 5 down
            self.m_BindPoses = PackedFloatVector(reader)
        self.m_Normals = PackedFloatVector(reader)
        self.m_Tangents = PackedFloatVector(reader)
        self.m_Weights = PackedIntVector(reader)
        self.m_NormalSigns = PackedIntVector(reader)
        self.m_TangentSigns = PackedIntVector(reader)
        if version >= (5,):  # 5 and up
            self.m_FloatColors = PackedFloatVector(reader)
        self.m_BoneIndices = PackedIntVector(reader)
        self.m_Triangles = PackedIntVector(reader)
        if version >= (3, 5):  # 3.5 and up
            if version[0] < 5:  # 5 down
                self.m_Colors = PackedIntVector(reader)
            else:
                self.m_UVInfo = reader.read_u_int()

    def save(self, writer, version):
        self.m_Vertices.save(writer)
        self.m_UV.save(writer)
        if version < (5,):  # 5 down
            self.m_BindPoses.save(writer)
        self.m_Normals.save(writer)
        self.m_Tangents.save(writer)
        self.m_Weights.save(writer)
        self.m_NormalSigns.save(writer)
        self.m_TangentSigns.save(writer)
        if version >= (5,):  # 5 and up
            self.m_FloatColors.save(writer)
        self.m_BoneIndices.save(writer)
        self.m_Triangles.save(writer)
        if version >= (3, 5):  # 3.5 and up
            if version < (5,):  # 5 down
                self.m_Colors.save(writer)
            else:
                writer.write_u_int(self.m_UVInfo)


class StreamInfo:
    def __init__(self, **kwargs):
        if "reader" in kwargs:
            reader = kwargs["reader"]
            version = reader.version
            self.channelMask = reader.read_u_int()
            self.offset = reader.read_u_int()

            if version < (4,):  # 4.0 down
                self.stride = reader.read_u_int()
                self.align = reader.read_u_int()
            else:
                self.stride = reader.read_byte()
                self.dividerOp = reader.read_byte()
                self.frequency = reader.read_u_short()
        else:
            self.__dict__ = kwargs

    def save(self, writer: EndianBinaryWriter, version: tuple):
        writer.write_u_int(self.channelMask)
        writer.write_u_int(self.offset)

        if version < (4,):  # 4.0 down
            writer.write_u_int(self.stride)
            writer.write_u_int(self.align)
        else:
            writer.write_byte(self.stride)
            writer.write_byte(self.dividerOp)
            writer.write_u_short(self.frequency)


class ChannelInfo:
    def __init__(self, reader):
        self.stream = reader.read_byte()
        self.offset = reader.read_byte()
        self.format = reader.read_byte()
        self.dimension = reader.read_byte()

    def save(self, writer):
        writer.write_byte(self.stream)
        writer.write_byte(self.offset)
        writer.write_byte(self.format)
        writer.write_byte(self.dimension)


class VertexData:
    def __init__(self, reader):
        self.reader = reader
        version = reader.version

        if version < (2018,):  # 2018 down
            self.m_CurrentChannels = reader.read_u_int()

        self.m_VertexCount = reader.read_u_int()

        if version >= (4,):  # 4.0 and up
            m_ChannelsSize = reader.read_int()
            self.m_Channels = [ChannelInfo(reader) for _ in range(m_ChannelsSize)]

        if version < (5,):  # 5.0 down
            if version < (4,):  # 4.0 down
                m_StreamsSize = 4
            else:
                m_StreamsSize = reader.read_int()

            self.m_Streams = [StreamInfo(reader=reader) for _ in range(m_StreamsSize)]

            if version < (4,):  # 4.0 down
                self.GetChannels()
        else:  # 5.0 and up
            self.GetStreams()

        self.m_DataSize = reader.read_bytes(reader.read_int())
        reader.align_stream()

    def save(self, writer: EndianBinaryWriter, version):
        if version < (2018,):  # 2018 down
            writer.write_u_int(self.m_CurrentChannels)

        writer.write_u_int(self.m_VertexCount)

        if version >= (4,):  # 4.0 and up
            writer.write_int(len(self.m_Channels))
            for ch in self.m_Channels:
                ch.save(writer)

        if (4,) <= version[:2] < (5,):  # 4.0 and up to 5.0
            writer.write_int(len(self.m_Streams))

            for stream in self.m_Streams:
                stream.save(writer=writer, version=version)

            if version < (4,):  # 4.0 down
                raise Exception("Unsupported version")
        else:  # 5.0 and up
            # for stream in self.m_Streams:
            #    stream.save(writer)
            pass

        writer.write_int(len(self.m_DataSize))
        writer.write_bytes(self.m_DataSize)
        writer.align_stream()

    def GetStreams(self):
        streamCount = 1
        if self.m_Channels:
            streamCount += max(x.stream for x in self.m_Channels)

        self.m_Streams = {}
        offset = 0
        for s in range(streamCount):
            chnMask = 0
            stride = 0
            for chn, m_Channel in enumerate(self.m_Channels):
                if m_Channel.stream == s:
                    if m_Channel.dimension > 0:
                        chnMask |= 1 << chn  # Shift 1UInt << chn
                        stride += m_Channel.dimension * MeshHelper.GetFormatSize(
                            self.reader.version, m_Channel.format
                        )
            self.m_Streams[s] = StreamInfo(
                channelMask=chnMask,
                offset=offset,
                stride=stride,
                dividerOp=0,
                frequency=0,
            )
            offset += self.m_VertexCount * stride
            # static size_t align_streamSize (size_t size) { return (size + (kVertexStreamAlign-1)) & ~(kVertexStreamAlign-1)
            offset = (offset + (16 - 1)) & ~(
                16 - 1
            )  # (offset + (16u - 1u)) & ~(16u - 1u);

    def GetChannels(self):
        self.m_Channels = []  # ChannelInfo[6]
        for i in range(6):
            self.m_Channels.append(ChannelInfo(self.reader))
        for s, m_Stream in enumerate(self.m_Streams):
            channelMask = bytearray(m_Stream.channelMask)  # BitArray
            offset = 0
            for i in range(6):
                if channelMask[i]:
                    m_Channel = self.m_Channels[i]
                    m_Channel.stream = s
                    m_Channel.offset = offset
                    if i in [0, 1]:
                        # 0 - kShaderChannelVertex
                        # 1 - kShaderChannelNormal
                        m_Channel.format = 0  # kChannelFormatFloat
                        m_Channel.dimension = 3
                    elif i == 2:  # kShaderChannelColor
                        m_Channel.format = 2  # kChannelFormatColor
                        m_Channel.dimension = 4
                    elif i in [3, 4]:
                        # 3 - kShaderChannelTexCoord0
                        # 4 - kShaderChannelTexCoord1
                        m_Channel.format = 0  # kChannelFormatFloat
                        m_Channel.dimension = 2
                    elif i == 5:  # kShaderChannelTangent
                        m_Channel.format = 0  # kChannelFormatFloat
                        m_Channel.dimension = 4
                    offset += m_Channel.dimension * MeshHelper.GetFormatSize(
                        self.reader.version, m_Channel.format
                    )


class BoneWeights4:
    def __init__(self, reader=None):
        if reader:
            self.weight = reader.read_float_array(4)
            self.boneIndex = reader.read_int_array(4)
        else:
            self.weight = [0.0] * 4
            self.boneIndex = [0] * 4

    def save(self, writer):
        writer.write_float_array(self.weight)
        writer.write_int_array(self.boneIndex)


class BlendShapeVertex:
    def __init__(self, reader):
        self.vertex = reader.read_vector3()
        self.normal = reader.read_vector3()
        self.tangent = reader.read_vector3()
        self.index = reader.read_u_int()


class MeshBlendShape:
    def __init__(self, reader):
        version = reader.version

        if version < (4, 3):  # 4.3 down
            self.name = reader.read_aligned_string()
        self.firstVertex = reader.read_u_int()
        self.vertexCount = reader.read_u_int()
        if version < (4, 3):  # 4.3 down
            self.aabbMinDelta = reader.read_vector3()
            self.aabbMaxDelta = reader.read_vector3()
        self.hasNormals = reader.read_boolean()
        self.hasTangents = reader.read_boolean()
        if version >= (4, 3):  # 4.3 and up
            reader.align_stream()


class MeshBlendShapeChannel:
    def __init__(self, reader):
        self.name = reader.read_aligned_string()
        self.nameHash = reader.read_u_int()
        self.frameIndex = reader.read_int()
        self.frameCount = reader.read_int()


class BlendShapeData:
    def __init__(self, reader):
        version = reader.version

        if version >= (4, 3):  # 4.3 and up
            numVerts = reader.read_int()
            self.vertices = [BlendShapeVertex(reader) for _ in range(numVerts)]

            numShapes = reader.read_int()
            self.shapes = [MeshBlendShape(reader) for _ in range(numShapes)]

            numChannels = reader.read_int()
            self.channels = [MeshBlendShapeChannel(reader) for _ in range(numChannels)]
            self.fullWeights = reader.read_float_array()
        else:
            m_ShapesSize = reader.read_int()
            self.m_Shapes = [MeshBlendShape(reader) for _ in range(m_ShapesSize)]
            reader.align_stream()
            m_ShapeVerticesSize = reader.read_int()
            self.m_ShapeVertices = [
                BlendShapeVertex(reader) for _ in range(m_ShapeVerticesSize)
            ]


class SubMesh:
    def __init__(self, reader):
        version = reader.version
        self.firstByte = reader.read_u_int()
        self.indexCount = reader.read_u_int()
        self.topology = GfxPrimitiveType(reader.read_int())

        if version < (4,):  # 4.0 down
            self.triangleCount = reader.read_u_int()

        if version >= (2017, 3):  # 2017.3 and up
            self.baseVertex = reader.read_u_int()

        if version >= (3,):  # 3.0 and up
            self.firstVertex = reader.read_u_int()
            self.vertexCount = reader.read_u_int()
            self.localAABB = AABB(reader)

    def save(self, writer, version):
        writer.write_u_int(self.firstByte)
        writer.write_u_int(self.indexCount)
        writer.write_int(self.topology.value)

        if version < (4,):  # 4.0 down
            writer.write_u_int(self.triangleCount)

        if version >= (2017, 3):  # 2017.3 and up
            writer.write_u_int(self.baseVertex)

        if version >= (3,):  # 3.0 and up
            writer.write_u_int(self.firstVertex)
            writer.write_u_int(self.vertexCount)
            self.localAABB.save(writer)


class Mesh(NamedObject):
    def export(self):
        return MeshExporter.export_mesh(self)

    def __init__(self, reader):
        super().__init__(reader=reader)
        version = reader.version

        self.m_Use16BitIndices = True
        self.m_Indices = []
        self.m_BindPose = []
        self.m_BoneNameHashes = []
        self.m_Vertices = []
        self.m_Skin = []
        self.m_Normals = []
        self.m_Colors = []
        self.m_UV0 = []
        self.m_UV1 = []
        self.m_UV2 = []
        self.m_UV3 = []
        self.m_UV4 = []
        self.m_UV5 = []
        self.m_UV6 = []
        self.m_UV7 = []
        self.m_Tangents = []

        if version < (3, 5):  # 3.5 down
            self.m_Use16BitIndices = reader.read_int() > 0

        if version[:2] <= (2, 5):  # 2.5 and down
            m_IndexBuffer_size = reader.read_int()

            if self.m_Use16BitIndices:
                self.m_IndexBuffer = [
                    reader.read_u_short()
                    for _ in range(math.ceil(m_IndexBuffer_size / 2))
                ]
                reader.align_stream()
            else:
                self.m_IndexBuffer = reader.read_u_int_array(
                    math.ceil(m_IndexBuffer_size / 4)
                )

        m_SubMeshesSize = reader.read_int()
        self.m_SubMeshes = [SubMesh(reader) for _ in range(m_SubMeshesSize)]

        if version >= (4, 1):  # 4.1 and up
            self.m_Shapes = BlendShapeData(reader)

        if version >= (4, 3):  # 4.3 and up
            self.m_BindPose = reader.read_matrix_array()
            self.m_BoneNameHashes = reader.read_u_int_array()
            self.m_RootBoneNameHash = reader.read_u_int()

        if version >= (2, 6):  # 2.6.0 and up
            if version >= (2019,):  # 2019 and up
                m_BonesAABBSize = reader.read_int()
                self.m_BonesAABB = [MinMaxAABB(reader) for _ in range(m_BonesAABBSize)]
                self.m_VariableBoneCountWeights = reader.read_u_int_array()

            self.m_MeshCompression = reader.read_byte()
            if version >= (4,):  #
                if version < (5,):  #
                    self.m_StreamCompression = reader.read_byte()
                self.m_IsReadable = reader.read_boolean()
                self.m_KeepVertices = reader.read_boolean()
                self.m_KeepIndices = reader.read_boolean()
            reader.align_stream()

            # Unity fixed it in 2017.3.1p1 and later versions
            if (
                version >= (2017, 4)  # 2017.4
                # fixed after 2017.3.1px
                or version[:3] == (2017, 3, 1)
                and self.build_type.IsPatch
                # 2017.3.xfx with no compression
                or version[:2] == (2017, 3)
                and self.m_MeshCompression == 0
            ):
                self.m_IndexFormat = reader.read_int()
                self.m_Use16BitIndices = self.m_IndexFormat == 0

            m_IndexBuffer_size = reader.read_int()
            if self.m_Use16BitIndices:
                self.m_IndexBuffer = [
                    reader.read_u_short()
                    for _ in range(math.ceil(m_IndexBuffer_size / 2))
                ]
                reader.align_stream()
            else:
                self.m_IndexBuffer = reader.read_u_int_array(
                    math.ceil(m_IndexBuffer_size / 4)
                )

        if version < (3, 5):  # 3.4.2 and earlier
            self.m_VertexCount = reader.read_int()
            self.m_Vertices = reader.read_float_array(self.m_VertexCount * 3)  # Vector3

            self.m_SkinSize = reader.read_int()
            self.m_Skin = [BoneWeights4(reader) for _ in range(self.m_SkinSize)]

            self.m_BindPose = reader.read_matrix_array()
            self.m_UV0 = reader.read_float_array(reader.read_int() * 2)  # Vector2
            self.m_UV1 = reader.read_float_array(reader.read_int() * 2)  # Vector2

            if version[:2] <= (2, 5):  # 2.5 and down
                m_TangentSpace_size = reader.read_int()
                self.m_Normals = [0] * (m_TangentSpace_size * 3)
                self.m_Tangets = [0] * (m_TangentSpace_size * 4)
                for v in range(m_TangentSpace_size):
                    self.m_Normals[v * 3] = reader.read_float()
                    self.m_Normals[v * 3 + 1] = reader.read_float()
                    self.m_Normals[v * 3 + 2] = reader.read_float()
                    self.m_Tangents[v * 3] = reader.read_float()
                    self.m_Tangents[v * 3 + 1] = reader.read_float()
                    self.m_Tangents[v * 3 + 2] = reader.read_float()
                    # handedness
                    self.m_Tangents[v * 3 + 3] = reader.read_float()
            else:  # 2.6.0 and later
                self.m_Tangents = reader.read_float_array(
                    reader.read_int() * 4
                )  # Vector4
                self.m_Normals = reader.read_float_array(
                    reader.read_int() * 3
                )  # Vector3
        else:
            if version[:2] < (2018, 2):  # 2018.2 down
                m_SkinSize = reader.read_int()
                self.m_Skin = [BoneWeights4(reader) for _ in range(m_SkinSize)]

            if version[:2] <= (4, 2):  # 4.2 and down
                self.m_BindPose = reader.read_matrix_array()

            self.m_VertexData = VertexData(reader)

        if version >= (2, 6):  # 2.6.0 and later
            self.m_CompressedMesh = CompressedMesh(reader)

        self.m_LocalAABB = AABB(reader)

        if version[:2] <= (3, 4):  # 3.4.2 and earlier
            m_Colors_size = reader.read_int()
            self.mColors = [reader.read_byte() / 0xFF for _ in range(m_Colors_size * 4)]

            m_CollisionTriangles_size = reader.read_int()
            reader.Position += m_CollisionTriangles_size * 4  # UInt32 indices
            m_CollisionVertexCount = reader.read_int()

        self.m_MeshUsageFlags = reader.read_int()
        if version >= (5,):  # 5.0 and up
            self.m_BakedConvexCollisionMesh = reader.read_bytes(reader.read_int())
            reader.align_stream()
            self.m_BakedTriangleCollisionMesh = reader.read_bytes(reader.read_int())
            reader.align_stream()

        if version >= (2018, 2):  # 2018.2 and up
            self.m_MeshMetrics = [reader.read_float(), reader.read_float()]

        if version >= (2018, 3):  # 2018.3 and up
            reader.align_stream()
            self.m_StreamData = StreamingInfo(reader, version)

            if self.m_StreamData.path and self.m_VertexData.m_VertexCount > 0:
                self.m_VertexData.m_DataSize = get_resource_data(
                    self.m_StreamData.path,
                    self.assets_file,
                    self.m_StreamData.offset,
                    self.m_StreamData.size,
                )

        # Fix channel after 2018.3
        version = self.version
        if version >= (3, 5):  # 3.5 and up
            self.ReadVertexData()

        if version >= (2, 6):  # 2.6.0 and later
            self.DecompressCompressedMesh()

        self.GetTriangles()

    def ReadVertexData(self):
        version = self.version
        m_VertexData = self.m_VertexData
        m_VertexCount = self.m_VertexCount = m_VertexData.m_VertexCount

        for chn, m_Channel in enumerate(m_VertexData.m_Channels):
            if m_Channel.dimension > 0:
                m_Stream = m_VertexData.m_Streams[m_Channel.stream]
                channelMask = bin(m_Stream.channelMask)[::-1]
                if channelMask[chn] == "1":
                    if version[0] < 2018 and chn == 2 and m_Channel.format == 2:
                        m_Channel.dimension = 4

                    componentByteSize = MeshHelper.GetFormatSize(
                        version, m_Channel.format
                    )
                    componentBytes = bytearray(
                        m_VertexCount * m_Channel.dimension * componentByteSize
                    )
                    for v in range(m_VertexCount):
                        vertexOffset = (
                            int(m_Stream.offset)
                            + m_Channel.offset
                            + int(m_Stream.stride) * v
                        )
                        for d in range(m_Channel.dimension):
                            componentOffsetSrc = (
                                vertexOffset + componentByteSize * d
                            )  # src offset
                            componentOffsetDst = componentByteSize * (
                                v * m_Channel.dimension + d
                            )  # dst offst

                            buff = m_VertexData.m_DataSize[
                                componentOffsetSrc : componentOffsetSrc
                                + componentByteSize
                            ]

                            if (
                                self.reader.endian == "<" and componentByteSize > 1
                            ):  # swap bytes
                                buff = buff[::-1]

                            componentBytes[
                                componentOffsetDst : componentOffsetDst
                                + componentByteSize
                            ] = buff
                            # Buffer.BlockCopy(m_VertexData.m_DataSize, componentOffset, componentBytes, componentByteSize * (v * m_Channel.dimension + d), componentByteSize);
                            # (Array src, int srcOffset, Array dst, int dstOffset, int count);

                    if MeshHelper.IsIntFormat(version, m_Channel.format):
                        componentsIntArray = MeshHelper.BytesToIntArray(
                            componentBytes, componentByteSize
                        )
                    else:
                        componentsFloatArray = MeshHelper.BytesToFloatArray(
                            componentBytes, componentByteSize
                        )

                    if version[0] >= 2018:
                        if chn == 0:  # kShaderChannelVertex
                            self.m_Vertices = componentsFloatArray
                        elif chn == 1:  # kShaderChannelNormal
                            self.m_Normals = componentsFloatArray
                        elif chn == 2:  # kShaderChannelTangent
                            self.m_Tangents = componentsFloatArray
                        elif chn == 3:  # kShaderChannelColor
                            self.m_Colors = componentsFloatArray
                        elif chn == 4:  # kShaderChannelTexCoord0
                            self.m_UV0 = componentsFloatArray
                        elif chn == 5:  # kShaderChannelTexCoord1
                            self.m_UV1 = componentsFloatArray
                        elif chn == 6:  # kShaderChannelTexCoord2
                            self.m_UV2 = componentsFloatArray
                        elif chn == 7:  # kShaderChannelTexCoord3
                            self.m_UV3 = componentsFloatArray
                        elif chn == 8:  # kShaderChannelTexCoord4
                            self.m_UV4 = componentsFloatArray
                        elif chn == 9:  # kShaderChannelTexCoord5
                            self.m_UV5 = componentsFloatArray
                        elif chn == 10:  # kShaderChannelTexCoord6
                            self.m_UV6 = componentsFloatArray
                        elif chn == 11:  # kShaderChannelTexCoord7
                            self.m_UV7 = componentsFloatArray
                        # 2018.2 and up
                        elif chn == 12:  # kShaderChannelBlendWeight
                            if not self.m_Skin:
                                self.InitMSkin()
                            for i in range(m_VertexCount):
                                for j in range(m_Channel.dimension):
                                    self.m_Skin[i].weight[j] = componentsFloatArray[
                                        i * m_Channel.dimension + j
                                    ]
                        elif chn == 13:  # kShaderChannelBlendIndices
                            if not self.m_Skin:
                                self.InitMSkin()
                            for i in range(m_VertexCount):
                                for j in range(m_Channel.dimension):
                                    self.m_Skin[i].boneIndex[j] = componentsIntArray[
                                        i * m_Channel.dimension + j
                                    ]
                    else:
                        if chn == 0:  # kShaderChannelVertex
                            self.m_Vertices = componentsFloatArray
                        elif chn == 1:  # kShaderChannelNormal
                            self.m_Normals = componentsFloatArray
                        elif chn == 2:  # kShaderChannelColor
                            self.m_Colors = componentsFloatArray
                        elif chn == 3:  # kShaderChannelTexCoord0
                            self.m_UV0 = componentsFloatArray
                        elif chn == 4:  # kShaderChannelTexCoord1
                            self.m_UV1 = componentsFloatArray
                        elif chn == 5:
                            if version[0] >= 5:  # kShaderChannelTexCoord2
                                self.m_UV2 = componentsFloatArray
                            else:  # kShaderChannelTangent
                                self.m_Tangents = componentsFloatArray
                        elif chn == 6:  # kShaderChannelTexCoord3
                            self.m_UV3 = componentsFloatArray
                        elif chn == 7:  # kShaderChannelTangent
                            self.m_Tangents = componentsFloatArray

    def DecompressCompressedMesh(self):
        # Vertex
        version = self.version
        m_CompressedMesh = self.m_CompressedMesh
        if m_CompressedMesh.m_Vertices.m_NumItems > 0:
            self.m_VertexCount = int(m_CompressedMesh.m_Vertices.m_NumItems / 3)
            self.m_Vertices = m_CompressedMesh.m_Vertices.UnpackFloats(3, 3 * 4)
        m_VertexCount = self.m_VertexCount
        # UV
        if m_CompressedMesh.m_UV.m_NumItems > 0:  #

            m_UVInfo = m_CompressedMesh.m_UVInfo
            if m_UVInfo != 0:
                kInfoBitsPerUV = 4
                kUVDimensionMask = 3
                kUVChannelExists = 4
                kMaxTexCoordShaderChannels = 8

                uvSrcOffset = 0

                for uv in range(kMaxTexCoordShaderChannels):
                    texCoordBits = m_UVInfo >> (uv * kInfoBitsPerUV)
                    texCoordBits &= (1 << kInfoBitsPerUV) - 1
                    if (texCoordBits & kUVChannelExists) != 0:
                        uvDim = 1 + int(texCoordBits & kUVDimensionMask)
                        m_UV = m_CompressedMesh.m_UV.UnpackFloats(
                            uvDim, uvDim * 4, uvSrcOffset, self.m_VertexCount
                        )
                        self.SetUV(uv, m_UV)
            else:
                self.m_UV0 = m_CompressedMesh.m_UV.UnpackFloats(
                    2, 2 * 4, 0, m_VertexCount
                )
                if m_CompressedMesh.m_UV.m_NumItems >= m_VertexCount * 4:  #
                    self.m_UV1 = m_CompressedMesh.m_UV.UnpackFloats(
                        2, 2 * 4, m_VertexCount * 2, m_VertexCount
                    )

        # BindPose
        if version < (5,):  # 5.0 down
            if m_CompressedMesh.m_BindPoses.m_NumItems > 0:  #
                m_BindPoses_Unpacked = m_CompressedMesh.m_BindPoses.UnpackFloats(
                    16, 4 * 16
                )
                self.m_BindPose = [
                    Matrix4x4(m_BindPoses_Unpacked[i : i + 16])
                    for i in range(0, m_CompressedMesh.m_BindPoses.m_NumItems, 16)
                ]
        # Normal
        if m_CompressedMesh.m_Normals.m_NumItems > 0:
            normalData = m_CompressedMesh.m_Normals.UnpackFloats(2, 4 * 2)
            signs = m_CompressedMesh.m_NormalSigns.UnpackInts()
            self.m_Normals = []  # float[m_CompressedMesh.m_Normals.m_NumItems / 2 * 3]
            for i in range(0, math.ceil(m_CompressedMesh.m_Normals.m_NumItems / 2)):
                x = normalData[i * 2 + 0]
                y = normalData[i * 2 + 1]
                zsqr = 1 - x * x - y * y
                if zsqr >= 0:
                    z = math.sqrt(zsqr)
                else:
                    z = 0
                    normal = Vector3(x, y, z)
                    normal.normalize()
                    x = normal.X
                    y = normal.Y
                    z = normal.Z
                if signs[i] == 0:
                    z = -z
                self.m_Normals.extend([x, y, z])
        # Tangent
        if m_CompressedMesh.m_Tangents.m_NumItems > 0:
            tangentData = m_CompressedMesh.m_Tangents.UnpackFloats(2, 4 * 2)
            signs = m_CompressedMesh.m_TangentSigns.UnpackInts()
            self.m_Tangents = (
                []
            )  # float[m_CompressedMesh.m_Tangents.m_NumItems / 2 * 4]
            for i in range(0, math.ceil(m_CompressedMesh.m_Tangents.m_NumItems / 2)):
                x = tangentData[i * 2 + 0]
                y = tangentData[i * 2 + 1]
                zsqr = 1 - x * x - y * y
                if zsqr >= 0:
                    z = math.sqrt(zsqr)
                else:
                    z = 0
                    vector3f = Vector3(x, y, z)
                    vector3f.normalize()
                    x = vector3f.X
                    y = vector3f.Y
                    z = vector3f.Z
                if signs[i * 2 + 0] == 0:  #
                    z = -z
                w = 1.0 if signs[i * 2 + 1] > 0 else -1.0
                self.m_Tangents.extend([x, y, z, w])

        # FloatColor
        if version >= (5,):  # 5.0 and up
            if m_CompressedMesh.m_FloatColors.m_NumItems > 0:  #
                self.m_Colors = m_CompressedMesh.m_FloatColors.UnpackFloats(1, 4)

        # Skin
        if m_CompressedMesh.m_Weights.m_NumItems > 0:
            weights = m_CompressedMesh.m_Weights.UnpackInts()
            boneIndices = m_CompressedMesh.m_BoneIndices.UnpackInts()
            self.InitMSkin()
            bonePos = 0
            boneIndexPos = 0
            j = 0
            sum = 0

            for i in range(m_CompressedMesh.m_Weights.m_NumItems):
                # read bone index and weight.
                self.m_Skin[bonePos].weight[j] = weights[i] / 31.0
                self.m_Skin[bonePos].boneIndex[j] = boneIndices[boneIndexPos]
                boneIndexPos += 1
                j += 1
                sum += weights[i]

                # the weights add up to one. fill the rest for this vertex with zero, and continue with next one.
                if sum >= 31:  #
                    while j < 4:
                        self.m_Skin[bonePos].weight[j] = 0
                        self.m_Skin[bonePos].boneIndex[j] = 0
                        j += 1

                    bonePos += 1
                    j = 0
                    sum = 0
                # we read three weights, but they don't add up to one. calculate the fourth one, and read
                # missing bone index. continue with next vertex.
                elif j == 3:  #
                    self.m_Skin[bonePos].weight[j] = (31 - sum) / 31.0
                    self.m_Skin[bonePos].boneIndex[j] = boneIndices[boneIndexPos]
                    boneIndexPos += 1
                    bonePos += 1
                    j = 0
                    sum = 0
        # IndexBuffer
        if m_CompressedMesh.m_Triangles.m_NumItems > 0:  #
            self.m_IndexBuffer = m_CompressedMesh.m_Triangles.UnpackInts()
        # Color
        if (
            hasattr(m_CompressedMesh, "m_Colors")
            and m_CompressedMesh.m_Colors.m_NumItems > 0
        ):
            m_CompressedMesh.m_Colors.m_NumItems *= 4
            m_CompressedMesh.m_Colors.m_BitSize /= 4
            tempColors = m_CompressedMesh.m_Colors.UnpackInts()
            self.m_Colors = [color / 255 for color in tempColors]

    def GetTriangles(self):
        m_IndexBuffer = self.m_IndexBuffer
        m_Indices = self.m_Indices

        for m_SubMesh in self.m_SubMeshes:
            firstIndex = m_SubMesh.firstByte // 2
            if not self.m_Use16BitIndices:
                firstIndex //= 2

            indexCount = m_SubMesh.indexCount
            topology = m_SubMesh.topology
            if topology == GfxPrimitiveType.kPrimitiveTriangles:
                m_Indices.extend(
                    m_IndexBuffer[firstIndex : firstIndex + indexCount - indexCount % 3]
                )

            elif (
                self.version[0] < 4
                or topology == GfxPrimitiveType.kPrimitiveTriangleStrip
            ):
                # de-stripify :
                triIndex = 0
                for i in range(indexCount - 2):
                    a, b, c = m_IndexBuffer[firstIndex + i : firstIndex + i + 3]

                    # skip degenerates
                    if a == b or a == c or b == c:
                        continue

                    # do the winding flip-flop of strips :
                    m_Indices.extend([b, a, c] if ((i & 1) == 1) else [a, b, c])
                    triIndex += 3
                # fix indexCount
                m_SubMesh.indexCount = triIndex

            elif topology == GfxPrimitiveType.kPrimitiveQuads:
                for q in range(0, indexCount, 4):
                    m_Indices.extend(
                        [
                            m_IndexBuffer[firstIndex + q],
                            m_IndexBuffer[firstIndex + q + 1],
                            m_IndexBuffer[firstIndex + q + 2],
                            m_IndexBuffer[firstIndex + q],
                            m_IndexBuffer[firstIndex + q + 2],
                            m_IndexBuffer[firstIndex + q + 3],
                        ]
                    )
                # fix indexCount
                m_SubMesh.indexCount = indexCount // 2 * 3

            else:
                raise NotImplementedError(
                    "Failed getting triangles. Submesh topology is lines or points."
                )

    def InitMSkin(self):
        self.m_Skin = [BoneWeights4() for _ in range(self.m_VertexCount)]

    def SetUV(self, uv: int, m_UV):
        if uv == 0:
            self.m_UV0 = m_UV
        elif uv == 1:
            self.m_UV1 = m_UV
        elif uv == 2:
            self.m_UV2 = m_UV
        elif uv == 3:
            self.m_UV3 == m_UV
        elif uv == 4:
            self.m_UV4 == m_UV
        elif uv == 5:
            self.m_UV5 == m_UV
        elif uv == 6:
            self.m_UV6 == m_UV
        elif uv == 7:
            self.m_UV7 == m_UV
        else:
            raise IndexError("Out of Range")

    def GetUV(self, uv: int):
        if uv == 0:
            return self.m_UV0
        elif uv == 1:
            return self.m_UV1
        elif uv == 2:
            return self.m_UV2
        elif uv == 3:
            return self.m_UV3
        elif uv == 4:
            return self.m_UV4
        elif uv == 5:
            return self.m_UV5
        elif uv == 6:
            return self.m_UV6
        elif uv == 7:
            return self.m_UV7
        else:
            raise IndexError("Out of Range")


class MeshHelper:
    @staticmethod
    def GetFormatSize(version, format: int) -> int:
        if version[0] < 2017:
            if format == VertexChannelFormat.kChannelFormatFloat:
                return 4
            elif format == VertexChannelFormat.kChannelFormatFloat16:
                return 2
            elif format == VertexChannelFormat.kChannelFormatColor:  # in 4.x is size 4
                return 1
            elif format == VertexChannelFormat.kChannelFormatByte:
                return 1
            elif format == VertexChannelFormat.kChannelFormatUInt32:  # in 5.x
                return 4
        elif version[0] < 2019:
            if format == VertexFormat.kVertexFormatFloat:
                return 4
            elif format == VertexFormat.kVertexFormatFloat16:
                return 2
            elif format == VertexFormat.kVertexFormatColor:
                return 1
            elif format == VertexFormat.kVertexFormatUNorm8:
                return 1
            elif format == VertexFormat.kVertexFormatSNorm8:
                return 1
            elif format == VertexFormat.kVertexFormatUNorm16:
                return 2
            elif format == VertexFormat.kVertexFormatSNorm16:
                return 2
            elif format == VertexFormat.kVertexFormatUInt8:
                return 1
            elif format == VertexFormat.kVertexFormatSInt8:
                return 1
            elif format == VertexFormat.kVertexFormatUInt16:
                return 2
            elif format == VertexFormat.kVertexFormatSInt16:
                return 2
            elif format == VertexFormat.kVertexFormatUInt32:
                return 4
            elif format == VertexFormat.kVertexFormatSInt32:
                return 4
        else:
            if format == VertexFormatV2019.kVertexFormatFloat:
                return 4
            elif format == VertexFormatV2019.kVertexFormatFloat16:
                return 2
            elif format == VertexFormatV2019.kVertexFormatUNorm8:
                return 1
            elif format == VertexFormatV2019.kVertexFormatSNorm8:
                return 1
            elif format == VertexFormatV2019.kVertexFormatUNorm16:
                return 2
            elif format == VertexFormatV2019.kVertexFormatSNorm16:
                return 2
            elif format == VertexFormatV2019.kVertexFormatUInt8:
                return 1
            elif format == VertexFormatV2019.kVertexFormatSInt8:
                return 1
            elif format == VertexFormatV2019.kVertexFormatUInt16:
                return 2
            elif format == VertexFormatV2019.kVertexFormatSInt16:
                return 2
            elif format == VertexFormatV2019.kVertexFormatUInt32:
                return 4
            elif format == VertexFormatV2019.kVertexFormatSInt32:
                return 4
        raise ValueError(format)

    @staticmethod
    def IsIntFormat(version, format: int) -> bool:
        if version[0] < 2017:
            return format == 4
        elif version[0] < 2019:
            return format >= 7
        else:
            return format >= 6

    @staticmethod
    def BytesToFloatArray(inputBytes, size):
        return [
            inputBytes[i] / 255.0
            if size == 1
            else struct.unpack(">e", inputBytes[i * 2 : i * 2 + 2])[0]
            if size == 2
            else struct.unpack(">f", inputBytes[i * 4 : i * 4 + 4])[0]
            if size == 4
            else 0
            for i in range(len(inputBytes) // size)
        ]

    @staticmethod
    def BytesToIntArray(inputBytes, size):
        return [
            int.from_bytes(inputBytes[i : i + size], byteorder="big", signed=True)
            for i in range(0, len(inputBytes), size)
        ]


class VertexChannelFormat(IntEnum):
    kChannelFormatFloat = 0
    kChannelFormatFloat16 = 1
    kChannelFormatColor = 2
    kChannelFormatByte = 3
    kChannelFormatUInt32 = 4


class VertexFormat(IntEnum):
    kVertexFormatFloat = 0
    kVertexFormatFloat16 = 1
    kVertexFormatColor = 2
    kVertexFormatUNorm8 = 3
    kVertexFormatSNorm8 = 4
    kVertexFormatUNorm16 = 5
    kVertexFormatSNorm16 = 6
    kVertexFormatUInt8 = 7
    kVertexFormatSInt8 = 8
    kVertexFormatUInt16 = 9
    kVertexFormatSInt16 = 10
    kVertexFormatUInt32 = 11
    kVertexFormatSInt32 = 12


class VertexFormatV2019(IntEnum):
    kVertexFormatFloat = 0
    kVertexFormatFloat16 = 1
    kVertexFormatUNorm8 = 2
    kVertexFormatSNorm8 = 3
    kVertexFormatUNorm16 = 4
    kVertexFormatSNorm16 = 5
    kVertexFormatUInt8 = 6
    kVertexFormatSInt8 = 7
    kVertexFormatUInt16 = 8
    kVertexFormatSInt16 = 9
    kVertexFormatUInt32 = 10
    kVertexFormatSInt32 = 11
