using System;
using System.IO;
using System.Collections.Generic;
using Quik.VertexGenerator;
using static Quik.OpenGL.GLEnum;

namespace Quik.OpenGL
{
    public class GL21Driver : IDisposable
    {
        private int program;
        private int v2Position;
        private int iZIndex;
        private int v2TexPos;
        private int v4Color;
        private int m4Transforms;
        private int iMaxZ;
        private int iEnableSdf;
        private int iEnableTexture;
        private int iAlphaDiscard;
        private int fSdfThreshold;
        private int txTexture;
        private bool isDiposed;
        private readonly Dictionary<DrawQueue, DrawData> data = new Dictionary<DrawQueue, DrawData>();
        public bool IsInit { get; private set; } = false;
        public event Action<GL21Driver> OnGCDispose;

        public GL21Driver()
        {
        }

        ~GL21Driver()
        {
            Dispose(false);
        }

        public void Init()
        {
            if (IsInit) return;

            int vs = CreateShader(GL_VERTEX_SHADER, "Quik.res.gl21.vert");
            int fs = CreateShader(GL_FRAGMENT_SHADER, "Quik.res.gl21.frag");

            program = GL.CreateProgram();
            GL.AttachShader(program, vs);
            GL.AttachShader(program, fs);

            GL.LinkProgram(program);

            if (CheckProgram(program, out string msg) == false)
            {
                GraphicsException ex = new GraphicsException("Could not link shader program.");
                ex.Data.Add("Program Info Log", msg);
            }

            GL.DeleteShader(vs);
            GL.DeleteShader(fs);

            v2Position = GL.GetAttribLocation(program, nameof(v2Position));
            iZIndex    = GL.GetAttribLocation(program, nameof(iZIndex));
            v2TexPos   = GL.GetAttribLocation(program, nameof(v2TexPos));
            v4Color    = GL.GetAttribLocation(program, nameof(v4Color));

            m4Transforms   = GL.GetUniformLocation(program, nameof(m4Transforms));
            iMaxZ          = GL.GetUniformLocation(program, nameof(iMaxZ));
            iEnableSdf     = GL.GetUniformLocation(program, nameof(iEnableSdf));
            iEnableTexture = GL.GetUniformLocation(program, nameof(iEnableSdf));
            iAlphaDiscard  = GL.GetUniformLocation(program, nameof(iAlphaDiscard));
            txTexture      = GL.GetUniformLocation(program, nameof(txTexture));

            IsInit = true;
        }

        private void AssertInit()
        {
            if (!IsInit) throw new InvalidOperationException("Initialize the driver first.");
        }

        public void Draw(DrawQueue queue, in QRectangle view)
        {
            AssertInit();

            if (!data.TryGetValue(queue, out DrawData draw))
            {
                draw = new DrawData(this, queue);
                data.Add(queue, draw);
            }

            // This already binds the vertex array for me.
            draw.PrepareFrame();

            QVec2 size = view.Size;
            QMat4.Orthographic(out QMat4 matrix, view);

            GL.UseProgram(program);
            GL.Uniform1(iMaxZ, queue.ZDepth);
            GL.UniformMatrix4(m4Transforms, false, in matrix);
            GL.Uniform1(fSdfThreshold, 0.0f);
            GL.Uniform1(txTexture, 0);
            GL.Enable(GL_BLEND);
            GL.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

            foreach (DrawCall call in queue)
            {
                GL.ActiveTexture(GL_TEXTURE0);
                GL.BindTexture(GL_TEXTURE_2D, 0);
                if (call.Texture != null)
                {
                    // TODO: actually use textures?
                    GL.Uniform1(iEnableTexture, 1);
                    GL.Uniform1(iEnableSdf, call.Texture.SignedDistanceField ? 1 : 0);
                    GL.Uniform1(iAlphaDiscard, 1);
                }
                else
                {
                    GL.Uniform1(iEnableTexture, 0);
                }

                GL.DrawArrays(GL_TRIANGLES, call.Count, call.Start);
            }
        }

        public void ClearDrawQueue(DrawQueue queue)
        {
            AssertInit();

            if (!data.TryGetValue(queue, out DrawData draw))
                return;
            draw.Dispose();
            data.Remove(queue);
        }

