using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Numerics;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Render;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Utilities;
using SlatedGameToolkit.Framework.Utilities.Collections;
using StbTrueTypeSharp;

namespace SlatedGameToolkit.Framework.Graphics.Text
{
    public class BitmapFont : IDisposable
    {
        private GLContext context;
        private LRUCache<char, int> glyphIndices;
        private LRUCache<char, CharacterMetrics> metricsCache;
        private LRUCache<(int, int), int> kerningCache;
        FontTexture[] textures;
        StbTrueType.stbtt_fontinfo info;
        private int spaceAdvance;
        private float scale;
        public float PixelHeight
        {
            get
            {
                return scale * (ascent - descent);
            }
            set
            {
                this.scale = value / (ascent - descent);
            }
        }
        private Dictionary<(char, float), int> glyphTexLocations = new Dictionary<(char, float), int>();
        private int ascent, descent, lineGap;
        public float PixelsPerUnitHeight { get; set; }
        public float PixelsPerUnitWidth { get; set; }
        private int drawingTo;
        private bool disposed;

        public unsafe BitmapFont(byte[] data, GLContext glContext = null, int cacheSize = 1024, int textures = 2, uint textureSizes = 512) {
            info = new StbTrueType.stbtt_fontinfo();
            fixed(byte* dataPtr = &data[0]) {
                StbTrueType.stbtt_InitFont(info, dataPtr, 0);
            }

            this.glyphIndices = new LRUCache<char, int>(cacheSize);
            this.metricsCache = new LRUCache<char, CharacterMetrics>(cacheSize);
            this.kerningCache = new LRUCache<(int, int), int>(cacheSize);
            this.textures = new FontTexture[textures];

            this.context = glContext ?? WindowContextsManager.CurrentGL;
            for (int i = 0; i < this.textures.Length; i++) {
                this.textures[i] = new FontTexture(info, context, glyphIndices, textureSizes);
            }
            PixelHeight = 64;
            int spaceAdvance, leftSideBearing;
            StbTrueType.stbtt_GetCodepointHMetrics(info, ' ', &spaceAdvance, &leftSideBearing);
            this.spaceAdvance = spaceAdvance;
            int ascent, descent, lineGap;
            StbTrueType.stbtt_GetFontVMetrics(info, &ascent, &descent, &lineGap);
            this.ascent = ascent;
            this.descent = descent;
            this.lineGap = lineGap;

            int vWidth, vHeight, vX, vY;
            context.GetViewport(out vX, out vY, out vWidth, out vHeight);
            this.PixelsPerUnitWidth = vWidth;
            this.PixelsPerUnitHeight = vHeight;
        }

        public BitmapFont(string path, GLContext glContext = null, int cacheSize = 1024, int textures = 2, uint textureSizes = 512) : this(File.ReadAllBytes(path), glContext, cacheSize, textures, textureSizes) {
        }

        public ITexture GetTextureBacking(int index) {
            return textures[index];
        }

