using System;
using System.Drawing;
using System.Numerics;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Render.Programs;
using SlatedGameToolkit.Framework.Graphics.Render.Shaders;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Utilities;

namespace SlatedGameToolkit.Framework.Graphics.Render
{
    public class MeshBatch : IDisposable {
        public bool Debug { get; set; }
        private const int VERTEX_LENGTH = 9;
        private bool disposed;
        private float batchDelta;
        public GLContext GLContext {get; private set; }
        private int projALoc, viewALoc, modelALoc, texturedALoc, singleChanneledALoc, flippedALoc;
        private Camera camera;
        private RenderProgram renderProgram;
        private ITexture texture;
        private VertexArrayBuffers vertexBuffers;
        public bool Batching { get; private set; }

        private Matrix4x4 modelsMatrix;
        private float[] data;
        private uint[] indices;
        private int[] lengths;
        private int[] indiceOffsets;
        private int[] verticeOffsets;
        private int dataIndex, indicesIndex, offsetIndex;

        public MeshBatch(Camera camera, RenderProgram renderProgram = null, uint BatchVertexSize = 4096) {
            if (renderProgram == null) {
                VertexShader vert = new VertexShader(EmbeddedResUtils.ReadEmbeddedResourceText("default.vert"));
                FragmentShader frag = new FragmentShader(EmbeddedResUtils.ReadEmbeddedResourceText("default.frag"));
                renderProgram = new RenderProgram(null, vert, frag);
            }
            this.renderProgram = renderProgram;
            this.GLContext = renderProgram.GLContext;
            this.camera = camera ?? throw new ArgumentNullException("camera");
            indices = new uint[BatchVertexSize];
            lengths = new int[BatchVertexSize];
            indiceOffsets = new int[BatchVertexSize];
            data = new float[BatchVertexSize * VERTEX_LENGTH];
            verticeOffsets = new int[BatchVertexSize];
            vertexBuffers = new VertexArrayBuffers(GLContext);
            VertexAttributeDefinition[] definitions = new VertexAttributeDefinition[3];
            definitions[0] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, "aPosition"), 3);
            definitions[1] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, "aColor"), 4);
            definitions[2] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, "aTexCoord"), 2);
            renderProgram.Use();
            modelALoc = GLContext.GetUniformLocation(renderProgram.Handle, "models");
            viewALoc = GLContext.GetUniformLocation(renderProgram.Handle, "view");
            projALoc = GLContext.GetUniformLocation(renderProgram.Handle, "projection");
            texturedALoc = GLContext.GetUniformLocation(renderProgram.Handle, "textured");
            singleChanneledALoc = GLContext.GetUniformLocation(renderProgram.Handle, "singleChanneled");
            flippedALoc = GLContext.GetUniformLocation(renderProgram.Handle, "flipped");
            vertexBuffers.defineVertexAttributes(definitions: definitions);

            GLContext.UniformMatrix4fv(projALoc, 1, false, camera.ProjectionMatrix.ToColumnMajorArray());
        }

        /// <summary>
        /// Begins a mesh batch for rendering.
        /// </summary>
        /// <param name="modelsMatrix">The models matrix.</param>
        /// <param name="delta">The time elapsed since the last update.</param>
        public virtual void Begin(Matrix4x4 modelsMatrix, double delta) {
            if (Batching) throw new InvalidOperationException("This batch is already started.");
            this.Batching = true;
            this.modelsMatrix = modelsMatrix;

            this.batchDelta = (float)delta;
        }

        /// <summary>
        /// Draws the given mesh.
        /// </summary>
        /// <param name="mesh">Draws the given mesh.</param>
        public virtual void Draw(IMesh mesh) {
            IMoveable moveable = mesh as IMoveable;
            if (moveable != null) {
                if (GameEngine.UpdatesPerSecond <= 0) {
                    moveable.Position = moveable.MoveTo;
                } else {
                    moveable.Position = moveable.Position + (moveable.MoveTo - moveable.Position) * batchDelta;
                }
            }
            if (mesh.Texture?.Handle != this.texture?.Handle) {
                Flush();
                this.texture = mesh.Texture;
                GLContext.Uniform1i(texturedALoc, texture == null ? 0 : 1);
            }
            if (texture != null) {
                GLContext.Uniform1i(singleChanneledALoc, texture.SingleChanneled ? 1 : 0);
                GLContext.Uniform1i(flippedALoc, texture.Flipped ? 1 : 0);
            }
            ValueTuple<Vector3, Vector2>[] vertices = mesh.Vertices;
            uint[] indices = mesh.Elements;
            if (vertices.Length * VERTEX_LENGTH + dataIndex >= data.Length || indices.Length + indicesIndex >= this.indices.Length) 
                Flush();
            for (int i = 0; i < vertices.Length; i++) {
                data[dataIndex] = vertices[i].Item1.X;
                dataIndex++;
                data[dataIndex] = vertices[i].Item1.Y;
                dataIndex++;
                data[dataIndex] = vertices[i].Item1.Z;
                dataIndex++;

                data[dataIndex] = mesh.Color.RedAsFloat();
                dataIndex++;
                data[dataIndex] = mesh.Color.GreenAsFloat();
                dataIndex++;
                data[dataIndex] = mesh.Color.BlueAsFloat();
                dataIndex++;
                data[dataIndex] = mesh.Color.AlphaAsFloat();
                dataIndex++;

                data[dataIndex] = vertices[i].Item2.X;
                dataIndex++;
                data[dataIndex] = vertices[i].Item2.Y;
                dataIndex++;
            }
            int elementsCount = indices.Length;
            Array.Copy(indices, 0, this.indices, indicesIndex, elementsCount);
            indiceOffsets[offsetIndex] = indicesIndex * sizeof(uint);
            indicesIndex += elementsCount;
            if (offsetIndex + 1 < verticeOffsets.Length) {
                verticeOffsets[offsetIndex + 1] = verticeOffsets[offsetIndex] + vertices.Length;
            }
            lengths[offsetIndex] = elementsCount;
            offsetIndex++;
        }
        
        public virtual void End() {
            if (!Batching) throw new InvalidOperationException("This batch was never started.");
            this.Batching = false;
            Flush();
        }

        public virtual void Flush() {
            if (offsetIndex == 0) return;
            if (texture != null) {
                GLContext.BindTexture(TextureTarget.Texture2D, texture.Handle);
            }
            renderProgram.Use();
            if (GameEngine.UpdatesPerSecond <= 0) {
                camera.Position = camera.MoveTo;
            } else {
                camera.Position = camera.Position + (camera.MoveTo - camera.Position) * batchDelta;
            }

            vertexBuffers.BufferVertices(data, true);
            vertexBuffers.BufferIndices(indices, true);
            GLContext.UniformMatrix4fv(modelALoc, 1, false, modelsMatrix.ToColumnMajorArray());
            if (camera.ViewChanged) GLContext.UniformMatrix4fv(viewALoc, 1, false, camera.ViewMatrix.ToColumnMajorArray());
            if (camera.ProjectionChanged) GLContext.UniformMatrix4fv(projALoc, 1, false, camera.ProjectionMatrix.ToColumnMajorArray());

            if (Debug) {
                GLContext.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Line);
            } else {
                GLContext.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);
            }
            GLContext.MultiDrawElementsBaseVertex(PrimitiveType.Triangles, lengths, DrawElementsType.UnsignedInt, indiceOffsets, offsetIndex, verticeOffsets);
            dataIndex = 0;
            indicesIndex = 0;
            offsetIndex = 0;
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                }
                if (disposed) return;
                if (Batching) End();
                disposed = true;
                vertexBuffers.Dispose();
                renderProgram.Dispose();
                data = null;
                indices = null;
                lengths = null;
                verticeOffsets = null;
            }
        }

        ~MeshBatch()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}