using System.Numerics;
using System.Text;

namespace KeepersCompound.LGS;

// TODO: Remove all the things that don't actually need to be stored
public class ModelFile
{
    public readonly struct BHeader
    {
        public string Signature { get; }
        public int Version { get; }

        public BHeader(BinaryReader reader)
        {
            Signature = reader.ReadNullString(4);
            Version = reader.ReadInt32();
        }
    }

    public readonly struct MHeader
    {
        public string Name { get; }
        public float Radius { get; }
        public float MaxPolygonRadius { get; }
        public Vector3 MaxBounds { get; }
        public Vector3 MinBounds { get; }
        public Vector3 Center { get; }

        public ushort PolygonCount { get; }
        public ushort VertexCount { get; }
        public ushort ParameterCount { get; }
        public byte MaterialCount { get; }
        public byte VCallCount { get; }
        public byte VHotCount { get; }
        public byte ObjectCount { get; }

        public uint ObjectOffset { get; }
        public uint MaterialOffset { get; }
        public uint UvOffset { get; }
        public uint VHotOffset { get; }
        public uint VertexOffset { get; }
        public uint LightOffset { get; }
        public uint NormalOffset { get; }
        public uint PolygonOffset { get; }
        public uint NodeOffset { get; }

        public uint ModelSize { get; }

        public uint AuxMaterialFlags { get; }
        public uint AuxMaterialOffset { get; }
        public uint AuxMaterialSize { get; }

        public MHeader(BinaryReader reader, int version)
        {
            Name = reader.ReadNullString(8);
            Radius = reader.ReadSingle();
            MaxPolygonRadius = reader.ReadSingle();
            MaxBounds = reader.ReadVec3();
            MinBounds = reader.ReadVec3();
            Center = reader.ReadVec3();
            PolygonCount = reader.ReadUInt16();
            VertexCount = reader.ReadUInt16();
            ParameterCount = reader.ReadUInt16();
            MaterialCount = reader.ReadByte();
            VCallCount = reader.ReadByte();
            VHotCount = reader.ReadByte();
            ObjectCount = reader.ReadByte();
            ObjectOffset = reader.ReadUInt32();
            MaterialOffset = reader.ReadUInt32();
            UvOffset = reader.ReadUInt32();
            VHotOffset = reader.ReadUInt32();
            VertexOffset = reader.ReadUInt32();
            LightOffset = reader.ReadUInt32();
            NormalOffset = reader.ReadUInt32();
            PolygonOffset = reader.ReadUInt32();
            NodeOffset = reader.ReadUInt32();
            ModelSize = reader.ReadUInt32();

            if (version == 4)
            {
                AuxMaterialFlags = reader.ReadUInt32();
                AuxMaterialOffset = reader.ReadUInt32();
                AuxMaterialSize = reader.ReadUInt32();
            }
            else
            {
                AuxMaterialFlags = 0;
                AuxMaterialOffset = 0;
                AuxMaterialSize = 0;
            }
        }
    }

    public struct SubObject
    {
        public string Name;
        public byte Type;
        public int Joint;
        public float MinJointValue;
        public float MaxJointValue;
        public Matrix4x4 Transform;
        public short Child;
        public short Next;
        public ushort VhotIdx;
        public ushort VhotCount;
        public ushort PointIdx;
        public ushort PointCount;
        public ushort LightIdx;
        public ushort LightCount;
        public ushort NormalIdx;
        public ushort NormalCount;
        public ushort NodeIdx;
        public ushort NodeCount;