        private static int CreateShader(GLEnum type, string name)
        {
            StreamReader source = new StreamReader(typeof(GL21Driver).Assembly.GetManifestResourceStream(name));
            string text = source.ReadToEnd();
            source.Dispose();

            int shader = GL.CreateShader(type);
            GL.ShaderSource(shader, text);

            if (CheckShader(shader, out string msg) == false)
            {
                GraphicsException ex = new GraphicsException($"Failed to compile {type} shader stage.");
                ex.Data.Add("Shader Stage", type);
                ex.Data.Add("Shader Info Log", msg);
                ex.Data.Add("Shader Source", text);
                throw ex;
            }

            return shader;
        }

        private static bool CheckShader(int shader, out string message)
        {
            message = string.Empty;

            GL.GetShader(shader, GL_COMPILE_STATUS, out int i);

            if (i != (int)GL_OK)
            {
                message = GL.GetShaderInfoLog(shader);
                return false;
            }

            return true;
        }

        private static bool CheckProgram(int program, out string message)
        {
            message = string.Empty;

            GL.GetProgram(program, GL_LINK_STATUS, out int i);

            if (i != (int)GL_OK)
            {
                message = GL.GetProgramInfoLog(program);

                return false;
            }

            return true;
        }

        private void Dispose(bool disposing)
        {
            if (isDiposed) return;

            if (!IsInit)
            {
                isDiposed = true;
                return;
            }

            if (!disposing)
            {
                if (OnGCDispose == null)
                {
                    throw new Exception("This object must strictly be disposed from the owning thread, not GC");
                }
                else
                {
                    OnGCDispose(this);
                    return;
                }
            }

            GL.DeleteProgram(program);

            foreach (DrawData datum in data.Values)
            {
                datum.Dispose();
            }

            isDiposed = true;
            GC.SuppressFinalize(this);
        }

        public void Dispose() => Dispose(true);

        private class DrawData : IDisposable
        {
            public DrawQueue Queue { get; }
            public int VertexArray { get; }

            private readonly GL21Driver driver;
            private int vbo1, vbo2;
            private int ebo1, ebo2;

            public DrawData(GL21Driver driver, DrawQueue queue)
            {
                int a = 0, b = 0, c = 0, d = 0;
                Queue = queue;
                this.driver = driver;
                VertexArray = GL.GenVertexArray();
                GL.GenBuffers(4, out a);
                vbo1  = a;
                vbo2  = b;
                ebo1 = c;
                ebo2 = d;
                isDisposed = false;
            }

            public void PrepareFrame()
            {
                int vbo, ebo;
                vbo = Swap(ref vbo1, ref vbo2);
                ebo = Swap(ref ebo1, ref ebo2);

                if (Queue.VertexCount == 0 || Queue.ElementCount == 0)
                    return;

                GL.BindVertexArray(VertexArray);
                GL.BindBuffer(GL_ARRAY_BUFFER, vbo);
                GL.BufferData(GL_ARRAY_BUFFER, QuikVertex.Stride * Queue.VertexCount, Queue.VertexArray, GL_STREAM_DRAW);

                GL.VertexAttribPointer(driver.v2Position, 2, GL_FLOAT, false, QuikVertex.Stride, QuikVertex.PositionOffset);
                GL.VertexAttribIPointer(driver.iZIndex, 1, GL_UNSIGNED_INT, QuikVertex.Stride, QuikVertex.ZIndexOffset);
                GL.VertexAttribPointer(driver.v2TexPos, 2, GL_FLOAT, false, QuikVertex.Stride, QuikVertex.TextureCoordinatesOffset);
                GL.VertexAttribPointer(driver.v4Color, 4, GL_FLOAT, false, QuikVertex.Stride, QuikVertex.ColorOffset);

                GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
                GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, Queue.ElementCount * sizeof(int), Queue.ElementArray, GL_STREAM_DRAW);

                int Swap(ref int a, ref int b)
                {
                    a ^= b;
                    b ^= a;
                    a ^= b;
                    return a;
                }
            }

            private bool isDisposed;
            public void Dispose()
            {
                if (isDisposed) return;

                GL.DeleteVertexArray(VertexArray);
                GL.DeleteBuffer(vbo1);
                GL.DeleteBuffer(vbo2);
                GL.DeleteBuffer(ebo1);
                GL.DeleteBuffer(ebo2);
            }
        }
    }
}