//----------------------------------------------------------------------------------------------------------------------
// QB64-PE Font Library
// Powered by FreeType (https://freetype.org/)
//----------------------------------------------------------------------------------------------------------------------

#include "font.h"
#include "../../../libqb.h"
#include "error_handle.h"
#include "graphics.h"
#include "gui.h"
#include "image.h"
#include "libqb-common.h"
#include "mutex.h"
#include "rounding.h"
#include <codecvt>
#include <cstdio>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <locale>
#include <string>
#include <unordered_map>
#include <vector>

// Note: QB64 expects invalid font handles to be zero
#define IS_VALID_FONT_HANDLE(_h_) ((_h_) > INVALID_FONT_HANDLE && (_h_) < fontManager.fonts.size() && fontManager.fonts[_h_]->isUsed)
#define IS_VALID_QB64_FONT_HANDLE(_h_) ((_h_) <= lastfont && ((fontwidth[_h_] && fontheight[_h_]) || ((_h_) >= 32 && font[_h_])))
#define IS_VALID_UTF_ENCODING(_e_) ((_e_) == 0 || (_e_) == 8 || (_e_) == 16 || (_e_) == 32)

// These are from libqb.cpp
extern const img_struct *write_page;
extern const int32_t *font;
extern const int32_t *fontwidth;
extern const int32_t *fontheight;
extern const int32_t *fontflags;
extern const int32_t lastfont;
extern const uint8_t charset8x8[256][8][8];
extern const uint8_t charset8x16[256][16][8];

void pset_and_clip(int32_t x, int32_t y, uint32_t col);

/// @brief A simple class that manages conversions from various encodings to UTF-32.
/// Note: This class uses the deprecated codecvt library from C++17.
/// We will need to replace this with a better implementation in the future when we adopt C++26 or later.
class UTF32 {
    static constexpr uint32_t MAX_UNICODE_CODEPOINT = 0x10FFFFu;

    // Internal reusable UTF-32 buffer
    std::u32string string;
    // Reused converters
    std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convUTF8;
    std::wstring_convert<std::codecvt_utf16<char32_t, MAX_UNICODE_CODEPOINT,
                                            static_cast<std::codecvt_mode>(std::codecvt_mode::consume_header | std::codecvt_mode::little_endian)>,
                         char32_t>
        convUTF16LEBOM;
    std::wstring_convert<std::codecvt_utf16<char32_t, MAX_UNICODE_CODEPOINT, std::codecvt_mode::consume_header>, char32_t> convUTF16BEBOM;
    std::wstring_convert<std::codecvt_utf16<char32_t, MAX_UNICODE_CODEPOINT, std::codecvt_mode::little_endian>, char32_t> convUTF16LENoBOM;

  public:
    UTF32() = default;
    UTF32(const UTF32 &) = delete;
    UTF32 &operator=(const UTF32 &) = delete;
    UTF32(UTF32 &&) noexcept = default;
    UTF32 &operator=(UTF32 &&) noexcept = default;

    /// @brief Converts a code page 437 byte string to UTF-32.
    /// @param str The code page 437 string.
    /// @param len The size of the string in bytes.
    /// @return Number of code points on success; 0 on failure.
    [[nodiscard]] size_t ConvertCP437(const uint8_t *str, size_t len) noexcept {
        string.resize(len);

        for (size_t i = 0; i < len; i++) {
            string[i] = codepage437_to_unicode16[str[i]]; // codepage437_to_unicode16 is from libqb
        }

        return string.size();
    }

    /// @brief Converts UTF-8 byte string to UTF-32.
    /// @param str The UTF-8 string.
    /// @param len The size of the string in bytes.
    /// @return Number of code points on success; 0 on failure.
    [[nodiscard]] size_t ConvertUTF8(const uint8_t *str, size_t len) {
        try {
            string = convUTF8.from_bytes(reinterpret_cast<const char *>(str), reinterpret_cast<const char *>(str + len));
        } catch (...) {
            string.clear();
        }

        return string.size();
    }

    /// @brief Converts UTF-16 (LE/BE) byte string to UTF-32.
    /// @param str The UTF-16 string. If BOM is present, honors it; otherwise assumes little-endian without BOM.
    /// @param len The size of the string in bytes.
    /// @return Number of code points on success; 0 on failure.
    [[nodiscard]] size_t ConvertUTF16(const uint8_t *str, size_t len) {
        try {
            bool hasLEBOM, hasBEBOM;
            if (len >= 2) {
                hasLEBOM = (str[0] == 0xFF && str[1] == 0xFE);
                hasBEBOM = (str[0] == 0xFE && str[1] == 0xFF);
            } else {
                hasLEBOM = false;
                hasBEBOM = false;
            }

            if (hasLEBOM) {
                // Little-endian with BOM. Use consume_header.
                string = convUTF16LEBOM.from_bytes(reinterpret_cast<const char *>(str), reinterpret_cast<const char *>(str + len));
            } else if (hasBEBOM) {
                // Big-endian with BOM. Use consume_header.
                string = convUTF16BEBOM.from_bytes(reinterpret_cast<const char *>(str), reinterpret_cast<const char *>(str + len));
            } else {
                // No BOM. Assume little-endian. Do not use consume_header.
                string = convUTF16LENoBOM.from_bytes(reinterpret_cast<const char *>(str), reinterpret_cast<const char *>(str + len));
            }
        } catch (...) {
            string.clear();
        }

        return string.size();
    }

    /// @brief Returns the converted UTF-32 string.
    [[nodiscard]] const std::u32string &GetString() const noexcept {
        return string;
    }
};

/// @brief This class manages all font handles, bitmaps, hashmaps of glyph bitmaps etc.
struct FontManager {
    FT_Library library;       // FreeType library object
    int32_t lowestFreeHandle; // the lowest free handle that can be allocated
    int32_t reservedHandle;   // this is set to handle 0 so that it is not returned to QB64

    /// @brief Manages a single font
    struct Font {
        bool isUsed;           // is this handle in use?
        uint8_t *fontData;     // raw font data (we always store a copy as long as the font is in use)
        FT_Face face;          // FreeType face object
        FT_Pos monospaceWidth; // the monospace width (if font was loaded as monospace, else zero)
        FT_Pos defaultHeight;  // default (max) pixel height the user wants
        FT_Pos baseline;       // font baseline in pixels
        int32_t options;       // fonts options that were passed by QB64 while loading the font

        /// @brief Manages a single glyph in a font
        struct Glyph {
            // Usually the bitmap size & metrics returned by FT for mono and gray can be the same
            // But it's a bad idea to assume that is the case every time
            struct Bitmap {
                uint8_t *data;       // pointer to the raw pixels
                FT_Vector size;      // bitmap width & height in pixels
                FT_Pos advanceWidth; // glyph advance width in pixels
                FT_Vector bearing;   // glyph left and top side bearing in pixels
            };

            FT_UInt index;  // glyph index
            Bitmap bmpMono; // monochrome bitmap in 8-bit format
            Bitmap bmpGray; // anti-aliased bitmap in 8-bit format
            Bitmap *bitmap; // pointer to the currently selected bitmap (mono / gray)

            // Delete copy and move constructors and assignments
            Glyph(const Glyph &) = delete;
            Glyph &operator=(const Glyph &) = delete;
            Glyph(Glyph &&) = delete;
            Glyph &operator=(Glyph &&) = delete;

            /// @brief Just initializes everything
            Glyph() {
                index = 0;
                bmpMono = {};
                bmpGray = {};
                bitmap = nullptr;
            }

            /// @brief Frees any cached glyph bitmap
            ~Glyph() {
                // image_log_trace("Freeing bitmaps %p, %p", bmpMono.data, bmpGray.data);

                free(bmpGray.data);
                free(bmpMono.data);
            }