        public SubObject(BinaryReader reader)
        {
            Name = reader.ReadNullString(8);
            Type = reader.ReadByte();
            Joint = reader.ReadInt32();
            MinJointValue = reader.ReadSingle();
            MaxJointValue = reader.ReadSingle();
            var v1 = reader.ReadVec3();
            var v2 = reader.ReadVec3();
            var v3 = reader.ReadVec3();
            var v4 = reader.ReadVec3();
            Transform = new Matrix4x4(v1.X, v1.Y, v1.Z, 0, v2.X, v2.Y, v2.Z, 0, v3.X, v3.Y, v3.Z, 0, v4.X, v4.Y, v4.Z, 1);
            Child = reader.ReadInt16();
            Next = reader.ReadInt16();
            VhotIdx = reader.ReadUInt16();
            VhotCount = reader.ReadUInt16();
            PointIdx = reader.ReadUInt16();
            PointCount = reader.ReadUInt16();
            LightIdx = reader.ReadUInt16();
            LightCount = reader.ReadUInt16();
            NormalIdx = reader.ReadUInt16();
            NormalCount = reader.ReadUInt16();
            NodeIdx = reader.ReadUInt16();
            NodeCount = reader.ReadUInt16();
        }
    }
    
    public struct Polygon
    {
        public ushort Index;
        public ushort Data;
        public byte Type;
        public byte VertexCount;
        public ushort Normal;
        public float D;
        public ushort[] VertexIndices;
        public ushort[] LightIndices;
        public ushort[] UvIndices;
        public byte Material;

        public Polygon(BinaryReader reader, int version)
        {
            Index = reader.ReadUInt16();
            Data = reader.ReadUInt16();
            Type = reader.ReadByte();
            VertexCount = reader.ReadByte();
            Normal = reader.ReadUInt16();
            D = reader.ReadSingle();
            VertexIndices = new ushort[VertexCount];
            for (var i = 0; i < VertexCount; i++)
            {
                VertexIndices[i] = reader.ReadUInt16();
            }
            LightIndices = new ushort[VertexCount];
            for (var i = 0; i < VertexCount; i++)
            {
                LightIndices[i] = reader.ReadUInt16();
            }
            UvIndices = new ushort[Type == 0x1B ? VertexCount : 0];
            for (var i = 0; i < UvIndices.Length; i++)
            {
                UvIndices[i] = reader.ReadUInt16();
            }

            Material = version == 4 ? reader.ReadByte() : (byte)0;
        }
    }

    public struct Material
    {
        public string Name;
        public byte Type;
        public byte Slot;
        public uint Handle;
        public float Uv;

        public Material(BinaryReader reader)
        {
            Name = reader.ReadNullString(16);
            Type = reader.ReadByte();
            Slot = reader.ReadByte();
            Handle = reader.ReadUInt32();
            Uv = reader.ReadSingle();
        }
    }

    public enum VhotId
    {
        LightPosition = 1,
        LightDirection = 8,
        Anchor = 2,
        Particle1 = 3,
        Particle2 = 4,
        Particle3 = 5,
        Particle4 = 6,
        Particle5 = 7,
    }

    public struct VHot
    {
        public int Id;
        public Vector3 Position;

        public VHot(BinaryReader reader)
        {
            Id = reader.ReadInt32();
            Position = reader.ReadVec3();
        }
    }

    public BHeader BinHeader { get; set; }
    public MHeader Header { get; set; }
    public Vector3[] Vertices { get; }
    public Vector2[] Uvs { get; }
    public Vector3[] Normals { get; }
    public Polygon[] Polygons { get; }
    public Material[] Materials { get; }
    public VHot[] VHots { get; }
    public SubObject[] Objects { get; }

