using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;

namespace FModel.Views.Snooper.Models;

public class VertexAttribute
{
    public int Size;
    public VertexAttribPointerType Type;
    public bool Enabled;
}

public abstract class UModel<TVertex> : IRenderableModel where TVertex : struct, IMeshVertex
{
    protected const int LodLevel = 0;

    private readonly UObject _export;
    private readonly List<VertexAttribute> _vertexAttributes =
    [
        new VertexAttribute { Size = 1, Type = VertexAttribPointerType.Int, Enabled = false },    // VertexIndex
        new VertexAttribute { Size = 3, Type = VertexAttribPointerType.Float, Enabled = true },   // Position
        new VertexAttribute { Size = 3, Type = VertexAttribPointerType.Float, Enabled = false },  // Normal
        new VertexAttribute { Size = 3, Type = VertexAttribPointerType.Float, Enabled = false },  // Tangent
        new VertexAttribute { Size = 2, Type = VertexAttribPointerType.Float, Enabled = false },  // UV
        new VertexAttribute { Size = 1, Type = VertexAttribPointerType.Float, Enabled = false },  // TextureLayer
        new VertexAttribute { Size = 1, Type = VertexAttribPointerType.Float, Enabled = false },  // Colors
        new VertexAttribute { Size = 4, Type = VertexAttribPointerType.Float, Enabled = false },  // BoneIds
        new VertexAttribute { Size = 4, Type = VertexAttribPointerType.Float, Enabled = false }   // BoneWeights
    ];

    public int Handle { get; set; }
    public BufferObject<uint> Ebo { get; set; }
    public BufferObject<float> Vbo { get; set; }
    public BufferObject<Matrix4x4> MatrixVbo { get; set; }
    public VertexArrayObject<float, uint> Vao { get; set; }

    public string Path { get; }
    public string Name { get; }
    public string Type { get; protected set; }
    public int UvCount { get; }
    public uint[] Indices { get; set; }
    public float[] Vertices { get; set; }
    public Section[] Sections { get; set; }
    public List<Transform> Transforms { get; }
    public Attachment Attachments { get; }

    public FBox Box { get; init; }
    public List<Socket> Sockets { get; }
    public List<Collision> Collisions { get; }
    public Material[] Materials { get; init; }
    public bool IsTwoSided { get; set; }
    public bool IsProp { get; set; }

    public int VertexSize => _vertexAttributes.Where(x => x.Enabled).Sum(x => x.Size);
    public bool HasVertexColors => _vertexAttributes[(int) EAttribute.Colors].Enabled;
    public bool HasSockets => Sockets.Count > 0;
    public bool HasCollisions => Collisions.Count > 0;
    public int TransformsCount => Transforms.Count;

    public bool IsSetup { get; set; }
    public bool IsVisible { get; set; }
    public bool IsSelected { get; set; }
    public bool ShowWireframe { get; set; }
    public bool ShowCollisions { get; set; }
    public int SelectedInstance { get; set; }

    protected UModel()
    {
        _export = null;
        UvCount = 1;

        Box = new FBox(new FVector(-2f), new FVector(2f));
        Sockets = new List<Socket>();
        Collisions = new List<Collision>();
        Transforms = new List<Transform>();
    }

    protected UModel(UObject export)
    {
        _export = export;
        Path = _export.GetPathName();
        Name = export.Name;
        Type = export.ExportType;
        UvCount = 1;

        Box = new FBox(new FVector(-2f), new FVector(2f));
        Sockets = new List<Socket>();
        Collisions = new List<Collision>();
        Transforms = new List<Transform>();
        Attachments = new Attachment(Name);

        _vertexAttributes[(int) EAttribute.Index].Enabled =
            _vertexAttributes[(int) EAttribute.Normals].Enabled =
                _vertexAttributes[(int) EAttribute.Tangent].Enabled =
                    _vertexAttributes[(int) EAttribute.UVs].Enabled =
                        _vertexAttributes[(int) EAttribute.Layer].Enabled = true;
    }

