using System.Numerics;

using FreeTypeSharp;
using static FreeTypeSharp.FT;
using static FreeTypeSharp.FT_LOAD;

namespace Voile;

public struct Glyph
{
    public int TextureId { get; set; } = -1;
    public Vector2 Offset;

    public float Width { get; set; }
    public float Height { get; set; }
    public Vector2 Bearing { get; set; }
    /// <summary>
    /// Glyph's advance in pixels.
    /// </summary>
    public int Advance { get; set; }

    public Glyph() { }
}

/// <summary>
/// Represents font data.
/// </summary>
public class Font : Resource, IUpdatableResource, IDisposable
{
    /// <summary>
    /// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
    /// </summary>
    internal int Handle { get; set; } = -1;
    public int Size { get; set; } = 24;

    public float LetterSpacing { get; set; } = 0f;
    public float SpacingScale { get; set; } = 0.8f; // TODO: this is a super temporary fix for character spacing. Should be fixed once custom font rendering will be implemented.

    public byte[]? Buffer { get; private set; }
    public long BufferSize { get; set; }

    public bool Dirty => _dirty;

    internal float UnitsPerEm;

    internal nint FacePtr;
    internal nint LibraryPtr;

    internal List<int> Codepoints => _glyphs.Keys.ToList();

    public Font(string path, byte[] buffer) : base(path)
    {
        Buffer = buffer;
    }

    public void Dispose()
    {
        unsafe
        {
            if (FacePtr != IntPtr.Zero)
                FT_Done_Face((FT_FaceRec_*)FacePtr);

            if (LibraryPtr != IntPtr.Zero)
                FT_Done_FreeType((FT_LibraryRec_*)LibraryPtr);
        }
    }

    /// <summary>
    /// Loads a basic ASCII charset for this font.
    /// </summary>
    public void LoadAsciiData()
    {
        for (char c = ' '; c < 127; c++)
        {
            GetGlyph(c);
        }
    }

    /// <summary>
    /// Measures a given string using the font metrics.
    /// </summary>
    /// <param name="text">Text to measure.</param>
    /// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
    public Rect Measure(string text)
    {
        if (string.IsNullOrEmpty(text))
            return Rect.Zero;

        float totalWidth = 0;
        float maxAscent = 0;
        float maxDescent = 0;

        for (int i = 0; i < text.Length; i++)
        {
            char c = text[i];
            Glyph glyph = GetGlyph(c);

            totalWidth += glyph.Advance * SpacingScale + LetterSpacing;

            float ascent = glyph.Bearing.Y;
            float descent = glyph.Height - glyph.Bearing.Y;

            if (ascent > maxAscent)
                maxAscent = ascent;
            if (descent > maxDescent)
                maxDescent = descent;

            if (i > 0)
            {
                char prevChar = text[i - 1];
                totalWidth += GetKerning(prevChar, c);
            }
        }

        float totalHeight = Size;
        return new Rect(totalWidth, totalHeight);
    }

    public int GetKerning(char left, char right)
    {
        unsafe
        {
            var face = (FT_FaceRec_*)FacePtr;

            if (face == null)
                return 0;

            uint leftIndex = FT_Get_Char_Index(face, left);
            uint rightIndex = FT_Get_Char_Index(face, right);

            if (leftIndex == 0 || rightIndex == 0)
                return 0;

            FT_Vector_ kerning;
            if (FT_Get_Kerning(face, leftIndex, rightIndex, FT_Kerning_Mode_.FT_KERNING_DEFAULT, &kerning) != 0)
                return 0;

            return (int)(kerning.x >> 6);
        }
    }

    public Glyph GetGlyph(char character)
    {
        if (!HasGlyph(character))
        {
            _glyphs.TryGetValue('?', out var defGlyph);
            return defGlyph;
        }
        if (_glyphs.TryGetValue(character, out var glyph))
            return glyph;

        var loaded = LoadGlyph(character);
        _glyphs[character] = loaded;

        _dirty = true;

        return loaded;
    }

    public bool HasGlyph(char character)
    {
        unsafe
        {
            var face = (FT_FaceRec_*)FacePtr;

            if (FacePtr == IntPtr.Zero)
                return false;

            return FT_Get_Char_Index(face, character) != 0;
        }
    }

    internal unsafe void InitializeFontSize()
    {
        var face = (FT_FaceRec_*)FacePtr;

        if (face == null)
            throw new Exception("Font face not initialized.");

        FT_Set_Pixel_Sizes(face, 0, (uint)Size);
    }

    private unsafe Glyph LoadGlyph(char character)
    {
        var face = (FT_FaceRec_*)FacePtr;

        var error = FT_Load_Char(face, character, FT_LOAD_RENDER);

        if (error != 0)
            throw new Exception($"Failed to load glyph for '{character}'");

        var g = face->glyph;
        var bitmap = g->bitmap;
        var metrics = g->metrics;

        return new Glyph
        {
            Width = bitmap.width,
            Height = bitmap.rows,

            Bearing = new Vector2(
                metrics.horiBearingX >> 6,
                metrics.horiBearingY >> 6
            ),

            Advance = (int)(g->advance.x >> 6)
        };
    }

    public void MarkUpdated()
    {
        _dirty = false;
    }

    private bool _dirty;
    private Dictionary<int, Glyph> _glyphs = new();
}