    public ModelFile(string filename)
    {
        if (!File.Exists(filename)) return;

        using MemoryStream stream = new(File.ReadAllBytes(filename));
        using BinaryReader reader = new(stream, Encoding.UTF8, false);

        BinHeader = new BHeader(reader);
        if (BinHeader.Signature != "LGMD") return;

        Header = new MHeader(reader, BinHeader.Version);
        stream.Seek(Header.VertexOffset, SeekOrigin.Begin);
        Vertices = new Vector3[Header.VertexCount];
        for (var i = 0; i < Vertices.Length; i++)
        {
            Vertices[i] = reader.ReadVec3();
        }
        stream.Seek(Header.UvOffset, SeekOrigin.Begin);
        Uvs = new Vector2[(Header.VHotOffset - Header.UvOffset) / 8];
        for (var i = 0; i < Uvs.Length; i++)
        {
            Uvs[i] = reader.ReadVec2();
        }
        stream.Seek(Header.NormalOffset, SeekOrigin.Begin);
        Normals = new Vector3[(Header.PolygonOffset - Header.NormalOffset) / 12];
        for (var i = 0; i < Normals.Length; i++)
        {
            Normals[i] = reader.ReadVec3();
        }
        stream.Seek(Header.PolygonOffset, SeekOrigin.Begin);
        Polygons = new Polygon[Header.PolygonCount];
        for (var i = 0; i < Polygons.Length; i++)
        {
            Polygons[i] = new Polygon(reader, BinHeader.Version);
        }
        stream.Seek(Header.MaterialOffset, SeekOrigin.Begin);
        Materials = new Material[Header.MaterialCount];
        for (var i = 0; i < Materials.Length; i++)
        {
            Materials[i] = new Material(reader);
        }
        stream.Seek(Header.VHotOffset, SeekOrigin.Begin);
        VHots = new VHot[Header.VHotCount];
        for (var i = 0; i < VHots.Length; i++)
        {
            VHots[i] = new VHot(reader);
        }
        stream.Seek(Header.ObjectOffset, SeekOrigin.Begin);
        Objects = new SubObject[Header.ObjectCount];
        for (var i = 0; i < Objects.Length; i++)
        {
            Objects[i] = new SubObject(reader);
        }
    }

    // TODO: Apply transforms to normals and stuff
    public void ApplyJoints(float[] joints)
    {
        // Build map of objects to their parent id
        var objCount = Objects.Length;
        var parentIds = new int[objCount];
        for (var i = 0; i < objCount; i++)
        {
            parentIds[i] = -1;
        }
        for (var i = 0; i < objCount; i++)
        {
            var subObj = Objects[i];
            var childIdx = subObj.Child;
            while (childIdx != -1)
            {
                parentIds[childIdx] = i;
                childIdx = Objects[childIdx].Next;
            }
        }
        
        // Calculate base transforms for every subobj (including joint)
        var subObjTransforms = new Matrix4x4[objCount];
        for (var i = 0; i < objCount; i++)
        {
            var subObj = Objects[i];
            var objTrans = Matrix4x4.Identity;
            if (subObj.Joint != -1)
            {
                var ang = subObj.Joint >= joints.Length ? 0 : float.DegreesToRadians(joints[subObj.Joint]);
                // TODO: Is this correct? Should I use a manual rotation matrix?
                var jointRot = Matrix4x4.CreateFromYawPitchRoll(0, ang, 0);
                objTrans = jointRot * subObj.Transform;
            }
           
            subObjTransforms[i] = objTrans;
        }
        
        // Apply sub object transforms
        for (var i = 0; i < objCount; i++)
        {
            var subObj = Objects[i];
            var transform = subObjTransforms[i];
        
            // Build compound transformation
            var parentId = parentIds[i];
            while (parentId != -1)
            {
                transform *= subObjTransforms[parentId];
                parentId = parentIds[parentId];
            }

            for (var j = 0; j < subObj.VhotCount; j++)
            {
                var v = VHots[subObj.VhotIdx + j];
                v.Position = Vector3.Transform(v.Position, transform);
                VHots[subObj.VhotIdx + j] = v;
            }

            for (var j = 0; j < subObj.PointCount; j++)
            {
                var v = Vertices[subObj.PointIdx + j];
                Vertices[subObj.PointIdx + j] = Vector3.Transform(v, transform);
            }
        }
    }

    public bool TryGetVhot(VhotId id, out VHot vhot)
    {
        foreach (var v in VHots)
        {
            if (v.Id == (int)id)
            {
                vhot = v;
                return true;
            }
        }
        vhot = new VHot();
        return false;
    }
}