        /// <summary>
        /// Prepares the characters for rendering. All characters to be rendered in one frame should be loaded using this method.
        /// This will also ensure that the bitmap font is big enough.
        /// Calling this multiple times per frame is not good for performance as multiple texture uploads are occurring.
        /// </summary>
        /// <param name="characters">An array of characters to load. Won't load if already loaded.</param>
        public void PrepareCharacterGroup(params char[] characters) {
            int textureChanges = 0;
            foreach (char c in characters)
            {
                if (!glyphTexLocations.ContainsKey((c, scale)) || !textures[glyphTexLocations[(c, scale)]].ContainsChar(c, scale)) {
                    glyphTexLocations.Remove((c, scale));
                    if (!textures[drawingTo].Upload(scale, c)) {
                        drawingTo++;
                        if (drawingTo >= textures.Length) drawingTo = 0;
                        FontTexture fontTexture = textures[drawingTo];
                        fontTexture.Clear();
                        fontTexture.Upload(scale, c);
                        textureChanges++;
                        if (textureChanges > textures.Length) throw new FrameworkUsageException(string.Format("Character group \"{0}\" takes up too much texture space! Consider increasing decreasing font size, or increasing texture lengths, or number of backing textures.", new string(characters)));
                    }
                    glyphTexLocations.Add((c, scale), drawingTo);
                    int glyphIndex = glyphIndices.ComputeIfNonExistent(c, (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
                    metricsCache.ComputeIfNonExistent(c, (p) => new CharacterMetrics(glyphIndex, info));
                }
            }
        }

        public unsafe void WriteLine(MeshBatch batch, float x, float y, string text, Color color) {
            float currentPoint = x;
            float baseLine = y;
            char[] chars = text.ToCharArray();
            for (int i = 0; i < chars.Length; i++) {
                char c = chars[i];
                if (c == ' ') {
                    currentPoint += (scale * spaceAdvance) / PixelsPerUnitWidth;
                } else if (c == '\n') {
                    currentPoint = x;
                    baseLine -= ((ascent - descent + lineGap) * scale) / PixelsPerUnitHeight;
                } else {
                    int glyphIndex = glyphIndices.ComputeIfNonExistent(c, (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
                    CharacterMetrics metrics = metricsCache.ComputeIfNonExistent(c, (p) => new CharacterMetrics(glyphIndex, info));
                    //Check if glyph is loaded, if not, throw exception.
                    if (!glyphTexLocations.ContainsKey((c, scale)) || !textures[glyphTexLocations[(c, scale)]].ContainsChar(c, scale)) {
                        throw new FrameworkUsageException(string.Format("Character \'{0}\' was not prepared and is missing!", c));
                    }

                    FontTexture texture = textures[glyphTexLocations[(c, scale)]];
                    RectangleF texBounds = texture.GetGlyphBoundaries(c, scale);
                    RectangleF transformedTexBounds = texBounds.MultiplyBy(1f/texture.Length);
                    transformedTexBounds.Y += transformedTexBounds.Height;
                    transformedTexBounds.Height *= -1;
                    RectangleF glyphBounds = new RectangleF(currentPoint, baseLine, texBounds.Width / PixelsPerUnitWidth, texBounds.Height / PixelsPerUnitHeight);
                    glyphBounds.X += (metrics.leftSideBearing * scale) / PixelsPerUnitWidth;
                    glyphBounds.Y += (metrics.vertOffset * scale) / PixelsPerUnitHeight;
                    
                    batch.Draw(new RectangleMesh(glyphBounds, transformedTexBounds, texture, color));
                    currentPoint += (metrics.advanceWidth * scale) / PixelsPerUnitWidth;
                    if (i + 1 < chars.Length) {
                        int nextGlyphIndex = glyphIndices.ComputeIfNonExistent(chars[i + 1], (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
                        currentPoint += (kerningCache.ComputeIfNonExistent((glyphIndex, nextGlyphIndex), (gIndices) => StbTrueType.stbtt_GetGlyphKernAdvance(info, gIndices.Item1, gIndices.Item2)) * scale) / PixelsPerUnitWidth;
                    }
                }
            }
        }

        private struct CharacterMetrics {
            public readonly int vertOffset;
            public readonly int leftSideBearing;
            public readonly int advanceWidth;

            public unsafe CharacterMetrics(int glyphIndex, StbTrueType.stbtt_fontinfo info) {
                int x0, y0, leftSideBearing, advanceWidth;
                StbTrueType.stbtt_GetGlyphHMetrics(info, glyphIndex, &advanceWidth, &leftSideBearing);
                StbTrueType.stbtt_GetGlyphBox(info, glyphIndex, &x0, &y0, null, null);
                this.vertOffset = y0;
                this.leftSideBearing = leftSideBearing;
                this.advanceWidth = advanceWidth;
            }
        }

        private class FontTexture : ITexture {
            private LRUCache<char, int> glyphIndices;
            public uint Length {get; private set;}
            StbTrueType.stbtt_fontinfo info;
            GLContext context;
            private bool disposed;

            public bool SingleChanneled => true;

            public uint Handle {get; private set;}

            public uint Width => Length;

            public uint Height => Length;

            public bool Flipped => false;
            private Dictionary<(char, float), Rectangle> glyphBounds = new Dictionary<(char, float), Rectangle>();
            private int row, column, previousRow;

            public FontTexture(StbTrueType.stbtt_fontinfo info, GLContext context, LRUCache<char, int> glyphIndices, uint length) {
                this.context = context;
                this.Length = length;
                this.info = info;
                this.glyphIndices = glyphIndices;
                context.DetectGLError();
                uint[] handles = new uint[1];
                context.GenTextures(handles.Length, handles);
                Handle = handles[0];

                context.BindTexture(TextureTarget.Texture2D, Handle);
                context.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Red, length, length, 0, PixelFormat.Red, PixelType.UnsignedByte, IntPtr.Zero);
                context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
                context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
                context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
                context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);

            }

            public bool ContainsChar(char c, float scale) {
                return glyphBounds.ContainsKey((c, scale));
            }

            public Rectangle GetGlyphBoundaries(char c, float scale) {
                return glyphBounds[(c, scale)];
            }

            public unsafe bool Upload(float scale, char c) {
                int x0, y0, x1, y1;
                int glyphIndex = glyphIndices.ComputeIfNonExistent(c, (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
                StbTrueType.stbtt_GetGlyphBitmapBox(info, glyphIndex, scale, scale, &x0, &y0, &x1, &y1);
                int width = x1 - x0, height = y1 - y0;
                if (width == 0 || height == 0) throw new InternalFrameworkException("Glyph width or height was 0. Character was: " + c);
                if (column + width >= Length) {
                    column = 0;
                    if (row + height + 1 >= Length) {
                        return false;
                    } else {
                        previousRow = row;
                    }
                }
                if (row - height <= previousRow) {
                    row = previousRow + height + 1;
                }
                column += 1 + width;

                context.BindTexture(TextureTarget.Texture2D, Handle);
                int alignment;
                int[] alignments = new int[1];
                context.GetIntegerv(GetPName.UnpackAlignment, alignments);
                alignment = alignments[0];
                if (alignment != 1) {
                    context.PixelStorei(PixelStoreParameter.UnpackAlignment, 1);
                }
                byte[] bitmap = new byte[width * height];
                fixed(byte* data = &bitmap[0]) {
                    StbTrueType.stbtt_MakeGlyphBitmap(info, data, width, height, width, scale, scale, glyphIndex);
                    context.TexSubImage2D(TextureTarget.Texture2D, 0, column - width, row - height, width, height, PixelFormat.Red, PixelType.UnsignedByte, new IntPtr(data));
                }
                if (alignment != 1) {
                    context.PixelStorei(PixelStoreParameter.UnpackAlignment, alignment);
                }

                glyphBounds.Add((c, scale), new Rectangle(column - width, row - height, width, height));
                return true;
            }

            public bool CanFit(int width, int height) {
                if (row + height < Length) {
                    return true;
                } else if (column + width < Length) {
                    return true;
                }
                return false;
            }

            public void Clear() {
                glyphBounds.Clear();
                row = 0;
                column = 0;
                previousRow = 0;
            }

            protected virtual void Dispose(bool disposing)
            {
                if (!disposed)
                {
                    if (disposing)
                    {
                    }
                    context.DeleteTextures(1, new uint[] {Handle});
                    disposed = true;
                }
            }

            ~FontTexture()
            {
                Dispose(disposing: false);
            }

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

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects)
                }
                foreach (FontTexture texture in this.textures)
                {
                    texture.Dispose();
                }
                disposed = true;
            }
        }

        ~BitmapFont()
        {
            Dispose(disposing: false);
        }

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