            /// @brief Assuming a glyph was previously loaded and rendered by FreeType, this will prepare an internal bitmap struct
            /// @param bmp A pointer to a bitmap struct to prepare
            /// @param parentFont The parent font object
            /// @return True if successful, false otherwise
            bool PrepareBitmap(Bitmap *bmp, Font *parentFont) {
                // IMAGE_DEBUG_CHECK(bmp && !bmp->data);

                // First get all needed glyph metrics
                bmp->size.x = parentFont->face->glyph->bitmap.width;         // get the width of the bitmap
                bmp->size.y = parentFont->face->glyph->bitmap.rows;          // get the height of the bitmap
                bmp->advanceWidth = parentFont->face->glyph->advance.x >> 6; // get the advance width of the glyph
                bmp->bearing.x = parentFont->face->glyph->bitmap_left;       // get the bitmap left side bearing
                bmp->bearing.y = parentFont->face->glyph->bitmap_top;        // get the bitmap top side bearing

                // Check if the glyph has a valid bitmap
                if (!parentFont->face->glyph->bitmap.buffer || bmp->size.x < 1 || bmp->size.y < 1 ||
                    (parentFont->face->glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO && parentFont->face->glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)) {
                    // Ok, this means the font does not have a glyph for the codepoint index
                    // Simply make a blank bitmap and update width and height
                    // image_log_trace("Entering missing glyph path");

                    bmp->size.x = std::max(bmp->advanceWidth, bmp->size.x);
                    if (bmp->size.x < 1) {
                        image_log_error("Failed to get default width for empty glyph");
                        *bmp = {};
                        return false; // something seriously went wrong
                    }
                    bmp->size.y = parentFont->defaultHeight;

                    // image_log_trace("Creating empty (%i x %i) bitmap for missing glyph", bmp->size.x, bmp->size.y);

                    // Allocate zeroed memory for monochrome bitmap
                    bmp->data = (uint8_t *)calloc(bmp->size.x, bmp->size.y);
                    if (!bmp->data) {
                        image_log_error("Failed to allocate memory for empty glyph bitmap");
                        *bmp = {};
                        return false; // memory allocation failed
                    }
                } else {
                    // The bitmap rendered successfully
                    // image_log_trace("(%i x %i) bitmap found", bmp->size.x, bmp->size.y);

                    // So, we have a valid glyph bitmap. We'll use that
                    // Allocate zeroed memory for the bitmap
                    bmp->data = (uint8_t *)calloc(bmp->size.x, bmp->size.y);
                    if (!bmp->data) {
                        image_log_error("Failed to allocate memory for glyph bitmap");
                        *bmp = {};
                        return false; // memory allocation failed
                    }

                    auto src = parentFont->face->glyph->bitmap.buffer;
                    auto dst = bmp->data;

                    // Copy the bitmap based on the pixel mode
                    if (parentFont->face->glyph->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
                        for (FT_Pos y = 0; y < bmp->size.y; y++, src += parentFont->face->glyph->bitmap.pitch, dst += bmp->size.x) {
                            for (FT_Pos x = 0; x < bmp->size.x; x++) {
                                dst[x] = (((src[x >> 3]) >> (7 - (x & 7))) & 1) * 255; // this looks at each bit and then sets the pixel
                            }
                        }
                    } else if (parentFont->face->glyph->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
                        for (FT_Pos y = 0; y < bmp->size.y; y++, src += parentFont->face->glyph->bitmap.pitch, dst += bmp->size.x) {
                            memcpy(dst, src, bmp->size.x); // simply copy the line
                        }
                    } else {
                        image_log_error("Unknown bitmap pixel mode %i", (int)parentFont->face->glyph->bitmap.pixel_mode); // this should never happen
                        free(bmp->data);
                        *bmp = {};
                        return false;
                    }
                }

                return true;
            }

            /// @brief Caches a glyph bitmap with a given codepoint and this happens only once
            /// @param codepoint A valid UTF-32 codepoint
            /// @param parentFont The parent font object
            /// @return True if successful or if bitmap is already cached
            bool CacheBitmap(char32_t codepoint, Font *parentFont) {
                if (!bitmap) {
                    // Get the glyph index first and store it
                    // Note that this can return a valid glyph index but the index need not have any glyph bitmap
                    index = FT_Get_Char_Index(parentFont->face, codepoint);
                    if (!index) {
                        image_log_error("Got glyph index zero for codepoint %lu", codepoint);
                    }

                    // Load the mono glyph to query details and render
                    if (FT_Load_Glyph(parentFont->face, index, FT_LOAD_TARGET_MONO)) {
                        image_log_error("Failed to load mono glyph for codepoint %lu (%u)", codepoint, index);
                    }

                    // We'll attempt to render the monochrome font first
                    if (FT_Render_Glyph(parentFont->face->glyph, FT_RENDER_MODE_MONO)) {
                        image_log_error("Failed to render mono glyph for codepoint %lu (%u)", codepoint, index);
                    }

                    if (!PrepareBitmap(&bmpMono, parentFont)) {
                        image_log_error("Failed to prepare mono glyph for codepoint %lu (%u)", codepoint, index);
                        return false;
                    }

                    // Load the gray glyph to query details and render
                    if (FT_Load_Char(parentFont->face, codepoint, FT_LOAD_RENDER)) {
                        image_log_error("Failed to load gray glyph for codepoint %lu (%u)", codepoint, index);
                    }

                    // Render the gray bitmap
                    if (FT_Render_Glyph(parentFont->face->glyph, FT_RENDER_MODE_NORMAL)) {
                        image_log_error("Failed to render gray glyph for codepoint %lu (%u)", codepoint, index);
                    }

                    if (!PrepareBitmap(&bmpGray, parentFont)) {
                        image_log_error("Failed to prepare gray glyph for codepoint %lu (%u)", codepoint, index);
                        free(bmpMono.data); // free mono bitmap
                        bmpMono = {};
                        return false;
                    }

                    /*
                    image_log_trace("Bitmap cached (%p, %p) for codepoint %u, index %i", bmpMono.data, bmpGray.data, codepoint, index);
                    image_log_trace("Mono: W = %i, H = %i, AW = %i, BX = %i, BY = %i", bmpMono.size.x, bmpMono.size.y, bmpMono.advanceWidth, bmpMono.bearing.x,
                                    bmpMono.bearing.y);
                    image_log_trace("Gray: W = %i, H = %i, AW = %i, BX = %i, BY = %i", bmpGray.size.x, bmpGray.size.y, bmpGray.advanceWidth, bmpGray.bearing.x,
                                    bmpGray.bearing.y);
                    */

                    bitmap = &bmpGray; // set bitmap to gray bitmap by default
                }

                return bitmap != nullptr;
            }

            /// @brief Renders the glyph bitmap to the target bitmap using "alpha blending"
            /// @param dst The target bitmap to render to
            /// @param dstW The width of the target bitmap
            /// @param dstH The height of the target bitmap
            /// @param dstL The x position on the target bitmap where the rendering should start
            /// @param dstT The y position on the target bitmap where the rendering should start
            /// @return True if successful
            void RenderBitmap(uint8_t *dst, FT_Pos dstW, FT_Pos dstH, FT_Pos dstL, FT_Pos dstT) {
                // IMAGE_DEBUG_CHECK(bitmap && dst);

                auto dstR = dstL + bitmap->size.x; // right of dst + 1 where we will end
                auto dstB = dstT + bitmap->size.y; // bottom of dst + 1 where we will end
                auto alphaSrc = bitmap->data;
                for (FT_Pos dy = dstT; dy < dstB; dy++) {
                    for (FT_Pos dx = dstL; dx < dstR; dx++) {
                        if (dx >= 0 && dx < dstW && dy >= 0 && dy < dstH) { // if we are not clipped
                            auto dstP = (dst + dstW * dy + dx);             // dst pointer
                            if (*alphaSrc > *dstP)                          // blend both alpha and save to dst pointer
                                *dstP = *alphaSrc;
                        }
                        ++alphaSrc;
                    }
                }
            }
        };

        std::unordered_map<char32_t, Glyph *> glyphs; // holds pointers to cached glyph data for codepoints

