using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Quik.Stb
{
    public unsafe class StbFont : IDisposable
    {
        IntPtr _buffer;
        stbtt_fontinfo* _info;
        List<stbtt_kerningentry> _kerningTable;

        public IntPtr FontBuffer => _buffer;
        public ref stbtt_fontinfo FontInfo => ref *_info;

        public IReadOnlyList<stbtt_kerningentry> KerningTable
        {
            get
            {
                if (_kerningTable != null)
                    return _kerningTable;

                int count = Stbtt.GetKerningTableLength(_info);

                if (count == 0)
                {
                    return _kerningTable = new List<stbtt_kerningentry>();
                }
                else
                {
                    stbtt_kerningentry[] array = new stbtt_kerningentry[count];

                    fixed (stbtt_kerningentry *ptr = array)
                        Stbtt.GetKerningTable(_info, ptr, count);

                    return _kerningTable = new List<stbtt_kerningentry>(array);
                }
            }
        }

        public int Ascend { get; }
        public int Descend { get; }
        public int VerticalLineGap { get; }
        public int AscendOS2 { get; }
        public int DescendOS2 { get; }
        public int VerticalLineGapOS2 { get; }
        public Box BoundingBox { get; }

        private StbFont(IntPtr buffer, stbtt_fontinfo* info)
        {
            _buffer = buffer;
            _info = info;

            int a, b, c, d;

            Stbtt.GetFontVMetrics(_info, &a, &b, &c);
            Ascend = a;
            Descend = b;
            VerticalLineGap = c;

            Stbtt.GetFontVMetricsOS2(_info, &a, &b, &c);
            AscendOS2 = a;
            DescendOS2 = b;
            VerticalLineGapOS2 = c;

            Stbtt.GetFontBoundingBox(_info, &a, &b, &c, &d);
            BoundingBox = new Box(a, b, c, d);
        }
        ~StbFont()
        {
            Dispose(false);
        }


        public int FindGlyphIndex(int codepoint)
        {
            return Stbtt.FindGlyphIndex(_info, codepoint);
        }
        public int FindGlyphIndex(Rune codepoint) => FindGlyphIndex(codepoint.Value);

        public float ScaleForPixelHeight(float pixels)
        {
            return Stbtt.ScaleForPixelHeight(_info, pixels);
        }

        public float ScaleForMappingEmToPixels(float pixels)
        {
            return Stbtt.ScaleForMappingEmToPixels(_info, pixels);
        }

        public void GetCodepointHMetrics(int codepoint, out int advance, out int bearing)
        {
            int a, b;

            Stbtt.GetCodepointHMetrics(_info, codepoint, &a, &b);

            advance = a;
            bearing = b;
        }

        public void GetCodepointHMetrics(Rune codepoint, out int advance, out int bearing)
            => GetCodepointHMetrics(codepoint.Value, out advance, out bearing);

        public int GetCodepointKernAdvance(int cp1, int cp2)
        {
            return Stbtt.GetCodepointKernAdvance(_info, cp1, cp2);
        }
        public int GetCodepointKernAdvance(Rune cp1, Rune cp2) => GetCodepointKernAdvance(cp1.Value, cp2.Value);

        public int GetCodepointBox(int codepoint, out Box box)
        {
            int x0, y0;
            int x1, y1;
            int rval;

            rval = Stbtt.GetCodepointBox(_info, codepoint, &x0, &y0, &x1, &y1);

            box = new Box(x0, y0, x1, y1);

            return rval;
        }

        public void GetGlyphHMetrics(int glyph, out int advance, out int bearing)
        {
            int a, b;

            Stbtt.GetGlyphHMetrics(_info, glyph, &a, &b);

            advance = a;
            bearing = b;
        }

        public int GetGlyphKernAdvance(int gl1, int gl2)
        {
            return Stbtt.GetGlyphKernAdvance(_info, gl1, gl2);
        }

        public int GetGlyphBox(int glyph, out Box box)
        {
            int x0, y0;
            int x1, y1;
            int rval;

            rval = Stbtt.GetGlyphBox(_info, glyph, &x0, &y0, &x1, &y1);

            box = new Box(x0, y0, x1, y1);

            return rval;
        }

        public bool IsGlyphEmpty(int glyph)
        {
            return Stbtt.IsGlyphEmpty(_info, glyph) != 0;
        }

        public Bitmap GetCodepointBitmap(float scaleX, float scaleY, int codepoint, out int offsetX, out int offsetY)
        {
            int w, h, x, y;
            void* ptr = Stbtt.GetCodepointBitmap(_info, scaleX, scaleY, codepoint, &w, &h, &x, &y);

            offsetX = x;
            offsetY = y;

            return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
        }

        public Bitmap GetCodepointBitmap(float scaleX, float scaleY, Rune codepoint, out int offsetX, out int offsetY)
            => GetCodepointBitmap(scaleX, scaleY, codepoint.Value, out offsetX, out offsetY);

        public Bitmap GetCodepointBitmapSubpixel(float scaleX, float scaleY, float shiftX, float shiftY, int codepoint, out int offsetX, out int offsetY)
        {
            int w, h, x, y;
            void* ptr = Stbtt.GetCodepointBitmapSubpixel(_info, scaleX, scaleY, shiftX, shiftY, codepoint, &w, &h, &x, &y);

            offsetX = x;
            offsetY = y;

            return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
        }

        public Bitmap GetCodepointBitmapSubpixel(float scaleX, float scaleY, float shiftX, float shiftY, Rune codepoint, out int offsetX, out int offsetY)
            => GetCodepointBitmapSubpixel(scaleX, scaleY, shiftX, shiftY, codepoint.Value, out offsetX, out offsetY);

        public Bitmap GetGlyphBitmap(float scaleX, float scaleY, int glyph, out int offsetX, out int offsetY)
        {
            int w, h, x, y;
            void* ptr = Stbtt.GetGlyphBitmap(_info, scaleX, scaleY, glyph, &w, &h, &x, &y);

            offsetX = x;
            offsetY = y;

            return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
        }

        public Bitmap GetGlyphBitmapSubpixel(float scaleX, float scaleY, float shiftX, float shiftY, int glyph, out int offsetX, out int offsetY)
        {
            int w, h, x, y;
            void* ptr = Stbtt.GetGlyphBitmapSubpixel(_info, scaleX, scaleY, shiftX, shiftY, glyph, &w, &h, &x, &y);

            offsetX = x;
            offsetY = y;

            return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
        }

        public Bitmap GetGlyphSdf(float scale, int glyph, int padding, byte edgeValue, float pixelDistScale, out int offsetX, out int offsetY)
        {
            int w, h, x, y;
            void *ptr = Stbtt.GetGlyphSDF(_info, scale, glyph, padding, edgeValue, pixelDistScale, &w, &h, &x, &y);

            offsetX = x;
            offsetY = y;

            return new Bitmap((IntPtr)ptr, w, h, FreeSdf);
        }

        public Bitmap GetCodepointSdf(float scale, int codepoint, int padding, byte edgeValue, float pixelDistScale, out int offsetX, out int offsetY)
        {
            int w, h, x, y;
            void *ptr = Stbtt.GetCodepointSDF(_info, scale, codepoint, padding, edgeValue, pixelDistScale, &w, &h, &x, &y);

            offsetX = x;
            offsetY = y;

            return new Bitmap((IntPtr)ptr, w, h, FreeSdf);
        }

        public Bitmap GetCodepointSdf(float scale, Rune codepoint, int padding, byte edgeValue, float pixelDistScale, out int offsetX, out int offsetY)
            => GetCodepointSdf(scale, codepoint.Value, padding, edgeValue, pixelDistScale, out offsetX, out offsetY);

        public void Dispose()
        {
            Dispose(true);
        }

        bool isDisposed = false;
        private void Dispose(bool disposing)
        {
            if (isDisposed) return;

            if (disposing)
            {
                GC.SuppressFinalize(this);
            }


            Marshal.FreeHGlobal(_buffer);
            Marshal.FreeHGlobal((IntPtr)_info);
            isDisposed = true;
        }

        public static bool TryLoad(Stream stream, out StbFont font)
        {
            byte* buffer = (byte*)Marshal.AllocHGlobal((int)stream.Length);
            stbtt_fontinfo* fontInfo = (stbtt_fontinfo*)Marshal.AllocHGlobal(sizeof(stbtt_fontinfo));

            stream.Read(new Span<byte>(buffer, (int)stream.Length));

            int nfont = Stbtt.GetNumberOfFonts(buffer);

            if (nfont == 0)
            {
                font = null;
                return false;
            }

            int offset = Stbtt.GetFontOffsetForIndex(buffer, 0);

            if (Stbtt.InitFont(fontInfo, (byte*)buffer, offset) == 0)
            {
                Marshal.FreeHGlobal((IntPtr)buffer);
                Marshal.FreeHGlobal((IntPtr)fontInfo);

                font = null;
                return false;
            }

            font = new StbFont((IntPtr)buffer, fontInfo);
            return true;
        }

        public static StbFont Load(Stream stream)
        {
            if (TryLoad(stream, out StbFont font))
            {
                return font;
            }

            throw new Exception("Could not load the font.");
        }

        private static void FreeBitmap(IntPtr buffer)
        {
            Stbtt.FreeBitmap((byte*)buffer, null);
        }

        private static void FreeSdf(IntPtr buffer)
        {
            Stbtt.FreeSDF((byte*)buffer, null);
        }

        public struct Box
        {
            public int X0;
            public int Y0;
            public int X1;
            public int Y1;

            public Box(int x0, int y0, int x1, int y1)
            {
                X0 = x0; Y0 = y0;
                X1 = x1; Y1 = y1;
            }
        }

        public class Bitmap : IDisposable
        {
            public IntPtr Buffer { get; }
            public int Width { get; }
            public int Height { get; }

            private readonly Action<IntPtr> Destroy;

            public Bitmap(IntPtr buffer, int width, int height, Action<IntPtr> destroy)
            {
                Buffer = buffer;
                Width = width;
                Height = height;
                Destroy = destroy;
            }
            ~Bitmap()
            {
                Dispose(false);
            }

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

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

                if (disposing)
                {
                    GC.SuppressFinalize(this);
                }

                Destroy(Buffer);
                isDiposed = true;
            }
        }
    }
}