    protected UModel(UObject export, MeshLodDto<TVertex> lod, IReadOnlyList<FPackageIndex> materials, IReadOnlyList<TVertex> vertices, int numLods, Transform transform = null) : this(export)
    {
        var hasCustomUvs = lod.ExtraUvs.Length > 0;
        UvCount = hasCustomUvs ? Math.Max(lod.ExtraUvs.Length, numLods) : lod.ExtraUvs.Length + 1;
        IsTwoSided = lod.IsTwoSided;

        Indices = new uint[lod.Indices.Length];
        for (int i = 0; i < Indices.Length; i++)
        {
            Indices[i] = lod.Indices[i];
        }

        Materials = new Material[materials.Count];
        for (int m = 0; m < Materials.Length; m++)
        {
            if ((materials[m]?.TryLoad(out var material) ?? false) && material is UMaterialInterface unrealMaterial)
                Materials[m] = new Material(unrealMaterial); else Materials[m] = new Material();
        }

        _vertexAttributes[(int) EAttribute.Colors].Enabled = lod.VertexColors is { Length: > 0 };
        _vertexAttributes[(int) EAttribute.BonesId].Enabled =
            _vertexAttributes[(int) EAttribute.BonesWeight].Enabled = vertices is SkinnedMeshVertex[];

        Vertices = new float[vertices.Count * VertexSize];
        for (int i = 0; i < vertices.Count; i++)
        {
            var count = 0;
            var baseIndex = i * VertexSize;
            var vert = vertices[i];
            Vertices[baseIndex + count++] = i;
            Vertices[baseIndex + count++] = vert.Position.X * Constants.SCALE_DOWN_RATIO;
            Vertices[baseIndex + count++] = vert.Position.Z * Constants.SCALE_DOWN_RATIO;
            Vertices[baseIndex + count++] = vert.Position.Y * Constants.SCALE_DOWN_RATIO;
            Vertices[baseIndex + count++] = vert.Normal.X;
            Vertices[baseIndex + count++] = vert.Normal.Z;
            Vertices[baseIndex + count++] = vert.Normal.Y;
            Vertices[baseIndex + count++] = vert.Tangent.X;
            Vertices[baseIndex + count++] = vert.Tangent.Z;
            Vertices[baseIndex + count++] = vert.Tangent.Y;
            Vertices[baseIndex + count++] = vert.Uv.U;
            Vertices[baseIndex + count++] = vert.Uv.V;
            Vertices[baseIndex + count++] = hasCustomUvs ? lod.ExtraUvs[0][i].U - 1 : .5f;

            if (HasVertexColors)
            {
                Vertices[baseIndex + count++] = lod.VertexColors![0].Colors[i].ToPackedARGB();
            }

            if (vert is SkinnedMeshVertex skelVert)
            {
                int max = skelVert.Influences.Length;
                for (int j = 0; j < 8; j++)
                {
                    var boneID = j < max ? skelVert.Influences[j].Bone : (ushort) 0;
                    var weight = j < max ? skelVert.Influences[j].RawWeight : (ushort) 0;

                    // Pack bone ID and weight
                    Vertices[baseIndex + count++] = (boneID << 16) | weight;
                }
            }
        }

        Sections = new Section[lod.Sections.Length];
        for (var s = 0; s < Sections.Length; s++)
        {
            var section = lod.Sections[s];
            Sections[s] = new Section(section.MaterialIndex, section.NumFaces * 3, section.FirstIndex);
            if (section.IsValid) Sections[s].SetupMaterial(Materials[section.MaterialIndex]);
        }

        AddInstance(transform ?? Transform.Identity);
    }

    public virtual void Setup(Options options)
    {
        Handle = GL.CreateProgram();
        Ebo = new BufferObject<uint>(Indices, BufferTarget.ElementArrayBuffer);
        Vbo = new BufferObject<float>(Vertices, BufferTarget.ArrayBuffer);
        Vao = new VertexArrayObject<float, uint>(Vbo, Ebo);

        var offset = 0;
        var broken = GL.GetInteger(GetPName.MaxTextureCoords) == 0;
        for (int i = 0; i < _vertexAttributes.Count; i++)
        {
            var attribute = _vertexAttributes[i];
            if (!attribute.Enabled) continue;

            if (i != 5 || !broken)
            {
                Vao.VertexAttributePointer((uint) i, attribute.Size, attribute.Type, VertexSize, offset);
            }

            offset += attribute.Size;
        }

        SetupInstances(); // instanced models transform

        // setup all used materials for use in different UV channels
        for (var i = 0; i < Materials.Length; i++)
        {
            if (!Materials[i].IsUsed) continue;
            Materials[i].Setup(options, broken ? 1 : UvCount);
        }

        foreach (var collision in Collisions)
        {
            collision.Setup();
        }

        if (options.Models.Count == 1 && Sections.All(x => !x.Show)) // visible if alone and invisible
        {
            IsVisible = true;
            foreach (var section in Sections)
            {
                section.Show = true;
            }
        }
        else if (!IsVisible) // default: visible if one section is visible
        {
            foreach (var section in Sections)
            {
                if (section.Show)
                {
                    IsVisible = true;
                    break;
                }
            }
        }
        else foreach (var section in Sections) // force visibility
        {
            section.Show = true;
        }

        IsSetup = true;
    }