        // Delete copy and move constructors and assignments
        Font(const Font &) = delete;
        Font &operator=(const Font &) = delete;
        Font(Font &&) = delete;
        Font &operator=(Font &&) = delete;

        /// @brief Initializes all members
        Font() {
            isUsed = false;
            fontData = nullptr;
            face = nullptr;
            monospaceWidth = defaultHeight = baseline = options = 0;
        }

        /// @brief Frees any cached glyph
        ~Font() {
            // Free the FreeType face object
            if (FT_Done_Face(face)) {
                image_log_error("Failed to free FreeType face object (%p)", face);
            } else {
                image_log_trace("FreeType face object freed");
            }

            // Free the buffered font data
            free(fontData);
            image_log_trace("Raw font data buffer freed");

            image_log_trace("Freeing cached glyphs");
            // Free any allocated glyph manager
            // This should also call the glyphs destructor freeing the bitmap data
            for (auto &it : glyphs)
                delete it.second;
        }

        /// @brief Creates a glyph belonging to a codepoint, caches its bitmap + info and adds it to the hash map
        /// @param codepoint A valid UTF-32 codepoint
        /// @param isMono True for mono bitmap and false for gray
        /// @return The glyph pointer if successful or if the glyph is already in the map, nullptr otherwise
        Glyph *GetGlyph(char32_t codepoint, bool isMono) {
            if (glyphs.count(codepoint) == 0) {
                // The glyph is not cached yet
                auto newGlyph = new Glyph;

                if (!newGlyph) {
                    image_log_error("Failed to allocate memory");
                    return nullptr; // failed to allocate memory
                }

                // Cache the glyph info and bitmap
                if (!newGlyph->CacheBitmap(codepoint, this)) {
                    delete newGlyph;
                    image_log_error("Failed to cache glyph data");
                    return nullptr; // failed to cache bitmap
                }

                glyphs[codepoint] = newGlyph;                                        // save the Glyph pointer to the map using the codepoint as key
                newGlyph->bitmap = isMono ? &newGlyph->bmpMono : &newGlyph->bmpGray; // select the correct bitmap

                image_log_trace("Glyph data for codepoint %u successfully cached", codepoint);

                return newGlyph; // return the glyph pointer
            }

            auto glyph = glyphs[codepoint];                             // we already have the glyph cached, so simply return the pointer
            glyph->bitmap = isMono ? &glyph->bmpMono : &glyph->bmpGray; // select the correct bitmap

            return glyph;
        }

        /// @brief This returns the length of a UTF32 codepoint array in pixels
        /// @param codepoint The codepoint array (string)
        /// @param codepoints The number of codepoints in the array
        /// @return The length of the string in pixels
        FT_Pos GetStringPixelWidth(const char32_t *codepoint, size_t codepoints) {
            if (monospaceWidth) // return monospace width simply by multiplying the fixed width by the codepoints
                return monospaceWidth * codepoints;

            FT_Pos width = 0;                       // the calculated width in pixel
            auto hasKerning = FT_HAS_KERNING(face); // set to true if font has kerning info
            Glyph *glyph = nullptr;
            Glyph *previousGlyph = nullptr;
            auto isMonochrome = (write_page->bytes_per_pixel == 1) || ((write_page->bytes_per_pixel == 4) && (write_page->alpha_disabled)) ||
                                (options & FONT_LOAD_DONTBLEND); // monochrome or AA?

            for (size_t i = 0; i < codepoints; i++) {
                auto cp = codepoint[i];

                glyph = GetGlyph(cp, isMonochrome);
                if (glyph) {
                    // Add kerning advance width if kerning table is available
                    if (hasKerning && previousGlyph && glyph) {
                        FT_Vector delta;
                        FT_Get_Kerning(face, previousGlyph->index, glyph->index, FT_KERNING_DEFAULT, &delta);
                        width += delta.x >> 6;
                    }

                    width += glyph->bitmap->advanceWidth; // add advance width
                    previousGlyph = glyph;                // save the current glyph pointer for use later
                }
            }

            // Adjust for the last glyph
            if (glyph) {
                auto adjust = glyph->bitmap->advanceWidth;
                if (adjust < glyph->bitmap->size.x)
                    adjust = glyph->bitmap->size.x;
                if (glyph->bitmap->bearing.x > 0 && (glyph->bitmap->size.x + glyph->bitmap->bearing.x) > adjust)
                    adjust = glyph->bitmap->size.x + glyph->bitmap->bearing.x;
                if (glyph->bitmap->bearing.x < 0)
                    adjust += -glyph->bitmap->bearing.x;
                width = width - glyph->bitmap->advanceWidth + adjust;
            }

            return width;
        }
    };

    std::vector<Font *> fonts; // vector that holds all font objects
    libqb_mutex *m;            // we'll use a mutex to give exclusive access to resources used by multiple threads

    FontManager(const FontManager &) = delete;
    FontManager(FontManager &&) = delete;
    FontManager &operator=(const FontManager &) = delete;
    FontManager &operator=(FontManager &&) = delete;

    /// @brief Initializes important stuff and reserves font handle 0
    FontManager() {
        if (FT_Init_FreeType(&library)) {
            gui_alert("Failed to initialize FreeType!");
            exit(5633);
        }

        image_log_info("FreeType library v%i.%i.%i initialized", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH);

        m = libqb_mutex_new();

        lowestFreeHandle = 0;
        reservedHandle = -1; // we cannot set 0 here since 0 is a valid internal handle

        // Reserve handle 0 so that nothing else can use it
        // We are doing this because QB64 treats handle 0 as invalid
        reservedHandle = CreateHandle();
        IMAGE_DEBUG_CHECK(reservedHandle == 0); // the first handle must return 0
    }

    /// @brief Frees any used resources
    ~FontManager() {
        // Free all font handles here
        for (size_t handle = 0; handle < fonts.size(); handle++) {
            ReleaseHandle(handle); // release the handle first
            delete fonts[handle];  // now free the object created by CreateHandle()
        }

        // Now that all fonts are closed and font objects are freed, clear the vector
        fonts.clear();

        libqb_mutex_free(m);

        if (FT_Done_FreeType(library)) {
            image_log_error("Failed to finalize FreeType!");
        }

        image_log_info("FreeType library finalized");
    }

    /// @brief Creates are recycles a font handle
    /// @return An unused font handle
    int32_t CreateHandle() {
        size_t h, vectorSize = fonts.size(); // save the vector size

        // Scan the vector starting from lowestFreeHandle
        // This will help us quickly allocate a free handle
        for (h = lowestFreeHandle; h < vectorSize; h++) {
            if (!fonts[h]->isUsed) {
                image_log_trace("Recent font handle %i recycled", h);
                break;
            }
        }

        if (h >= vectorSize) {
            // Scan through the entire vector and return a slot that is not being used
            // Ideally this should execute in extremely few (if at all) scenarios
            // Also, this loop should not execute if size is 0
            for (h = 0; h < vectorSize; h++) {
                if (!fonts[h]->isUsed) {
                    image_log_trace("Font handle %i recycled", h);
                    break;
                }
            }
        }

        if (h >= vectorSize) {
            // If we have reached here then either the vector is empty or there are no empty slots
            // Simply create a new handle at the back of the vector
            auto newHandle = new Font; // allocate and initialize

            if (!newHandle)
                return -1; // we cannot return 0 here since 0 is a valid internal handle

            fonts.push_back(newHandle);
            size_t newVectorSize = fonts.size();

            // If newVectorSize == vectorSize then push_back() failed
            if (newVectorSize <= vectorSize) {
                delete newHandle;
                return -1; // we cannot return 0 here since 0 is a valid internal handle
            }

            h = newVectorSize - 1; // the handle is simply newVectorSize - 1

            image_log_trace("Font handle %i created", h);
        }

        // IMAGE_DEBUG_CHECK(fonts[h]->isUsed == false);

        fonts[h]->fontData = nullptr;
        fonts[h]->face = nullptr;
        fonts[h]->monospaceWidth = 0;
        fonts[h]->defaultHeight = 0;
        fonts[h]->baseline = 0;
        fonts[h]->options = 0;
        fonts[h]->isUsed = true;

        lowestFreeHandle = h + 1; // set lowestFreeHandle to allocated handle + 1

        image_log_trace("Font handle %i returned", h);

        return (int32_t)h;
    }

