#include "Font.h"

#include <tinyxml2.h>

#include "Alloc.h"
#include "Constants.h"
#include "CustomLevels.h"
#include "FileSystemUtils.h"
#include "FontBidi.h"
#include "Graphics.h"
#include "GraphicsResources.h"
#include "GraphicsUtil.h"
#include "Localization.h"
#include "UTF8.h"
#include "UtilityClass.h"
#include "Vlogging.h"
#include "XMLUtils.h"

extern "C"
{
#include <c-hashmap/map.h>
}

namespace font
{

#define GLYPH_EXISTS 0x1
#define GLYPH_COLOR 0x2

struct GlyphInfo
{
    uint16_t image_idx;
    uint8_t advance;
    uint8_t flags;
};

/* Codepoints go up to U+10FFFF, so we have 0x110 (272) pages
 * of 0x1000 (4096) glyphs, allocated as needed */
#define FONT_N_PAGES 0x110
#define FONT_PAGE_SIZE 0x1000

enum FontType
{
    FontType_FONT,
    FontType_BUTTONS
};

struct Font
{
    char name[64];
    char display_name[SCREEN_WIDTH_CHARS + 1];

    FontType type;

    uint8_t glyph_w;
    uint8_t glyph_h;

    SDL_Texture* image;

    GlyphInfo* glyph_page[FONT_N_PAGES];