    public virtual void Render(Shader shader, Texture checker = null, bool outline = false)
    {
        if (outline) GL.Disable(EnableCap.DepthTest);
        if (IsTwoSided) GL.Disable(EnableCap.CullFace);
        if (IsSelected)
        {
            GL.Enable(EnableCap.StencilTest);
            GL.StencilFunc(outline ? StencilFunction.Notequal : StencilFunction.Always, 1, 0xFF);
        }

        if (this is SkeletalModel skeletalModel) skeletalModel.Render(shader);
        else if (this is SplineModel splineModel) splineModel.Render(shader);
        else
        {
            shader.SetUniform("uIsAnimated", false);
            shader.SetUniform("uIsSpline", false);
        }

        if (!outline)
        {
            shader.SetUniform("uUvCount", UvCount);
            shader.SetUniform("uOpacity", ShowCollisions && IsSelected ? 0.75f : 1f);
            shader.SetUniform("uHasVertexColors", HasVertexColors);
        }

        Vao.Bind();
        GL.PolygonMode(TriangleFace.FrontAndBack, ShowWireframe ? PolygonMode.Line : PolygonMode.Fill);
        foreach (var section in Sections)
        {
            if (!section.Show) continue;
            if (!outline)
            {
                if (checker != null)
                {
                    shader.SetUniform("uParameters.Diffuse[0].Sampler", 0);
                    checker.Bind(TextureUnit.Texture0);
                }
                else
                {
                    shader.SetUniform("uSectionColor", section.Color);
                    Materials[section.MaterialIndex].Render(shader);
                }
            }

            GL.DrawElementsInstanced(PrimitiveType.Triangles, section.FacesCount, DrawElementsType.UnsignedInt, section.FirstFaceIndexPtr, TransformsCount);
        }
        GL.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill);
        Vao.Unbind();

        if (IsSelected)
        {
            GL.StencilFunc(StencilFunction.Always, 0, 0xFF);
            GL.Disable(EnableCap.StencilTest);
        }
        if (IsTwoSided) GL.Enable(EnableCap.CullFace);
        if (outline) GL.Enable(EnableCap.DepthTest);
    }

    public void PickingRender(Shader shader)
    {
        if (IsTwoSided) GL.Disable(EnableCap.CullFace);
        if (this is SkeletalModel skeletalModel) skeletalModel.Render(shader);
        if (this is SplineModel splineModel) splineModel.Render(shader);
        else
        {
            shader.SetUniform("uIsAnimated", false);
            shader.SetUniform("uIsSpline", false);
        }

        Vao.Bind();
        foreach (var section in Sections)
        {
            if (!section.Show) continue;
            GL.DrawElementsInstanced(PrimitiveType.Triangles, section.FacesCount, DrawElementsType.UnsignedInt, section.FirstFaceIndexPtr, TransformsCount);
        }
        Vao.Unbind();

        if (IsTwoSided) GL.Enable(EnableCap.CullFace);
    }

    public virtual void RenderCollision(Shader shader)
    {
        shader.SetUniform("uInstanceMatrix", GetTransform().Matrix);
        shader.SetUniform("uScaleDown", Constants.SCALE_DOWN_RATIO);
    }

    public void Update(Options options)
    {
        MatrixVbo.Bind();
        for (int instance = 0; instance < TransformsCount; instance++)
        {
            MatrixVbo.Update(instance, Transforms[instance].Matrix);
        }
        MatrixVbo.Unbind();

        var worldMatrix = GetTransform().Matrix;
        foreach (var socket in Sockets)
        {
            if (!socket.IsDaron) continue;

            var boneMatrix = Matrix4x4.Identity;
            if (this is SkeletalModel skeletalModel && skeletalModel.Skeleton.BonesByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var bone))
                boneMatrix = skeletalModel.Skeleton.GetBoneMatrix(bone);

            var socketRelation = boneMatrix * worldMatrix;
            foreach (var info in socket.AttachedModels)
            {
                if (!options.TryGetModel(info.Guid, out var attachedModel))
                    continue;

                attachedModel.Transforms[info.Instance].Relation = socket.Transform.LocalMatrix * socketRelation;
                attachedModel.Update(options);
            }
        }
    }

    public void AddInstance(Transform transform)
    {
        SelectedInstance = TransformsCount;
        Transforms.Add(transform);
    }

    public void SetupInstances()
    {
        MatrixVbo = new BufferObject<Matrix4x4>(TransformsCount, BufferTarget.ArrayBuffer);
        for (int instance = 0; instance < TransformsCount; instance++)
        {
            Transforms[instance].Save();
            MatrixVbo.Update(instance, Transforms[instance].Matrix);
        }
        Vao.BindInstancing(); // VertexAttributePointer
    }

    public Transform GetTransform() => Transforms[SelectedInstance];
    public Matrix4x4 GetSocketTransform(int index)
    {
        var socket = Sockets[index];
        var worldMatrix = GetTransform().Matrix;
        var boneMatrix = Matrix4x4.Identity;
        if (this is SkeletalModel skeletalModel && skeletalModel.Skeleton.BonesByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var bone))
            boneMatrix = skeletalModel.Skeleton.GetBoneMatrix(bone);

        var socketRelation = boneMatrix * worldMatrix;
        return socket.Transform.LocalMatrix * socketRelation;
    }

    public void AddToExportSession(ExportSession session)
    {
        session.Add(_export);
    }

    public virtual void Dispose()
    {
        Ebo?.Dispose();
        Vbo?.Dispose();
        MatrixVbo?.Dispose();
        Vao?.Dispose();
        foreach (var socket in Sockets)
        {
            socket?.Dispose();
        }
        Sockets.Clear();
        foreach (var collision in Collisions)
        {
            collision?.Dispose();
        }
        Collisions.Clear();

        GL.DeleteProgram(Handle);
    }
}