    /// @brief This will mark a handle as free so that it's put up for recycling
    /// @param handle A font handle
    void ReleaseHandle(int32_t handle) {
        if (handle >= 0 && handle < fonts.size() && fonts[handle]->isUsed) {
            // Free the FreeType face object
            if (FT_Done_Face(fonts[handle]->face)) {
                image_log_error("Failed to free FreeType face object (%p)", fonts[handle]->face);
            } else {
                image_log_trace("FreeType face object freed");
            }
            fonts[handle]->face = nullptr;

            // Free the buffered font data
            free(fonts[handle]->fontData);
            fonts[handle]->fontData = nullptr;
            image_log_trace("Raw font data buffer freed");

            image_log_trace("Freeing cached glyphs");
            // Free cached glyph data
            // This should also call the glyphs destructor freeing the bitmap data
            for (auto &it : fonts[handle]->glyphs)
                delete it.second;

            // Reset the hash map
            fonts[handle]->glyphs.clear();
            image_log_trace("Hash map cleared");

            // Now simply set the 'isUsed' member to false so that the handle can be recycled
            fonts[handle]->isUsed = false;

            // Save the free handle to lowestFreeHandle if it is lower than lowestFreeHandle
            if (handle < lowestFreeHandle)
                lowestFreeHandle = handle;

            image_log_trace("Font handle %i marked as free", handle);
        }
    }
};

/// @brief Global font manager object
static FontManager fontManager;

/// @brief Loads a whole font file from disk to memory.
/// This will search for the file in known places if it is not found in the current directory
/// @param file_path_name The font file name. This can be a relative path
/// @param out_bytes The size of the data that was loaded. This cannot be NULL
/// @return A pointer to a buffer with the data. NULL on failure. The caller is responsible for freeing this memory
uint8_t *FontLoadFileToMemory(const char *file_path_name, int32_t *out_bytes) {
    // This is simply a list of known locations to look for a font
    static const char *const FONT_PATHS[][2] = {
#ifdef QB64_WINDOWS
        {"%s/Microsoft/Windows/Fonts/%s", "LOCALAPPDATA"}, {"%s/Fonts/%s", "SystemRoot"}
#elif defined(QB64_MACOSX)
        {"%s/Library/Fonts/%s", "HOME"}, {"%s/Library/Fonts/%s", nullptr}, {"%s/System/Library/Fonts/%s", nullptr}
#elif defined(QB64_LINUX)
        {"%s/.fonts/%s", "HOME"},
        {"%s/.local/share/fonts/%s", "HOME"},
        {"%s/usr/local/share/fonts/%s", nullptr},
        {"%s/usr/share/fonts/%s", nullptr},
        {"%s/usr/share/fonts/opentype/%s", nullptr},
        {"%s/usr/share/fonts/truetype/%s", nullptr}
#endif
    };

    // Attempt to open the file with the current file pathname
    auto fontFile = fopen(file_path_name, "rb");
    if (!fontFile) {
        image_log_trace("Failed to open font file: %s", file_path_name);
        image_log_trace("Attempting to load font file using known paths");

        static const auto PATH_BUFFER_SIZE = 4096;
        auto pathName = (char *)malloc(PATH_BUFFER_SIZE);
        if (!pathName) {
            image_log_error("Failed to allocate working buffer");
            return nullptr;
        }
        image_log_trace("Allocate working buffer");

        // Go over the known locations and see what works
        for (auto i = 0; i < (sizeof(FONT_PATHS) / sizeof(uintptr_t) / 2); i++) {
            memset(pathName, 0, PATH_BUFFER_SIZE);

            if (FONT_PATHS[i][1] && getenv(FONT_PATHS[i][1]))
                std::snprintf(pathName, PATH_BUFFER_SIZE, FONT_PATHS[i][0], getenv(FONT_PATHS[i][1]), file_path_name);
            else
                std::snprintf(pathName, PATH_BUFFER_SIZE, FONT_PATHS[i][0], "", file_path_name);

            image_log_trace("Attempting to load %s", pathName);

            fontFile = fopen(pathName, "rb");
            if (fontFile)
                break; // exit the loop if something worked
        }

        free(pathName);
        image_log_trace("Working buffer freed");

        if (!fontFile) {
            image_log_trace("No know locations worked");
            return nullptr; // return NULL if all attempts failed
        }
    }

    if (fseek(fontFile, 0, SEEK_END) != 0) {
        image_log_error("Failed to seek end of font file: %s", file_path_name);
        fclose(fontFile);
        return nullptr;
    }

    *out_bytes = ftell(fontFile);
    if (*out_bytes < 0) {
        image_log_error("Failed to determine size of font file: %s", file_path_name);
        fclose(fontFile);
        return nullptr;
    }

    if (fseek(fontFile, 0, SEEK_SET) != 0) {
        image_log_error("Failed to seek beginning of font file: %s", file_path_name);
        fclose(fontFile);
        return nullptr;
    }

    auto buffer = (uint8_t *)malloc(*out_bytes);
    if (!buffer) {
        image_log_error("Failed to allocate memory for font file: %s", file_path_name);
        fclose(fontFile);
        return nullptr;
    }

    if (fread(buffer, *out_bytes, 1, fontFile) != 1) {
        image_log_error("Failed to read font file: %s", file_path_name);
        fclose(fontFile);
        free(buffer);
        return nullptr;
    }

    fclose(fontFile);

    image_log_trace("Successfully loaded font file: %s", file_path_name);
    return buffer;
}