    char fallback_key[64];
    uint8_t fallback_idx;
    bool fallback_idx_valid;
};

struct FontContainer
{
    uint8_t count;
    Font* fonts;
    hashmap* map_name_idx;
};

struct PrintFlags
{
    uint8_t scale;
    Font* font_sel;
    uint8_t brightness;
    bool border;
    bool full_border;
    bool align_cen;
    bool align_right;
    bool cjk_low;
    bool cjk_high;
    bool rtl;
    bool rtl_xflip;
};

static FontContainer fonts_main = {};
static FontContainer fonts_custom = {};

static uint8_t font_idx_8x8 = 0;

uint8_t font_idx_options_n = 0;
uint8_t font_idx_options[20];

static bool font_level_is_interface = false;
bool font_idx_level_is_custom = false;
uint8_t font_idx_level = 0;

static void codepoint_split(
    const uint32_t codepoint,
    short* page,
    short* glyph
)
{
    // Splits a code point (0x10FFFF) into page (0x10F) and glyph (0xFFF)
    if (codepoint > 0x10FFFF)
    {
        codepoint_split(0xFFFD, page, glyph);
        return;
    }
    *page = codepoint >> 12;
    *glyph = codepoint % FONT_PAGE_SIZE;
}

static GlyphInfo* get_glyphinfo(
    const Font* f,
    const uint32_t codepoint
)
{
    short page, glyph;
    codepoint_split(codepoint, &page, &glyph);

    if (f->glyph_page[page] == NULL)
    {
        return NULL;
    }

    return &f->glyph_page[page][glyph];
}

static void add_glyphinfo(
    Font* f,
    const uint32_t codepoint,
    const int image_idx
)
{
    if (image_idx < 0 || image_idx > 65535)
    {
        return;
    }

    short page, glyph;
    codepoint_split(codepoint, &page, &glyph);

    if (f->glyph_page[page] == NULL)
    {
        f->glyph_page[page] = (GlyphInfo*) SDL_calloc(FONT_PAGE_SIZE, sizeof(GlyphInfo));
        if (f->glyph_page[page] == NULL)
        {
            return;
        }
    }

    f->glyph_page[page][glyph].image_idx = image_idx;
    f->glyph_page[page][glyph].advance = f->glyph_w;
    f->glyph_page[page][glyph].flags = GLYPH_EXISTS;
}

static bool glyph_is_valid(const GlyphInfo* glyph)
{
    return glyph->flags & GLYPH_EXISTS;
}

static Font* fallback_for(const Font* f)
{
    if (!f->fallback_idx_valid)
    {
        return NULL;
    }
    return &fonts_main.fonts[f->fallback_idx];
}

static GlyphInfo* find_glyphinfo(const Font* f, const uint32_t codepoint, const Font** f_glyph)
{
    /* Get the GlyphInfo for a specific codepoint, or <?> or ? if it doesn't exist.
     * f_glyph may be either set to f (the main specified font) or its fallback font, if it exists.
     * As a last resort, may return NULL. */
    *f_glyph = f;

    GlyphInfo* glyph = get_glyphinfo(f, codepoint);
    if (glyph != NULL && glyph_is_valid(glyph))
    {
        return glyph;
    }

    Font* f_fallback = fallback_for(f);
    if (f_fallback != NULL && (glyph = get_glyphinfo(f_fallback, codepoint)) != NULL && glyph_is_valid(glyph))
    {
        *f_glyph = f_fallback;
        return glyph;
    }

    glyph = get_glyphinfo(f, 0xFFFD);
    if (glyph != NULL && glyph_is_valid(glyph))
    {
        return glyph;
    }
    glyph = get_glyphinfo(f, '?');
    if (glyph != NULL && glyph_is_valid(glyph))
    {
        return glyph;
    }

    return NULL;
}

static int get_advance_ff(const Font* f, const Font* f_glyph, const GlyphInfo* glyph)
{
    /* Internal function - get the correct advance after we have
     * determined whether the glyph is from the fallback font or not. */

    if (glyph == NULL)
    {
        return f->glyph_w;
    }

    /* If the glyph is a fallback glyph, center it relative to the main font
     * instead of trusting the fallback's width */
    if (f_glyph != f)
    {
        return f->glyph_w;
    }

    return glyph->advance;
}

int get_advance(const Font* f, const uint32_t codepoint)
{
    // Get the width of a single character in a font
    if (f == NULL)
    {
        return 8;
    }

    const Font* f_glyph;
    GlyphInfo* glyph = find_glyphinfo(f, codepoint, &f_glyph);
    return get_advance_ff(f, f_glyph, glyph);
}

static bool decode_xml_range(tinyxml2::XMLElement* elem, unsigned* start, unsigned* end)
{
    // We do support hexadecimal start/end like "0x10FFFF"
    if (elem->QueryUnsignedAttribute("start", start) != tinyxml2::XML_SUCCESS
        || elem->QueryUnsignedAttribute("end", end) != tinyxml2::XML_SUCCESS
        || *end < *start || *start > 0x10FFFF
    )
    {
        return false;
    }

    *end = SDL_min(*end, 0x10FFFF);
    return true;
}

static uint8_t load_font(FontContainer* container, const char* name)
{
    if (container->count >= 254)
    {
        return 0;
    }
    Font* new_fonts = (Font*) SDL_realloc(container->fonts, sizeof(Font)*(container->count+1));
    if (new_fonts == NULL)
    {
        return 0;
    }
    container->fonts = new_fonts;
    uint8_t f_idx = container->count++;
    Font* f = &container->fonts[f_idx];

    vlog_info("Loading font \"%s\"...", name);

    char name_png[256];
    char name_txt[256];
    char name_xml[256];
    SDL_snprintf(name_png, sizeof(name_png), "graphics/%s.png", name);
    SDL_snprintf(name_txt, sizeof(name_txt), "graphics/%s.txt", name);
    SDL_snprintf(name_xml, sizeof(name_xml), "graphics/%s.fontmeta", name);
    SDL_strlcpy(f->name, name, sizeof(f->name));
    SDL_strlcpy(f->display_name, name, sizeof(f->display_name));

    f->type = FontType_FONT;

    f->glyph_w = 8;
    f->glyph_h = 8;

    f->fallback_key[0] = '\0';
    f->fallback_idx_valid = false;

    bool white_teeth = false;

    tinyxml2::XMLDocument doc;
    tinyxml2::XMLHandle hDoc(&doc);
    tinyxml2::XMLElement* pElem;
    bool xml_loaded = false;

    if (FILESYSTEM_areAssetsInSameRealDir(name_png, name_xml)
        && FILESYSTEM_loadAssetTiXml2Document(name_xml, doc)
    )
    {
        xml_loaded = true;
        hDoc = hDoc.FirstChildElement("font_metadata");

        if ((pElem = hDoc.FirstChildElement("display_name").ToElement()) != NULL)
        {
            SDL_strlcpy(f->display_name, pElem->GetText(), sizeof(f->display_name));
        }
        if ((pElem = hDoc.FirstChildElement("type").ToElement()) != NULL)
        {
            if (SDL_strcmp(pElem->GetText(), "buttons") == 0)
            {
                f->type = FontType_BUTTONS;
            }
        }
        if ((pElem = hDoc.FirstChildElement("width").ToElement()) != NULL)
        {
            f->glyph_w = help.Int(pElem->GetText());
        }
        if ((pElem = hDoc.FirstChildElement("height").ToElement()) != NULL)
        {
            f->glyph_h = help.Int(pElem->GetText());
        }
        if ((pElem = hDoc.FirstChildElement("white_teeth").ToElement()) != NULL)
        {
            // If 1, we don't need to whiten the entire font (like in old versions)
            white_teeth = help.Int(pElem->GetText());
        }
        if ((pElem = hDoc.FirstChildElement("fallback").ToElement()) != NULL)
        {
            SDL_strlcpy(f->fallback_key, pElem->GetText(), sizeof(f->fallback_key));
        }
    }

    f->image = LoadImage(name_png, white_teeth ? TEX_COLOR : TEX_WHITE);
    SDL_zeroa(f->glyph_page);

    if (f->image == NULL)
    {
        return f_idx;
    }

    /* We may have a 2.3-style font.txt with all the characters.
     * font.txt takes priority over <chars> in the XML.
     * If neither exist, it's just ASCII. */
    bool charset_loaded = false;
    bool special_loaded = false;
    unsigned char* charmap = NULL;
    size_t length;
    if (FILESYSTEM_areAssetsInSameRealDir(name_png, name_txt))
    {
        /* The .txt can contain null bytes, but it's still null-terminated - it protects
         * against incomplete sequences getting the UTF-8 decoder to read out of bounds. */
        FILESYSTEM_loadAssetToMemory(name_txt, &charmap, &length);
    }
    if (charmap != NULL)
    {
        // We have a .txt! It's an obsolete system, but it takes priority if the file exists.
        const char* current = (char*) charmap;
        const char* end = (char*) charmap + length;
        int pos = 0;
        while (current < end)
        {
            uint32_t codepoint = UTF8_next(&current);
            add_glyphinfo(f, codepoint, pos);
            ++pos;
        }

        VVV_free(charmap);
        charset_loaded = true;
    }

    if (xml_loaded && !charset_loaded && (pElem = hDoc.FirstChildElement("chars").ToElement()) != NULL)
    {
        // <chars> in the XML is the preferred system.
        int pos = 0;
        tinyxml2::XMLElement* subElem;
        FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
        {
            EXPECT_ELEM(subElem, "range");

            unsigned start, end;
            if (!decode_xml_range(subElem, &start, &end))
            {
                continue;
            }

            for (uint32_t codepoint = start; codepoint <= end; codepoint++)
            {
                add_glyphinfo(f, codepoint, pos);
                ++pos;
            }
        }
        charset_loaded = true;
    }

    if (!charset_loaded)
    {
        /* If we don't have font.txt and no <chars> tag either,
         * this font is 2.2-and-below-style plain ASCII.
         * Or well... 2.3 interpreted these as
         * "all unicode from 0 to however much is in the image"... */

        SDL_Surface* temp_surface = LoadImageSurface(name_png);
        if (temp_surface != NULL)
        {
            const uint32_t chars_per_line = temp_surface->w / f->glyph_w;
            const uint32_t max_codepoint = (temp_surface->h / f->glyph_h) * chars_per_line;

            for (uint32_t codepoint = 0x00; codepoint < max_codepoint; codepoint++)
            {
                if (codepoint > 0x7F)
                {
                    /* Only include characters with actual pixels...
                     * If the font.png is too big (normally it is) we _want_ question marks. */
                    const int glyph_x = (codepoint % chars_per_line) * f->glyph_w;
                    const int glyph_y = (codepoint / chars_per_line) * f->glyph_h;

                    bool found_pixel = false;
                    for (int pixel_y = 0; pixel_y < f->glyph_h; pixel_y++)
                    {
                        for (int pixel_x = 0; pixel_x < f->glyph_w; pixel_x++)
                        {
                            if (ReadPixel(temp_surface, glyph_x+pixel_x, glyph_y+pixel_y).a > 0)
                            {
                                found_pixel = true;
                                goto no_more_pixels;
                            }
                        }
                    }
                    no_more_pixels:
                    if (!found_pixel)
                    {
                        // Do not add it
                        continue;
                    }
                }
                add_glyphinfo(f, codepoint, codepoint);
            }

            VVV_freefunc(SDL_FreeSurface, temp_surface);
        }
    }

    if (xml_loaded && (pElem = hDoc.FirstChildElement("special").ToElement()) != NULL)
    {
        tinyxml2::XMLElement* subElem;
        FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
        {
            EXPECT_ELEM(subElem, "range");

            unsigned start, end;
            if (!decode_xml_range(subElem, &start, &end))
            {
                continue;
            }

            int advance = subElem->IntAttribute("advance", -1);
            int color = subElem->IntAttribute("color", -1);

            for (uint32_t codepoint = start; codepoint <= end; codepoint++)
            {
                GlyphInfo* glyph = get_glyphinfo(f, codepoint);
                if (glyph == NULL)
                {
                    continue;
                }
                if (advance >= 0 && advance < 256)
                {
                    glyph->advance = advance;
                }
                if (color == 0)
                {
                    glyph->flags &= ~GLYPH_COLOR;
                }
                else if (color == 1)
                {
                    glyph->flags |= GLYPH_COLOR;
                }
            }
        }
        special_loaded = true;
    }

    if (!special_loaded && f->glyph_w == 8 && f->glyph_h == 8)
    {
        /* If we don't have <special>, and the font is 8x8,
         * 0x00-0x1F will be less wide because that's how it has always been. */
        for (uint32_t codepoint = 0x00; codepoint < 0x20; codepoint++)
        {
            GlyphInfo* glyph = get_glyphinfo(f, codepoint);
            if (glyph != NULL)
            {
                glyph->advance = 6;
            }
        }
    }

    return f_idx;
}

static bool find_font_by_name(FontContainer* container, const char* name, uint8_t* idx)
{
    // Returns true if font found (and idx is set), false if not found
    if (container->map_name_idx == NULL)
    {
        // No fonts yet...
        return false;
    }

    uintptr_t i;
    if (hashmap_get(container->map_name_idx, name, SDL_strlen(name), &i))
    {
        *idx = i;
        return true;
    }
    return false;
}

bool find_main_font_by_name(const char* name, uint8_t* idx)
{
    return find_font_by_name(&fonts_main, name, idx);
}

uint8_t get_font_idx_8x8(void)
{
    return font_idx_8x8;
}

bool level_font_is_main_idx(const uint8_t idx)
{
    return !font_idx_level_is_custom && font_idx_level == idx;
}

void set_level_font(const char* name)
{
    /* Apply the choice for a certain level-specific font.
     * This function is for custom levels. */
    font_level_is_interface = false;

    if (find_font_by_name(&fonts_custom, name, &font_idx_level))
    {
        font_idx_level_is_custom = true;
    }
    else
    {
        font_idx_level_is_custom = false;
        if (!find_font_by_name(&fonts_main, name, &font_idx_level))
        {
            if (SDL_strcmp(name, "font") != 0)
            {
                set_level_font("font");
            }
            else
            {
                font_idx_level = font_idx_8x8;
            }
        }
    }

    cl.rtl = SDL_strcmp(name, "font_ar") == 0; // FIXME: make different menu options for choosing LTR/RTL of the same font
}

void set_level_font_interface(void)
{
    /* Set the level font equal to the interface font.
     * This function is for the main game. */
    font_level_is_interface = true;
}

void set_level_font_new(void)
{
    /* Set the level font to the default font for new levels.
     * This function is for starting the editor. */
    font_level_is_interface = false;
    font_idx_level_is_custom = false;
    if (loc::new_level_font == "")
    {
        /* Just take the language's font
         * (Japanese VVVVVV can make Japanese levels by default, etc) */
        font_idx_level = loc::get_langmeta()->font_idx;
    }
    else
    {
        /* If the user has changed the font (wants to make levels
         * for a different userbase) then remember that choice. */
        if (!find_main_font_by_name(loc::new_level_font.c_str(), &font_idx_level))
        {
            font_idx_level = font_idx_8x8;
        }
    }

    cl.level_font_name = get_main_font_name(font_idx_level);
    cl.rtl = cl.level_font_name == "font_ar"; // FIXME: make different menu options for choosing LTR/RTL of the same font
}

static void fill_map_name_idx(FontContainer* container)
{
    /* Initialize the name->idx hashmap for the fonts in this container.
     * This should only be done once, after all the fonts are added. */
    container->map_name_idx = hashmap_create();

    for (uint8_t i = 0; i < container->count; i++)
    {
        Font* f = &container->fonts[i];
        hashmap_set(container->map_name_idx, f->name, SDL_strlen(f->name), i);
    }
}

static void set_fallbacks(FontContainer* container)
{
    /* Initialize the value of fallback_idx for all fonts in this container.
     * Only main fonts can be fallback fonts. */
    for (uint8_t i = 0; i < container->count; i++)
    {
        Font* f = &container->fonts[i];
        if (find_main_font_by_name(f->fallback_key, &f->fallback_idx))
        {
            f->fallback_idx_valid = true;
        }
    }
}

static void load_font_filename(bool is_custom, const char* filename)
{
    // Load font.png, and everything that matches *.fontmeta (but not font.fontmeta)
    size_t expected_ext_start;
    bool is_fontpng = SDL_strcmp(filename, "font.png") == 0;
    if (is_fontpng)
    {
        expected_ext_start = SDL_strlen(filename)-4;
    }
    else
    {
        expected_ext_start = SDL_strlen(filename)-9;
    }
    if (is_fontpng || (endsWith(filename, ".fontmeta") && SDL_strcmp(filename, "font.fontmeta") != 0))
    {
        char font_name[64];
        SDL_strlcpy(font_name, filename, sizeof(font_name));
        font_name[SDL_min(63, expected_ext_start)] = '\0';

        uint8_t f_idx = load_font(is_custom ? &fonts_custom : &fonts_main, font_name);

        if (is_fontpng && !is_custom)
        {
            font_idx_8x8 = f_idx;
        }
    }
}

void load_main(void)
{
    // Load all global fonts
    EnumHandle handle = {};
    const char* item;
    while ((item = FILESYSTEM_enumerate("graphics", &handle)) != NULL)
    {
        load_font_filename(false, item);
    }
    FILESYSTEM_freeEnumerate(&handle);
    font_idx_level = font_idx_8x8;

    fill_map_name_idx(&fonts_main);
    set_fallbacks(&fonts_main);

    // Initialize the font menu options, 8x8 font first
    font_idx_options[0] = font_idx_8x8;
    font_idx_options_n = 1;

    for (uint8_t i = 0; i < fonts_main.count; i++)
    {
        if (i == font_idx_8x8)
        {
            continue;
        }
        if (fonts_main.fonts[i].type != FontType_FONT)
        {
            continue;
        }
        font_idx_options[font_idx_options_n++] = i;
        if (font_idx_options_n >= sizeof(font_idx_options))
        {
            break;
        }
    }
}

void load_custom(const char* name)
{
    // Load all custom (level-specific assets) fonts
    unload_custom();
    EnumHandle handle = {};
    const char* item;
    while ((item = FILESYSTEM_enumerateAssets("graphics", &handle)) != NULL)
    {
        load_font_filename(true, item);
    }
    FILESYSTEM_freeEnumerate(&handle);

    fill_map_name_idx(&fonts_custom);
    set_fallbacks(&fonts_custom);

    set_level_font(name);
}

void unload_font(Font* f)
{
    VVV_freefunc(SDL_DestroyTexture, f->image);

    for (int i = 0; i < FONT_N_PAGES; i++)
    {
        VVV_free(f->glyph_page[i]);
    }
}

void unload_font_container(FontContainer* container)
{
    VVV_freefunc(hashmap_free, container->map_name_idx);

    for (uint8_t i = 0; i < container->count; i++)
    {
        unload_font(&container->fonts[i]);
    }
    VVV_free(container->fonts);
    container->fonts = NULL;
    container->count = 0;
}

void unload_custom(void)
{
    // Unload all custom fonts
    unload_font_container(&fonts_custom);
}

void destroy(void)
{
    // Unload all fonts (main and custom) for exiting
    unload_custom();
    unload_font_container(&fonts_main);
}

static Font* container_get(FontContainer* container, uint8_t idx)
{
    /* Get a certain font from the given container (with bounds checking).
     * Does its best to return at least something,
     * but if there are no fonts whatsoever, can return NULL. */

    if (idx < container->count)
    {
        return &container->fonts[idx];
    }
    if (font_idx_8x8 < fonts_main.count)
    {
        return &fonts_main.fonts[font_idx_8x8];
    }
    if (fonts_main.count > 0)
    {
        return &fonts_main.fonts[0];
    }
    if (fonts_custom.count > 0)
    {
        return &fonts_custom.fonts[0];
    }
    return NULL;
}

static Font* fontsel_to_font(int sel, bool* rtl, bool custom)
{
    /* Take font selection integer (0-31) and turn it into the correct Font
     * 0: PR_FONT_INTERFACE - use interface font
     * 1: PR_FONT_LEVEL     - use level font
     * 2: PR_FONT_8X8       - use 8x8 font no matter what
     * 3-31:                - use font index 0-28 (depending on custom, from
     *                        PR_FONT_IDX_IS_CUSTOM)
     *
     * rtl will be set depending on whether we're requesting the interface
     * font (take it from the lang attributes) or level font (take it from
     * the level attributes), or false otherwise. */

    *rtl = false;

    if (sel < 0)
    {
        // Shouldn't happen but better safe than sorry...
        return NULL;
    }

    switch (sel)
    {
    case 1:
        if (!font_level_is_interface)
        {
            *rtl = cl.rtl;
            if (font_idx_level_is_custom)
            {
                return container_get(&fonts_custom, font_idx_level);
            }
            else
            {
                return container_get(&fonts_main, font_idx_level);
            }
        }
        SDL_FALLTHROUGH;
    case 0:
        *rtl = loc::get_langmeta()->rtl;
        return container_get(&fonts_main, loc::get_langmeta()->font_idx);
    case 2:
        return container_get(&fonts_main, font_idx_8x8);
    }

    if (custom)
    {
        return container_get(&fonts_custom, sel-3);
    }
    return container_get(&fonts_main, sel-3);
}

#define FLAG_PART(start, count) ((flags >> start) % (1 << count))
static PrintFlags decode_print_flags(uint32_t flags)
{
    PrintFlags pf;
    pf.scale = FLAG_PART(0, 3) + 1;
    pf.font_sel = fontsel_to_font(
        FLAG_PART(3, 5), &pf.rtl, flags & PR_FONT_IDX_IS_CUSTOM
    );
    if (flags & PR_RTL_FORCE)
    {
        pf.rtl = true;
    }
    pf.brightness = ~FLAG_PART(8, 8) & 0xff;
    pf.border = flags & PR_BOR;
    pf.full_border = flags & PR_FULLBOR;
    pf.align_cen = flags & PR_CEN;
    pf.align_right = flags & PR_RIGHT;
    pf.cjk_low = flags & PR_CJK_LOW;
    pf.cjk_high = flags & PR_CJK_HIGH;
    pf.rtl_xflip = flags & PR_RTL_XFLIP;

    if (pf.full_border)
    {
        pf.border = false;
    }

    return pf;
}
#undef FLAG_PART

static bool next_wrap(
    Font* f,
    size_t* start,
    size_t* len,
    const char* str,
    const int maxwidth
) {
    /* Get information about the current line in wordwrapped text,
     * given this line starts at str[*start].
     * *start is updated to the start of the next line. */
    size_t idx = 0;
    size_t lenfromlastspace = 0;
    size_t lastspace = 0;
    int linewidth = 0;
    *len = 0;

    if (str[idx] == '\0')
    {
        return false;
    }

    while (true)
    {
        uint8_t codepoint_nbytes;
        uint32_t codepoint = UTF8_peek_next(&str[idx], &codepoint_nbytes);

        switch (codepoint)
        {
        case ' ':
            if (loc::get_langmeta()->autowordwrap)
            {
                lenfromlastspace = idx;
                lastspace = *start;
            }
            break;
        case '\n':
        case '|':
            *start += 1;
            SDL_FALLTHROUGH;
        case '\0':
            return true;
        }

        linewidth += get_advance(f, codepoint);

        if (linewidth > maxwidth)
        {
            if (lenfromlastspace != 0)
            {
                *len = lenfromlastspace;
                *start = lastspace + 1;
            }
            if (idx == 0)
            {
                // Oops, we're stuck at a single character
                *len = 1;
                *start += 1;
            }
            return true;
        }

        idx += codepoint_nbytes;
        *start += codepoint_nbytes;
        *len += codepoint_nbytes;
    }
}

static bool next_wrap_buf(
    Font* f,
    char buffer[],
    const size_t buffer_size,
    size_t* start,
    const char* str,
    const int maxwidth
) {
    /* Get each line of wordwrapped text, writing one line at a time to a buffer.
     * Call as follows:
     *
     * char buf[256];
     * size_t start = 0;
     * while (next_wrap_buf(font, buf, sizeof(buf), &start, "String to wordwrap", 320))
     * {
     *     // buf contains a line of text
     * }
     */
    size_t len = 0;
    const size_t prev_start = *start;

    const bool retval = next_wrap(f, start, &len, &str[*start], maxwidth);

    if (retval)
    {
        /* Like next_split_s(), don't use SDL_strlcpy() here. */
        const size_t length = SDL_min(buffer_size - 1, len);
        SDL_memcpy(buffer, &str[prev_start], length);
        buffer[length] = '\0';
    }

    return retval;
}

std::string string_wordwrap(const uint32_t flags, const std::string& s, int maxwidth, short *lines /*= NULL*/)
{
    /* Return a string wordwrapped to a maximum limit by adding newlines.
     * CJK will need to have autowordwrap disabled and have manually inserted newlines. */

    if (lines != NULL)
    {
        *lines = 1;
    }

    PrintFlags pf = decode_print_flags(flags);
    if (pf.font_sel == NULL)
    {
        return s;
    }

    const char* orig = s.c_str();

    std::string result;
    size_t start = 0;
    bool first = true;

    while (true)
    {
        size_t len = 0;
        const char* part = &orig[start];

        const bool retval = next_wrap(pf.font_sel, &start, &len, part, maxwidth);

        if (!retval)
        {
            return result;
        }

        if (first)
        {
            first = false;
        }
        else
        {
            result.push_back('\n');

            if (lines != NULL)
            {
                (*lines)++;
            }
        }
        result.append(part, len);
    }
}

std::string string_wordwrap_balanced(const uint32_t flags, const std::string& s, int maxwidth)
{
    /* Return a string wordwrapped to a limit of maxwidth by adding newlines.
     * Try to fill the lines as far as possible, and return result where lines are most filled.
     * Goal is to have all lines in textboxes be about as long and to avoid wrapping just one word to a new line.
     * CJK will need to have autowordwrap disabled and have manually inserted newlines. */

    if (!loc::get_langmeta()->autowordwrap)
    {
        return s;
    }

    short lines;
    string_wordwrap(flags, s, maxwidth, &lines);

    int bestwidth = maxwidth;
    if (lines > 1)
    {
        for (int curlimit = maxwidth; curlimit > 1; curlimit -= 8)
        {
            short try_lines;
            string_wordwrap(flags, s, curlimit, &try_lines);

            if (try_lines > lines)
            {
                bestwidth = curlimit + 8;
                break;
            }
        }
    }

    return string_wordwrap(flags, s, bestwidth);
}

std::string string_unwordwrap(const std::string& s)
{
    /* Takes a string wordwrapped by newlines, and turns it into a single line, undoing the wrapping.
     * Also trims any leading/trailing whitespace and collapses multiple spaces into one (to undo manual centering)
     * Only applied to English, so langmeta.autowordwrap isn't used here (it'd break looking up strings) */

    std::string result;
    result.reserve(s.length());
    bool latest_was_space = true; // last character was a space (or the beginning, don't want leading whitespace)
    int consecutive_newlines = 0; // number of newlines currently encountered in a row (multiple newlines should stay!)

    const char* str = s.c_str();
    uint32_t ch;
    while ((ch = UTF8_next(&str)))
    {
        if (ch == '\n')
        {
            if (consecutive_newlines == 0)
            {
                ch = ' ';
            }
            else if (consecutive_newlines == 1)
            {
                if (!result.empty())
                {
                    // The last character was already a newline, so change it back from the space we thought it should have become.
                    result[result.size()-1] = '\n';
                }
                else
                {
                    // The string starts with two or more newlines, in this case we didn't add the first one at all.
                    result.append("\n\n");
                }
            }
            consecutive_newlines++;
        }
        else
        {
            consecutive_newlines = 0;
        }

        if (ch != ' ' || !latest_was_space)
        {
            result.append(UTF8_encode(ch).bytes);
        }

        latest_was_space = (ch == ' ' || ch == '\n');
    }

    // We could have one trailing space
    if (!result.empty() && result[result.size()-1] == ' ')
    {
        result.erase(result.end()-1);
    }

    return result;
}

static int print_char(
    const Font* f,
    const uint32_t codepoint,
    int x,
    int y,
    const int scale,
    uint8_t r,
    uint8_t g,
    uint8_t b,
    const uint8_t brightness
)
{
    /* Draws the glyph for a codepoint at x,y.
     * Returns the amount of pixels to advance the cursor. */
    if (is_directional_character(codepoint) || is_joiner(codepoint))
    {
        // Some characters should be completely invisible
        return 0;
    }

    const Font* f_glyph;
    GlyphInfo* glyph = find_glyphinfo(f, codepoint, &f_glyph);
    if (glyph == NULL)
    {
        return f->glyph_w * scale;
    }

    if (glyph->flags & GLYPH_COLOR && (r | g | b) != 0)
    {
        r = g = b = brightness;
    }
    else if (brightness < 255)
    {
        float bri_factor = brightness / (float) 255;
        r *= bri_factor;
        g *= bri_factor;
        b *= bri_factor;
    }

    // If the glyph is a fallback glyph, center it
    if (f_glyph != f)
    {
        x += (f->glyph_w - f_glyph->glyph_w) / 2;
        y += (f->glyph_h - f_glyph->glyph_h) / 2;
    }

    graphics.draw_grid_tile(
        f_glyph->image,
        glyph->image_idx,
        x,
        y,
        f_glyph->glyph_w,
        f_glyph->glyph_h,
        r, g, b,
        scale,
        scale * (graphics.flipmode ? -1 : 1)
    );

    return get_advance_ff(f, f_glyph, glyph) * scale;
}

const char* get_main_font_name(uint8_t idx)
{
    Font* f = container_get(&fonts_main, idx);
    if (f == NULL)
    {
        return "";
    }
    return f->name;
}

const char* get_main_font_display_name(uint8_t idx)
{
    Font* f = container_get(&fonts_main, idx);
    if (f == NULL)
    {
        return "";
    }

    if (idx == font_idx_8x8)
    {
        // Deciding the name for the 8x8 font was harder than I'd like to admit.
        if (loc::lang == "en" || loc::get_langmeta()->font_idx != font_idx_8x8)
        {
            // If you use English, or a CJK language: "english/..."
            SDL_strlcpy(
                f->display_name,
                "english/…",
                sizeof(f->display_name)
            );
        }
        else
        {
            // If you use another, e.g. German: "english/deutsch/..."
            SDL_snprintf(
                f->display_name, sizeof(f->display_name),
                "english/%s/…",
                loc::get_langmeta()->nativename.c_str()
            );
        }
    }

    return f->display_name;
}

const char* get_level_font_display_name(void)
{
    if (font_idx_level_is_custom)
    {
        Font* f = container_get(&fonts_custom, font_idx_level);
        if (f == NULL)
        {
            return "";
        }
        return f->display_name;
    }

    return get_main_font_display_name(font_idx_level);
}

bool glyph_dimensions(uint32_t flags, uint8_t* glyph_w, uint8_t* glyph_h)
{
    /* Gets the dimensions (glyph_w and glyph_h) of a certain font.
     * Returns true if the font is valid (glyph_w and/or glyph_h were written to if not NULL), false if not. */

    PrintFlags pf = decode_print_flags(flags);

    if (pf.font_sel == NULL)
    {
        return false;
    }
    if (glyph_w != NULL)
    {
        *glyph_w = pf.font_sel->glyph_w;
    }
    if (glyph_h != NULL)
    {
        *glyph_h = pf.font_sel->glyph_h;
    }
    return true;
}

int len(const uint32_t flags, const char* text)
{
    PrintFlags pf = decode_print_flags(flags);

    if (bidi_should_transform(pf.rtl, text))
    {
        text = bidi_transform(pf.rtl, text);
    }

    int text_len = 0;
    uint32_t codepoint;
    while ((codepoint = UTF8_next(&text)))
    {
        if (!is_directional_character(codepoint) && !is_joiner(codepoint))
        {
            text_len += get_advance(pf.font_sel, codepoint);
        }
    }
    return text_len * pf.scale;
}

int height(const uint32_t flags)
{
    PrintFlags pf = decode_print_flags(flags);
    if (pf.font_sel == NULL)
    {
        return 8;
    }

    return pf.font_sel->glyph_h * pf.scale;
}

bool is_rtl(const uint32_t flags)
{
    PrintFlags pf = decode_print_flags(flags);
    return pf.rtl;
}

void print(
    const uint32_t flags,
    int x,
    int y,
    const char* text,
    const uint8_t r,
    const uint8_t g,
    const uint8_t b
)
{
    PrintFlags pf = decode_print_flags(flags);
    if (pf.font_sel == NULL)
    {
        return;
    }

    if (pf.rtl && pf.rtl_xflip && (!pf.align_cen || x != -1))
    {
        x = SCREEN_WIDTH_PIXELS - x;
        pf.align_right = !pf.align_right;
    }

    if (pf.align_cen || pf.align_right)
    {
        const int textlen = len(flags, text);

        if (pf.align_cen)
        {
            if (x == -1)
            {
                x = SCREEN_WIDTH_PIXELS / 2;
            }
            x -= textlen/2;

            if (!pf.rtl)
            {
                x = SDL_max(x, 0);
            }
            else
            {
                x = SDL_min(x, SCREEN_WIDTH_PIXELS - textlen);
            }
        }
        else
        {
            x -= textlen;
        }
    }

    if (pf.border && !graphics.notextoutline)
    {
        static const int offsets[4][2] = {{0,-1}, {-1,0}, {1,0}, {0,1}};

        for (int offset = 0; offset < 4; offset++)
        {
            print(
                flags & ~PR_BOR & ~PR_CEN & ~PR_RIGHT & ~PR_RTL_XFLIP,
                x + offsets[offset][0]*pf.scale,
                y + offsets[offset][1]*pf.scale,
                text,
                0, 0, 0
            );
        }
    }

    if (pf.full_border)
    {
        static const int offsets[8][2] = { {0,-1}, {-1,0}, {1,0}, {0,1}, {-1,-1}, {1,1}, {-1,1}, {1,-1} };

        for (int offset = 0; offset < 8; offset++)
        {
            print(
                flags & ~PR_FULLBOR & ~PR_CEN & ~PR_RIGHT & ~PR_RTL_XFLIP,
                x + offsets[offset][0] * pf.scale,
                y + offsets[offset][1] * pf.scale,
                text,
                0, 0, 0
            );
        }
    }

    int h_diff_8 = (pf.font_sel->glyph_h-8)*pf.scale;
    if (h_diff_8 < 0)
    {
        /* If the font is less high than 8,
         * just center it (lower on screen) */
        y -= h_diff_8/2;
    }
    else if (pf.cjk_high)
    {
        y -= h_diff_8;
    }
    else if (!pf.cjk_low)
    {
        y -= h_diff_8/2;
    }

    if (bidi_should_transform(pf.rtl, text))
    {
        text = bidi_transform(pf.rtl, text);
    }

    int position = 0;
    uint32_t codepoint;
    while ((codepoint = UTF8_next(&text)))
    {
        position += font::print_char(
            pf.font_sel,
            codepoint,
            x + position,
            y,
            pf.scale,
            r,
            g,
            b,
            pf.brightness
        );
    }
}

void print(
    const uint32_t flags,
    int x,
    int y,
    const std::string& text,
    const uint8_t r,
    const uint8_t g,
    const uint8_t b
)
{
    // Just a std::string overload for now because it's more .c_str() to add than I'm comfortable with...
    print(flags, x, y, text.c_str(), r, g, b);
}

int print_wrap(
    uint32_t flags,
    const int x,
    int y,
    const char* text,
    const uint8_t r,
    const uint8_t g,
    const uint8_t b,
    int linespacing /*= -1*/,
    int maxwidth /*= -1*/
)
{
    PrintFlags pf = decode_print_flags(flags);
    if (pf.font_sel == NULL)
    {
        return y;
    }

    if (linespacing == -1)
    {
        linespacing = 10;
    }
    linespacing = SDL_max(linespacing, pf.font_sel->glyph_h * pf.scale);

    if (maxwidth == -1)
    {
        maxwidth = 304;
    }

    if (pf.border && !graphics.notextoutline && (r|g|b) != 0)
    {
        print_wrap(flags, x, y, text, 0, 0, 0, linespacing, maxwidth);
        flags &= ~PR_BOR;
    }

    if (pf.full_border && (r | g | b) != 0)
    {
        print_wrap(flags, x, y, text, 0, 0, 0, linespacing, maxwidth);
        flags &= ~PR_FULLBOR;
    }

    // This could fit 64 non-BMP characters onscreen, should be plenty
    char buffer[256];
    size_t start = 0;

    if (graphics.flipmode)
    {
        // Correct for the height of the resulting print.
        size_t len = 0;
        while (next_wrap(pf.font_sel, &start, &len, &text[start], maxwidth))
        {
            y += linespacing;
        }
        y -= linespacing;
        start = 0;
    }

    while (next_wrap_buf(pf.font_sel, buffer, sizeof(buffer), &start, text, maxwidth))
    {
        print(flags, x, y, buffer, r, g, b);

        if (graphics.flipmode)
        {
            y -= linespacing;
        }
        else
        {
            y += linespacing;
        }
    }

    return y + linespacing;
}

} // namespace font