/// @brief Loads a FreeType font from memory. The font data is locally copied and is kept alive while in use
/// @param content_original The original font data in memory that is copied
/// @param content_bytes The length of the data in bytes
/// @param default_pixel_height The maximum rendering height of the font
/// @param which_font The font index in a font collection (< 0 means default)
/// @param options [IN/OUT] 16=monospace (all old flags are ignored like it always was since forever)
/// @return A valid font handle (> 0) or 0 on failure
int32_t FontLoad(const uint8_t *content_original, int32_t content_bytes, int32_t default_pixel_height, int32_t which_font, int32_t &options) {
    libqb_mutex_guard lock(fontManager.m);

    // Allocate a font handle
    auto h = fontManager.CreateHandle();
    if (h <= INVALID_FONT_HANDLE)
        return INVALID_FONT_HANDLE;

    // Allocate memory to duplicate content
    // Note: You must not deallocate the memory before calling FT_Done_Face
    fontManager.fonts[h]->fontData = (uint8_t *)malloc(content_bytes);
    // Return invalid handle if memory allocation failed
    if (!fontManager.fonts[h]->fontData) {
        fontManager.ReleaseHandle(h);
        image_log_error("Failed to allocate memory");
        return INVALID_FONT_HANDLE;
    }

    memcpy(fontManager.fonts[h]->fontData, content_original, content_bytes); // duplicate content

    // Adjust font index
    if (which_font < 1)
        which_font = 0;

    // Attempt to initialize the font for use
    if (FT_New_Memory_Face(fontManager.library, fontManager.fonts[h]->fontData, content_bytes, which_font, &fontManager.fonts[h]->face)) {
        fontManager.ReleaseHandle(h); // this will also free the memory allocated above
        image_log_error("FT_New_Memory_Face() failed");
        return INVALID_FONT_HANDLE;
    }

    // Set the font pixel height
    if (FT_Set_Pixel_Sizes(fontManager.fonts[h]->face, 0, default_pixel_height)) {
        fontManager.ReleaseHandle(h); // this will also free the memory allocated above
        image_log_error("FT_Set_Pixel_Sizes() failed");
        return INVALID_FONT_HANDLE;
    }

    fontManager.fonts[h]->defaultHeight = default_pixel_height; // save default pixel height

    // Calculate the baseline using font metrics only if it is scalable
    if (FT_IS_SCALABLE(fontManager.fonts[h]->face))
        fontManager.fonts[h]->baseline = FT_MulDiv(FT_MulFix(fontManager.fonts[h]->face->ascender, fontManager.fonts[h]->face->size->metrics.y_scale),
                                                   default_pixel_height, fontManager.fonts[h]->face->size->metrics.height);

    image_log_trace("Font baseline = %d", fontManager.fonts[h]->baseline);

    image_log_trace("AUTOMONO requested: %s", (options & FONT_LOAD_AUTOMONO) ? "yes" : "no");

    // Check if automatic fixed width font detection was requested
    if ((options & FONT_LOAD_AUTOMONO) && FT_IS_FIXED_WIDTH(fontManager.fonts[h]->face)) {
        image_log_trace("Fixed-width font detected. Setting MONOSPACE flag");
        // Force set monospace flag and pass it upstream if the font is fixed width
        options |= FONT_LOAD_MONOSPACE;
    }

    if (options & FONT_LOAD_MONOSPACE) {
        const FT_ULong testCP = 'W'; // since W is usually the widest

        // Load using monochrome rendering
        if (FT_Load_Char(fontManager.fonts[h]->face, testCP, FT_LOAD_RENDER | FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO)) {
            image_log_warn("FT_Load_Char() (monochrome) failed");
            // Retry using gray-level rendering
            if (FT_Load_Char(fontManager.fonts[h]->face, testCP, FT_LOAD_RENDER)) {
                image_log_warn("FT_Load_Char() (gray) failed");
            }
        }

        if (fontManager.fonts[h]->face->glyph) {
            fontManager.fonts[h]->monospaceWidth =
                std::max<FT_Pos>(fontManager.fonts[h]->face->glyph->advance.x >> 6, fontManager.fonts[h]->face->glyph->bitmap.width); // save the max width

            image_log_trace("Monospace font (width = %li) requested", fontManager.fonts[h]->monospaceWidth);

            // Set the baseline to bitmap_top if the font is not scalable
            if (!FT_IS_SCALABLE(fontManager.fonts[h]->face))
                fontManager.fonts[h]->baseline = fontManager.fonts[h]->face->glyph->bitmap_top; // for bitmap fonts bitmap_top is the same for all glyph bitmaps
        }

        // Clear the monospace flag is we failed to get the monospace width
        if (!fontManager.fonts[h]->monospaceWidth)
            options &= ~FONT_LOAD_MONOSPACE;
    }

    fontManager.fonts[h]->options = options; // save the options for use later

    image_log_trace("Font (height = %i, index = %i) successfully initialized", default_pixel_height, which_font);
    return h;
}

/// @brief Frees the font and any locally cached data
/// @param fh A valid font handle
void FontFree(int32_t fh) {
    libqb_mutex_guard lock(fontManager.m);

    if (IS_VALID_FONT_HANDLE(fh))
        fontManager.ReleaseHandle(fh);
}

/// @brief Returns the font width
/// @param fh A valid font handle
/// @return The width of the font if the font is monospaced or zero otherwise
int32_t FontWidth(int32_t fh) {
    libqb_mutex_guard lock(fontManager.m);

    // Note: We don't check the validity of the font handle here because it is already checked in libqb.
    // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(fh));

    return fontManager.fonts[fh]->monospaceWidth;
}

/// @brief Returns the length of an UTF32 codepoint string in pixels
/// @param fh A valid font
/// @param codepoint The UTF32 codepoint array
/// @param codepoints The number of codepoints
/// @return Length in pixels
int32_t FontPrintWidthUTF32(int32_t fh, const char32_t *codepoint, int32_t codepoints) {
    libqb_mutex_guard lock(fontManager.m);

    if (codepoints > 0) {
        // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(fh));

        // image_log_trace("codepoint = %p, codepoints = %i", codepoint, codepoints);

        // Get the actual width in pixels
        return fontManager.fonts[fh]->GetStringPixelWidth(codepoint, codepoints);
    }

    return 0;
}

/// @brief Returns the length of an ASCII codepoint string in pixels
/// @param fh A valid font
/// @param codepoint The ASCII codepoint array
/// @param codepoints The number of codepoints
/// @return Length in pixels
int32_t FontPrintWidthASCII(int32_t fh, const uint8_t *codepoint, int32_t codepoints) {
    static UTF32 utf32;

    if (codepoints > 0) {
        // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(fh));

        // Attempt to convert the string to UTF32 and get the actual width in pixels
        auto count = utf32.ConvertCP437(codepoint, codepoints);
        return FontPrintWidthUTF32(fh, utf32.GetString().data(), count);
    }

    return 0;
}

/// @brief Master rendering routine (to be called by all other functions). None of the pointer args can be NULL
/// @param fh A valid font handle
/// @param codepoint A pointer to an array of UTF-32 codepoints that needs to be rendered
/// @param codepoints The number of codepoints in the array
/// @param options 1 = monochrome where black is 0 & white is 255 with nothing in between
/// @param out_data A pointer to a pointer to the output pixel data (alpha values)
/// @param out_x A pointer to the output width of the rendered text in pixels
/// @param out_y A pointer to the output height of the rendered text in pixels
/// @return success = 1, failure = 0
bool FontRenderTextUTF32(int32_t fh, const char32_t *codepoint, int32_t codepoints, int32_t options, uint8_t **out_data, int32_t *out_x, int32_t *out_y) {
    libqb_mutex_guard lock(fontManager.m);

    // Note: We don't check the validity of the font handle here because it is already checked in libqb.
    // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(fh));

    auto fnt = fontManager.fonts[fh];

    // Safety
    *out_data = nullptr;
    *out_x = 0;
    *out_y = fnt->defaultHeight;

    if (codepoints <= 0)
        return codepoints == 0; // true if zero, false if -ve

    auto isMonochrome = bool(options & FONT_RENDER_MONOCHROME); // do we need to do monochrome rendering?
    FT_Vector strPixSize = {
        fnt->GetStringPixelWidth(codepoint, codepoints), // get the total buffer width
        fnt->defaultHeight                               // height is always set by the QB64
    };
    auto outBuf = (uint8_t *)calloc(strPixSize.x, strPixSize.y);
    if (!outBuf)
        return false;

    // image_log_trace("Allocated (%lu x %lu) buffer", strPixSize.x, strPixSize.y);

    FT_Pos penX = 0;

    if (fnt->monospaceWidth) {
        for (size_t i = 0; i < codepoints; i++) {
            auto cp = codepoint[i];

            auto glyph = fnt->GetGlyph(cp, isMonochrome);
            if (glyph) {
                glyph->RenderBitmap(outBuf, strPixSize.x, strPixSize.y,
                                    penX + glyph->bitmap->bearing.x + (fnt->monospaceWidth >> 1) - (glyph->bitmap->advanceWidth >> 1),
                                    fnt->baseline - glyph->bitmap->bearing.y);
                penX += fnt->monospaceWidth;
            }
        }
    } else {
        auto hasKerning = FT_HAS_KERNING(fnt->face); // set to true if font has kerning info
        FontManager::Font::Glyph *glyph = nullptr;
        FontManager::Font::Glyph *previousGlyph = nullptr;

        for (size_t i = 0; i < codepoints; i++) {
            auto cp = codepoint[i];

            glyph = fnt->GetGlyph(cp, isMonochrome);
            if (glyph) {
                // Add kerning advance width if kerning table is available
                if (hasKerning && previousGlyph && glyph) {
                    FT_Vector delta;
                    FT_Get_Kerning(fnt->face, previousGlyph->index, glyph->index, FT_KERNING_DEFAULT, &delta);
                    penX += delta.x >> 6;
                }

                glyph->RenderBitmap(outBuf, strPixSize.x, strPixSize.y, penX + glyph->bitmap->bearing.x, fnt->baseline - glyph->bitmap->bearing.y);
                penX += glyph->bitmap->advanceWidth; // add advance width
                previousGlyph = glyph;               // save the current glyph pointer for use later
            }
        }
    }

    // image_log_trace("Buffer width = %li, render width = %li", strPixSize.x, penX);

    *out_data = outBuf;
    *out_x = strPixSize.x;
    *out_y = strPixSize.y;

    return true;
}

/// @brief This will call FontRenderTextUTF32() after converting the ASCII codepoint array to UTF-32. None of the pointer args can be NULL
/// @param fh A valid font handle
/// @param codepoint A pointer to an array of ASCII codepoints that needs to be rendered
/// @param codepoints The number of codepoints in the array
/// @param options 1 = monochrome where black is 0 & white is 255 with nothing in between
/// @param out_data A pointer to a pointer to the output pixel data (alpha values)
/// @param out_x A pointer to the output width of the rendered text in pixels
/// @param out_y A pointer to the output height of the rendered text in pixels
/// @return success = 1, failure = 0
bool FontRenderTextASCII(int32_t fh, const uint8_t *codepoint, int32_t codepoints, int32_t options, uint8_t **out_data, int32_t *out_x, int32_t *out_y) {
    static UTF32 utf32;

    if (codepoints > 0) {
        // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(fh));

        // Attempt to convert the string to UTF32 and forward to FontRenderTextUTF32()
        auto count = utf32.ConvertCP437(codepoint, codepoints);
        return FontRenderTextUTF32(fh, utf32.GetString().data(), count, options, out_data, out_x, out_y);
    }

    return false;
}

/// @brief Return the true font height in pixel
/// @param qb64_fh A QB64 font handle (this can be a builtin font as well)
/// @param passed Optional arguments flag
/// @return The height in pixels
int32_t func__UFontHeight(int32_t qb64_fh, int32_t passed) {
    libqb_mutex_guard lock(fontManager.m);

    if (is_error_pending())
        return 0;

    if (passed) {
        // Check if a valid font handle was passed
        if (!IS_VALID_QB64_FONT_HANDLE(qb64_fh)) {
            error(QB_ERROR_INVALID_HANDLE);
            return 0;
        }
    } else {
        qb64_fh = write_page->font; // else get the current write page font handle
    }

    if (qb64_fh < 32)
        return fontheight[qb64_fh];

    // Note: We don't check the validity of the font handle here because it is already checked in libqb.
    // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(font[qb64_fh]));

    // Else we will return the FreeType font height
    auto fnt = fontManager.fonts[font[qb64_fh]];
    auto face = fnt->face;

    if (FT_IS_SCALABLE(face))
        return FT_MulDiv(((FT_Long)face->ascender - (FT_Long)face->descender), (FT_Long)fnt->defaultHeight, (FT_Long)face->units_per_EM);

    return fnt->defaultHeight;
}

/// @brief Returns the text width in pixels
/// @param text The text to calculate the width for
/// @param utf_encoding The UTF encoding of the text (0 = ASCII, 8 = UTF-8, 16 - UTF-16, 32 = UTF-32)
/// @param qb64_fh A QB64 font handle (this can be a builtin font as well)
/// @param passed Optional arguments flag
/// @return The width in pixels
int32_t func__UPrintWidth(const qbs *text, int32_t utf_encoding, int32_t qb64_fh, int32_t passed) {
    static UTF32 utf32;
    libqb_mutex_guard lock(fontManager.m);

    if (is_error_pending() || !text->len)
        return 0;

    // Check UTF argument
    if (passed & 1) {
        if (!IS_VALID_UTF_ENCODING(utf_encoding)) {
            error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
            return 0;
        }
    } else {
        utf_encoding = 0;
    }

    // Check if a valid font handle was passed
    if (passed & 2) {
        if (!IS_VALID_QB64_FONT_HANDLE(qb64_fh)) {
            error(QB_ERROR_INVALID_HANDLE);
            return 0;
        }
    } else {
        qb64_fh = write_page->font; // else get the current write page font handle
    }

    // Convert the string to UTF-32 if needed
    char32_t const *str32 = nullptr;
    size_t codepoints = 0;

    switch (utf_encoding) {
    case 32: // UTF-32: no conversion needed
        str32 = (char32_t *)text->chr;
        codepoints = text->len / sizeof(char32_t);
        break;

    case 16: // UTF-16: conversion required
        codepoints = utf32.ConvertUTF16(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
        break;

    case 8: // UTF-8: conversion required
        codepoints = utf32.ConvertUTF8(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
        break;

    default: // ASCII: conversion required
        codepoints = utf32.ConvertCP437(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
    }

    if (qb64_fh < 32)
        return (int32_t)(codepoints * fontwidth[qb64_fh]);

    // Note: We don't check the validity of the font handle here because it is already checked in libqb.
    // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(font[qb64_fh]));

    return (int32_t)fontManager.fonts[font[qb64_fh]]->GetStringPixelWidth(str32, codepoints);
}

/// @brief Returns the vertical line spacing in pixels (font height + extra pixels if any)
/// @param qb64_fh A QB64 font handle (this can be a builtin font as well)
/// @param passed Optional arguments flag
/// @return The vertical spacing in pixels
int32_t func__ULineSpacing(int32_t qb64_fh, int32_t passed) {
    libqb_mutex_guard lock(fontManager.m);

    if (is_error_pending())
        return 0;

    if (passed) {
        // Check if a valid font handle was passed
        if (!IS_VALID_QB64_FONT_HANDLE(qb64_fh)) {
            error(QB_ERROR_INVALID_HANDLE);
            return 0;
        }
    } else {
        qb64_fh = write_page->font; // else get the current write page font handle
    }

    if (qb64_fh < 32)
        return fontheight[qb64_fh];

    // Note: We don't check the validity of the font handle here because it is already checked in libqb.
    // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(font[qb64_fh]));

    auto fnt = fontManager.fonts[font[qb64_fh]];
    auto face = fnt->face;

    if (FT_IS_SCALABLE(face))
        return FT_MulDiv((FT_Long)face->height, (FT_Long)fnt->defaultHeight, (FT_Long)face->units_per_EM);

    return fnt->defaultHeight;
}

/// @brief This renders text on an active destination (graphics mode only) using the currently selected color
/// @param start_x The starting x position
/// @param start_y The starting y position
/// @param text The text that needs to be rendered
/// @param max_width [optional] The maximum width of the text (rendering will be clipped beyond width)
/// @param utf_encoding [optional] The UTF encoding of the text (0 = ASCII, 8 = UTF-8, 16 - UTF-16, 32 = UTF-32)
/// @param qb64_fh [optional] A QB64 font handle (this can be a builtin font as well)
/// @param dst_img [optional] A QB64 image handle (zero designates current SCREEN page)
/// @param passed Optional arguments flag
void sub__UPrintString(int32_t start_x, int32_t start_y, const qbs *text, int32_t max_width, int32_t utf_encoding, int32_t qb64_fh, int32_t dst_img,
                       int32_t passed) {
    static UTF32 utf32;
    libqb_mutex_guard lock(fontManager.m);

    if (is_error_pending() || !text->len)
        return;

    // Check max width
    if (passed & 1) {
        if (max_width < 1) {
            return;
        }
    } else {
        max_width = 0;
    }

    // Check UTF argument
    if (passed & 2) {
        if (!IS_VALID_UTF_ENCODING(utf_encoding)) {
            error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
            return;
        }
    } else {
        utf_encoding = 0;
    }

    int32_t old_dst_img;

    if (passed & 8) {
        old_dst_img = func__dest();
        sub__dest(dst_img);
    }

    // Check if we are in text mode and generate an error if we are
    if (write_page->text) {
        error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
        if (passed & 8)
            sub__dest(old_dst_img);
        return;
    }

    // image_log_trace("Graphics mode set. Proceeding...");

    // Check if a valid font handle was passed
    if (passed & 4) {
        if (!IS_VALID_QB64_FONT_HANDLE(qb64_fh)) {
            error(QB_ERROR_INVALID_HANDLE);
            if (passed & 8)
                sub__dest(old_dst_img);
            return;
        }
    } else {
        qb64_fh = write_page->font; // else get the current write page font handle
    }

    // Convert the string to UTF-32 if needed
    char32_t const *str32 = nullptr;
    size_t codepoints = 0;

    switch (utf_encoding) {
    case 32: // UTF-32: no conversion needed
        // image_log_trace("UTF-32 string. Skipping conversion");
        str32 = (char32_t *)text->chr;
        codepoints = text->len / sizeof(char32_t);
        break;

    case 16: // UTF-16: conversion required
        // image_log_trace("UTF-16 string. Converting to UTF32");
        codepoints = utf32.ConvertUTF16(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
        break;

    case 8: // UTF-8: conversion required
        // image_log_trace("UTF-8 string. Converting to UTF32");
        codepoints = utf32.ConvertUTF8(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
        break;

    default: // ASCII: conversion required
        // image_log_trace("ASCII string. Converting to UTF32");
        codepoints = utf32.ConvertCP437(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
    }

    if (!codepoints) {
        if (passed & 8)
            sub__dest(old_dst_img);
        return;
    }

    FontManager::Font *fnt = nullptr;
    FT_Face face = nullptr;
    FT_Vector strPixSize, pen;

    if (qb64_fh < 32) {
        strPixSize.x = codepoints * 8;
        strPixSize.y = qb64_fh;
        pen.x = pen.y = 0;
        // image_log_trace("Using built-in font %i", qb64_fh);
    } else {
        // Note: We don't check the validity of the font handle here because it is already checked in libqb.
        // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(font[qb64_fh]));
        fnt = fontManager.fonts[font[qb64_fh]];
        face = fnt->face;
        strPixSize.x = fnt->GetStringPixelWidth(str32, codepoints);
        pen.x = 0;
        if (FT_IS_SCALABLE(face)) {
            strPixSize.y = FT_MulDiv(((FT_Long)face->ascender - (FT_Long)face->descender), (FT_Long)fnt->defaultHeight, (FT_Long)face->units_per_EM);
            pen.y = ((FT_Pos)face->ascender * fnt->defaultHeight) / (FT_Pos)face->units_per_EM;
        } else {
            strPixSize.y = fnt->defaultHeight;
            pen.y = fnt->baseline;
        }

        // image_log_trace("pen.y = %i", pen.y);
        // image_log_trace("Using custom font. Scalable = %i", FT_IS_SCALABLE(face));
    }

    if (max_width && max_width < strPixSize.x)
        strPixSize.x = max_width;

    auto drawBuf = (uint8_t *)calloc(strPixSize.x, strPixSize.y);
    if (!drawBuf) {
        if (passed & 8)
            sub__dest(old_dst_img);
        return;
    }

    // image_log_trace("Allocated (%lu x %lu) buffer", strPixSize.x, strPixSize.y);

    auto isMonochrome = (write_page->bytes_per_pixel == 1) || ((write_page->bytes_per_pixel == 4) && (write_page->alpha_disabled)) ||
                        (fontflags[qb64_fh] & FONT_LOAD_DONTBLEND); // do we need to do monochrome rendering?

    if (qb64_fh < 32) {
        // Render using a built-in font
        // image_log_trace("Rendering using built-in font");

        FT_Vector draw, pixmap;
        uint8_t const *builtinFont = nullptr;

        for (size_t i = 0; i < codepoints; i++) {
            auto cp = str32[i];
            if (cp > 255)
                cp = 32; // our built-in fonts only has ASCII glyphs

            if (max_width && pen.x + 8 > start_x + max_width)
                break;

            switch (qb64_fh) {
            case 8:
                builtinFont = &charset8x8[cp][0][0];
                break;

            case 14:
                builtinFont = &charset8x16[cp][1][0];
                break;

            case 16:
                builtinFont = &charset8x16[cp][0][0];
            }

            for (draw.y = pen.y, pixmap.y = 0; pixmap.y < qb64_fh; draw.y++, pixmap.y++) {
                for (draw.x = pen.x, pixmap.x = 0; pixmap.x < 8; draw.x++, pixmap.x++) {
                    *(drawBuf + strPixSize.x * draw.y + draw.x) = *builtinFont++;
                }
            }

            pen.x += 8;
        }
    } else {
        // Render using custom font
        // image_log_trace("Rendering using TrueType font");

        if (fnt->monospaceWidth) {
            // Monospace rendering
            for (size_t i = 0; i < codepoints; i++) {
                auto cp = str32[i];

                auto glyph = fnt->GetGlyph(cp, isMonochrome);
                if (glyph) {
                    if (max_width && pen.x + fnt->monospaceWidth > start_x + max_width)
                        break;

                    glyph->RenderBitmap(drawBuf, strPixSize.x, strPixSize.y,
                                        pen.x + glyph->bitmap->bearing.x + (fnt->monospaceWidth >> 1) - (glyph->bitmap->advanceWidth >> 1),
                                        pen.y - glyph->bitmap->bearing.y);
                    pen.x += fnt->monospaceWidth;
                }
            }
        } else {
            // Variable width rendering
            auto hasKerning = FT_HAS_KERNING(fnt->face); // set to true if font has kerning info
            FontManager::Font::Glyph *glyph = nullptr;
            FontManager::Font::Glyph *previousGlyph = nullptr;

            for (size_t i = 0; i < codepoints; i++) {
                auto cp = str32[i];

                glyph = fnt->GetGlyph(cp, isMonochrome);
                if (glyph) {

                    if (max_width && pen.x + glyph->bitmap->size.x > start_x + max_width)
                        break;

                    // Add kerning advance width if kerning table is available
                    if (hasKerning && previousGlyph && glyph) {
                        FT_Vector delta;
                        FT_Get_Kerning(fnt->face, previousGlyph->index, glyph->index, FT_KERNING_DEFAULT, &delta);
                        pen.x += delta.x >> 6;
                    }

                    glyph->RenderBitmap(drawBuf, strPixSize.x, strPixSize.y, pen.x + glyph->bitmap->bearing.x, pen.y - glyph->bitmap->bearing.y);
                    pen.x += glyph->bitmap->advanceWidth; // add advance width
                    previousGlyph = glyph;                // save the current glyph pointer for use later
                }
            }
        }
    }

    // Resolve coordinates based on current viewport settings
    if (write_page->clipping_or_scaling) {
        if (write_page->clipping_or_scaling == 2) {
            start_x = qbr_float_to_long((float)start_x * write_page->scaling_x + write_page->scaling_offset_x) + write_page->view_offset_x;
            start_y = qbr_float_to_long((float)start_y * write_page->scaling_y + write_page->scaling_offset_y) + write_page->view_offset_y;
        } else {
            start_x += write_page->view_offset_x;
            start_y += write_page->view_offset_y;
        }
    }

    auto alphaSrc = drawBuf;

    // 8-bit / alpha-disabled 32-bit / dont-blend (alpha may still be applied)
    if (isMonochrome) {
        switch (write_page->print_mode) {
        case 3:
            for (pen.y = 0; pen.y < strPixSize.y; pen.y++) {
                for (pen.x = 0; pen.x < strPixSize.x; pen.x++) {
                    if (*alphaSrc++)
                        pset_and_clip(start_x + pen.x, start_y + pen.y, write_page->color);
                    else
                        pset_and_clip(start_x + pen.x, start_y + pen.y, write_page->background_color);
                }
            }
            break;

        case 1:
            for (pen.y = 0; pen.y < strPixSize.y; pen.y++) {
                for (pen.x = 0; pen.x < strPixSize.x; pen.x++) {
                    if (*alphaSrc++)
                        pset_and_clip(start_x + pen.x, start_y + pen.y, write_page->color);
                }
            }
            break;

        case 2:
            for (pen.y = 0; pen.y < strPixSize.y; pen.y++) {
                for (pen.x = 0; pen.x < strPixSize.x; pen.x++) {
                    if (!(*alphaSrc++))
                        pset_and_clip(start_x + pen.x, start_y + pen.y, write_page->background_color);
                }
            }
        }
    } else {
        uint32_t a = image_get_bgra_alpha(write_page->color) + 1;
        uint32_t a2 = image_get_bgra_alpha(write_page->background_color) + 1;
        uint32_t z = image_get_bgra_bgr(write_page->color);
        uint32_t z2 = image_get_bgra_bgr(write_page->background_color);

        switch (write_page->print_mode) {
        case 3: {
            float alpha1 = image_get_bgra_alpha(write_page->color);
            float r1 = image_get_bgra_red(write_page->color);
            float g1 = image_get_bgra_green(write_page->color);
            float b1 = image_get_bgra_blue(write_page->color);
            float alpha2 = image_get_bgra_alpha(write_page->background_color);
            float r2 = image_get_bgra_red(write_page->background_color);
            float g2 = image_get_bgra_green(write_page->background_color);
            float b2 = image_get_bgra_blue(write_page->background_color);
            float dr = r2 - r1;
            float dg = g2 - g1;
            float db = b2 - b1;
            float da = alpha2 - alpha1;
            float cw =
                alpha1 ? alpha2 / alpha1 : 100000; // color weight multiplier, avoids seeing black when transitioning from RGBA(?,?,?,255) to RGBA(0,0,0,0)

            for (pen.y = 0; pen.y < strPixSize.y; pen.y++) {
                for (pen.x = 0; pen.x < strPixSize.x; pen.x++) {
                    float d = *alphaSrc++;
                    d = 255 - d;
                    d /= 255.0f;
                    float alpha3 = alpha1 + da * d;
                    d *= cw;
                    if (d > 1.0f)
                        d = 1.0f;
                    float r3 = r1 + dr * d;
                    float g3 = g1 + dg * d;
                    float b3 = b1 + db * d;
                    pset_and_clip(start_x + pen.x, start_y + pen.y,
                                  image_make_bgra(qbr_float_to_long(r3), qbr_float_to_long(g3), qbr_float_to_long(b3), qbr_float_to_long(alpha3)));
                }
            }
        } break;

        case 1:
            for (pen.y = 0; pen.y < strPixSize.y; pen.y++) {
                for (pen.x = 0; pen.x < strPixSize.x; pen.x++) {
                    if (*alphaSrc)
                        pset_and_clip(start_x + pen.x, start_y + pen.y, ((*alphaSrc * a) >> 8 << 24) + z);
                    ++alphaSrc;
                }
            }
            break;

        case 2:
            for (pen.y = 0; pen.y < strPixSize.y; pen.y++) {
                for (pen.x = 0; pen.x < strPixSize.x; pen.x++) {
                    if (*alphaSrc != 255)
                        pset_and_clip(start_x + pen.x, start_y + pen.y, (((255 - *alphaSrc) * a2) >> 8 << 24) + z2);
                    ++alphaSrc;
                }
            }
        }
    }

    free(drawBuf);

    if (passed & 8)
        sub__dest(old_dst_img);
}

/// @brief Calculate the starting pixel positions of each codepoint to an array. First one being zero.
/// This also calculates the pixel position of the last + 1 character.
/// @param text Text for which the data needs to be calculated. This can be unicode encoded
/// @param arr A QB64 LONG array. This should be codepoints + 1 long. If the array is shorter additional calculated data is ignored
/// @param utf_encoding The UTF encoding of the text (0 = ASCII, 8 = UTF-8, 16 - UTF-16, 32 = UTF-32)
/// @param qb64_fh A QB64 font handle (this can be a builtin font as well)
/// @param passed Optional arguments flag
/// @return Total codepoints in `text`
int32_t func__UCharPos(const qbs *text, void *arr, int32_t utf_encoding, int32_t qb64_fh, int32_t passed) {
    static UTF32 utf32;
    libqb_mutex_guard lock(fontManager.m);

    if (is_error_pending() || !text->len)
        return 0;

    // Check if have an array to work with
    // If not then simply return the count of codepoints later
    if (!(passed & 1)) {
        // image_log_trace("Array not passed");
        arr = nullptr;
    }

    // Check UTF argument
    if (passed & 2) {
        if (!IS_VALID_UTF_ENCODING(utf_encoding)) {
            error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
            return 0;
        }
    } else {
        utf_encoding = 0;
    }

    // Check if a valid font handle was passed
    if (passed & 4) {
        if (!IS_VALID_QB64_FONT_HANDLE(qb64_fh)) {
            error(QB_ERROR_INVALID_HANDLE);
            return 0;
        }
    } else {
        qb64_fh = write_page->font; // else get the current write page font handle
    }

    // Convert the string to UTF-32 if needed
    char32_t const *str32 = nullptr;
    size_t codepoints = 0;

    switch (utf_encoding) {
    case 32: // UTF-32: no conversion needed
        str32 = (char32_t *)text->chr;
        codepoints = text->len / sizeof(char32_t);
        break;

    case 16: // UTF-16: conversion required
        codepoints = utf32.ConvertUTF16(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
        break;

    case 8: // UTF-8: conversion required
        codepoints = utf32.ConvertUTF8(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
        break;

    default: // ASCII: conversion required
        codepoints = utf32.ConvertCP437(text->chr, text->len);
        if (codepoints)
            str32 = utf32.GetString().data();
    }

    // Simply return the codepoint count if we do not have any array
    if (!arr || !codepoints)
        return (int32_t)codepoints;

    auto element = (uint32_t *)((byte_element_struct *)arr)->offset;
    auto elements = ((byte_element_struct *)arr)->length / sizeof(uint32_t);
    FontManager::Font *fnt = nullptr;
    FT_Pos monospaceWidth;

    if (qb64_fh < 32) {
        monospaceWidth = 8; // built-in fonts always have a width of 8
    } else {
        // IMAGE_DEBUG_CHECK(IS_VALID_FONT_HANDLE(font[qb64_fh]));
        fnt = fontManager.fonts[font[qb64_fh]];
        monospaceWidth = fnt->monospaceWidth;
    }

    if (monospaceWidth) {
        // image_log_trace("Calculating positions for monospaced font");

        // Fixed width font character positions
        for (size_t i = 0; i < codepoints; i++) {
            if (i < elements)
                element[i] = i * monospaceWidth;
        }

        // (element[codepoints] - 1) = the end position of the last char in the string
        if (codepoints < elements)
            element[codepoints] = codepoints * monospaceWidth;
    } else {
        // image_log_trace("Calculating positions for variable width font");

        // Variable width font character positions
        auto face = fnt->face;
        auto hasKerning = FT_HAS_KERNING(fnt->face); // set to true if font has kerning info
        FontManager::Font::Glyph *glyph = nullptr;
        FontManager::Font::Glyph *previousGlyph = nullptr;
        FT_Pos penX = 0;
        auto isMonochrome = (write_page->bytes_per_pixel == 1) || ((write_page->bytes_per_pixel == 4) && (write_page->alpha_disabled)) ||
                            (fontflags[qb64_fh] & FONT_LOAD_DONTBLEND); // monochrome or AA?

        for (size_t i = 0; i < codepoints; i++) {
            auto cp = str32[i];

            glyph = fnt->GetGlyph(cp, isMonochrome);
            if (glyph) {
                if (i < elements)
                    element[i] = penX;

                // Add kerning advance width if kerning table is available
                if (hasKerning && previousGlyph && glyph) {
                    FT_Vector delta;
                    FT_Get_Kerning(fnt->face, previousGlyph->index, glyph->index, FT_KERNING_DEFAULT, &delta);
                    penX += delta.x >> 6;
                }

                penX += glyph->bitmap->advanceWidth; // add advance width
                previousGlyph = glyph;               // save the current glyph pointer for use later
            }
        }

        // (element[codepoints] - 1) = the end position of the last char in the string
        if (codepoints < elements)
            element[codepoints] = penX;
    }

    return (int32_t)codepoints;
}
