#include "gx/font/GxuFont.hpp"
#include "gx/font/CGxFont.hpp"
#include "gx/font/CGxStringBatch.hpp"
#include "gx/font/FreeType.hpp"
#include "gx/font/GxuFontInternal.hpp"
#include "gx/font/Wrap.hpp"
#include "gx/Device.hpp"
#include "gx/Gx.hpp"
#include "gx/Types.hpp"
#include <storm/Memory.hpp>
#include <storm/Unicode.hpp>
#include <tempest/Math.hpp>
#include <tempest/Rect.hpp>

CGxShader* g_fontPixelShader[1];
CGxShader* g_fontVertexShader[2];
STORM_LIST(CGxFont) g_fonts;
STORM_LIST(CGxString) g_freeStrings;
STORM_LIST(CGxString) g_strings;

static TSList<CGxStringBatch, TSGetLink<CGxStringBatch>> s_unusedBatches;

QUOTEDCODE GxuDetermineQuotedCode(const char* text, int32_t& advance, CImVector* color, uint32_t flags, uint32_t& wide) {
    wide = SUniSGetUTF8(reinterpret_cast<const uint8_t*>(text), &advance);

    switch (wide) {
        case 0x0:
        case 0xFFFFFFFF: {
            return CODE_INVALIDCODE;
        }

        case '\r': {
            auto next = SUniSGetUTF8(reinterpret_cast<const uint8_t*>(text + 1), &advance);
            advance = next == '\n' ? 2 : 1;

            return CODE_NEWLINE;
        }

        case '\n': {
            advance = 1;

            return CODE_NEWLINE;
        }

        default: {
            break;
        }
    }

    if (wide != '|' || flags & FLAG_IGNORE_PIPES) {
        return CODE_INVALIDCODE;
    }

    auto quotedCode = text[advance];

    if (!quotedCode) {
        return CODE_INVALIDCODE;
    }

    switch (quotedCode) {
        case 'C':
        case 'c': {
            if (flags & FLAG_IGNORE_COLORS) {
                return CODE_INVALIDCODE;
            }

            int32_t offset = advance + 1;
            uint8_t value[4];

            for (int32_t i = 0; i < 4; i++) {
                if (!text[offset + 0] || !text[offset + 1]) {
                    return CODE_INVALIDCODE;
                }

                char hex[4];
                hex[0] = text[offset + 0];
                hex[1] = text[offset + 1];
                hex[2] = '\0';

                char* end = nullptr;

                auto v = strtol(hex, &end, 16);

                // Error parsing hex
                if (end && *end) {
                    return CODE_INVALIDCODE;
                }

                value[i] = v;

                offset += 2;
            }

            if (color) {
                // Alpha is ignored
                color->value = CImVector::MakeARGB(0xFF, value[1], value[2], value[3]);
            }

            advance = 10;

            return CODE_COLORON;
        }

        case 'H': {
            if (flags & FLAG_IGNORE_HYPERLINKS) {
                return CODE_INVALIDCODE;
            }

            auto linkText = text + advance;

            while (*linkText) {
                auto code = SUniSGetUTF8(reinterpret_cast<const uint8_t*>(linkText), &advance);
                linkText += advance;

                if (code == '|') {
                    break;
                }
            }

            if (!*linkText) {
                return CODE_INVALIDCODE;
            }

            auto endCode = SUniSGetUTF8(reinterpret_cast<const uint8_t*>(linkText), &advance);
            linkText += advance;

            // Null terminator or end code isn't |h (end link payload)
            if (!*linkText || endCode != 'h') {
                return CODE_INVALIDCODE;
            }

            // Empty link (no payload)
            if (linkText - text == 4) {
                return CODE_INVALIDCODE;
            }

            // Empty display text
            if (linkText[0] == '|' && linkText[1] == 'h') {
                return CODE_INVALIDCODE;
            }

            advance = linkText - text;

            return CODE_HYPERLINKSTART;
        }

        case 'N':
        case 'n': {
            if (flags & FLAG_IGNORE_NEWLINES) {
                return CODE_INVALIDCODE;
            }

            advance = 2;

            return CODE_NEWLINE;
        }

        case 'R':
        case 'r': {
            if (flags & FLAG_IGNORE_COLORS) {
                return CODE_INVALIDCODE;
            }

            advance = 2;

            return CODE_COLORRESTORE;
        }

        case 'T': {
            if (flags & FLAG_IGNORE_TEXTURES) {
                return CODE_INVALIDCODE;
            }

            auto textureText = text + advance;

            while (*textureText) {
                auto code = SUniSGetUTF8(reinterpret_cast<const uint8_t*>(textureText), &advance);
                textureText += advance;

                if (code == '|') {
                    break;
                }
            }

            if (!*textureText) {
                return CODE_INVALIDCODE;
            }

            auto endCode = SUniSGetUTF8(reinterpret_cast<const uint8_t*>(textureText), &advance);
            textureText += advance;

            // Null terminator or end code isn't |t
            if (!*textureText || endCode != 't') {
                return CODE_INVALIDCODE;
            }

            // Empty texture (no payload)
            if (textureText - text == 4) {
                return CODE_INVALIDCODE;
            }

            advance = textureText - text;

            return CODE_TEXTURESTART;
        }

        case 'h': {
            if (flags & FLAG_IGNORE_HYPERLINKS) {
                return CODE_INVALIDCODE;
            }

            advance = 2;

            return CODE_HYPERLINKSTOP;
        }

        case 't': {
            if (flags & FLAG_IGNORE_TEXTURES) {
                return CODE_INVALIDCODE;
            }

            advance = 2;

            return CODE_TEXTURESTOP;
        }

        case '|': {
            advance = 2;

            return CODE_PIPE;
        }

        default: {
            return CODE_INVALIDCODE;
        }
    }
}

int32_t GxuFontAddToBatch(CGxStringBatch* batch, CGxString* string) {
    if (batch && string) {
        batch->AddString(string);
        return 1;
    } else {
        return 0;
    }
}

void GxuFontAddShadow(CGxString* string, const CImVector& color, const C2Vector& offset) {
    if (string) {
        if (!(string->m_flags & 0x80)) {
            string->AddShadow(offset, color);
        }
    }
}

CGxStringBatch* GxuFontCreateBatch(bool a1, bool a2) {
    CGxStringBatch* batch;

    if (s_unusedBatches.Head()) {
        batch = s_unusedBatches.Head();
        s_unusedBatches.UnlinkNode(batch);
    } else {
        auto m = SMemAlloc(sizeof(CGxStringBatch), __FILE__, __LINE__, 0x8);
        batch = new (m) CGxStringBatch();
    }

    if (a1) {
        batch->m_flags |= 0x1;
    } else {
        batch->m_flags &= ~0x1;
    }

    if (a2) {
        batch->m_flags |= 0x2;
    } else {
        batch->m_flags &= ~0x2;
    }

    return batch;
}

int32_t GxuFontCreateFont(const char* name, float fontHeight, CGxFont*& face, uint32_t flags) {
    STORM_ASSERT(name);
    STORM_ASSERT(*name);
    STORM_ASSERT((fontHeight <= 1.0f) && (fontHeight > 0.0f));

    CGxFont* newFace = g_fonts.NewNode(2, 0, 0);

    uint32_t v12 = flags;

    if (flags & 0x8) {
        v12 |= 0x1;
    }

    int32_t v13 = newFace->Initialize(name, v12, fontHeight);

    if (!v13) {
        g_fonts.DeleteNode(newFace);
        newFace = nullptr;
    }

    face = newFace;

    return v13;
}

int32_t GxuFontCreateString(CGxFont* face, const char* text, float fontHeight, const C3Vector& position, float blockWidth, float blockHeight, float spacing, CGxString*& string, EGxFontVJusts vertJustification, EGxFontHJusts horzJustification, uint32_t flags, const CImVector& color, float charSpacing, float scale) {
    STORM_VALIDATE_BEGIN;
    STORM_VALIDATE(face);
    STORM_VALIDATE(text);
    STORM_VALIDATE(fontHeight || (flags & EGxStringFlags_FixedSize));
    STORM_VALIDATE(vertJustification < GxVJ_Last);
    STORM_VALIDATE(horzJustification < GxHJ_Last);
    STORM_VALIDATE_END;

    auto newString = CGxString::GetNewString(1);

    int32_t result = newString->Initialize(
        fontHeight,
        position,
        blockWidth,
        blockHeight,
        face,
        text,
        vertJustification,
        horzJustification,
        spacing,
        flags & ~0x1,
        color,
        scale
    );

    if (result) {
        string = newString;
    } else {
        GxuFontDestroyString(newString);
    }

    return result;
}

int32_t GxuFontDestroyBatch(CGxStringBatch* batch) {
    if (!batch) {
        return 0;
    }

    batch->m_fontBatch.Clear();

    s_unusedBatches.LinkToTail(batch);

    return 1;
}

void GxuFontDestroyFont(CGxFont*& font) {
    if (font) {
        g_fonts.DeleteNode(font);
    }

    font = nullptr;
}

void GxuFontDestroyString(CGxString*& string) {
    if (string) {
        string->Unlink();
        string->Recycle();
        string = nullptr;
    }
}

uint32_t GxuFontGetFontFlags(CGxFont* font) {
    if (font) {
        return font->m_flags;
    }

    return 0;
}

const char* GxuFontGetFontName(CGxFont* font) {
    return font
        ? font->GetName()
        : nullptr;
}

uint32_t GxuFontGetMaxCharsWithinWidth(CGxFont* font, const char* text, float height, float maxWidth, uint32_t lineBytes, float* extent, float a7, float scale, float a9, uint32_t flags) {
    return InternalGetMaxCharsWithinWidth(
        font,
        text,
        height,
        maxWidth,
        lineBytes,
        extent,
        flags,
        a7,
        scale,
        nullptr,
        nullptr,
        nullptr
    );
}

uint32_t GxuFontGetMaxCharsWithinWidthAndHeight(CGxFont* font, const char* text, float fontHeight, float maxWidth, float maxHeight, uint32_t lineBytes, float a7, float scale, float a9, uint32_t flags) {
    if (!font) {
        return 0;
    }

    if (!text || !*text) {
        return 0;
    }

    if (fontHeight == 0.0f || maxWidth == 0.0f || maxHeight == 0.0f || g_heightPixels == 0.0f) {
        return 0;
    }

    if (lineBytes == 0) {
        return 0;
    }

    if (flags & 0x4) {
        fontHeight = GxuFontGetOneToOneHeight(font);
    }

    auto v24 = static_cast<float>(g_heightPixels) * a9;
    auto v22 = CMath::fuint_pi(v24);
    auto v12 = static_cast<float>(v22) / static_cast<float>(g_heightPixels);
    bool v26 = true;
    auto v27 = v12;
    v24 = 0.0f;
    auto v13 = fontHeight;
    const char* nextText;

    uint32_t maxChars = 0;

    auto currentText = text;
    int32_t v25 = 0;
    while (*currentText) {
        if (flags & 0x2000 && v25 == 1) {
            maxWidth -= g_indentNormWidth;
        }

        float v21;
        CalcWrapPoint(
            font,
            currentText,
            fontHeight,
            maxWidth,
            &v22,
            &v21,
            &nextText,
            a7,
            flags,
            &v26,
            0,
            scale
        );

        v22 = nextText - currentText;
        if (nextText == currentText) {
            break;
        }

        auto v15 = v24;
        auto v16 = v25 + 1 < 0;
        auto v17 = v24 + fontHeight;

        v25++;

        float v18 = v25;

        if (v16) {
            v18 = v18 + 4294967300.0;
        }

        if (v18 * 0.00000095367431640625 + maxHeight < v17) {
            break;
        }

        maxChars += nextText - currentText;
        currentText = nextText;
        v24 = fontHeight + v15 + v27;

        if (!v26 || !nextText) {
            break;
        }
    }

    return maxChars;
}

float GxuFontGetOneToOneHeight(CGxFont* font) {
    STORM_ASSERT(font);

    return PixelToScreenHeight(font->GetPixelSize());
}

void GxuFontGetTextExtent(CGxFont* font, const char* text, uint32_t numBytes, float height, float* extent, float a6, float scale, float a8, uint32_t flags) {
    InternalGetTextExtent(font, text, numBytes, height, extent, flags, a6, scale);
}

float GxuFontGetWrappedTextHeight(CGxFont* font, const char* text, float a3, float a4, const C2Vector& a5, float a6, float a7, uint32_t flags) {
    STORM_ASSERT(font);
    STORM_ASSERT(text);

    if (flags & 0x04) {
        a3 = GxuFontGetOneToOneHeight(font);
    }

    int32_t advance;
    uint32_t numBytes;
    uint32_t code;
    int32_t numLines = 0;
    float extent = 0.0f;
    float v17 = 0.0f;
    float v18 = 0.0f;
    const char* currentText = text;
    const char* nextText = nullptr;
    bool v21 = true;

    while (currentText && *currentText) {
        QUOTEDCODE quotedCode = GxuDetermineQuotedCode(currentText, advance, nullptr, flags, code);

        if (flags & 0x2000 && numLines == 1) {
            a4 = a4 - g_indentNormWidth;
        }

        if (quotedCode == CODE_NEWLINE) {
            nextText = currentText + advance;
        } else {
            CalcWrapPoint(
                font,
                currentText,
                a3,
                a4,
                &numBytes,
                &extent,
                &nextText,
                a5.x,
                flags,
                &v21,
                &v17,
                a6
            );

            if (currentText == nextText) {
                break;
            }
        }

        numLines++;
        currentText = nextText;

        float v14 = (float)GetScreenPixelHeight() * a3;
        if (v14 < v17) {
            v18 = v17 - v14 + v18;
        }

        if (flags & 0x02) {
            break;
        }
    }

    if (!(flags & 0x02) && currentText > text && GxuDetermineQuotedCode(currentText - 1, advance, nullptr, flags, code) == CODE_NEWLINE) {
        numLines++;
    }

    if (!numLines) {
        return 0.0f;
    }

    float v22 = GetScreenPixelHeight();
    float v14 = v22 * a7;
    auto v13 = CMath::fuint_pi(v14);
    auto v23 = static_cast<float>(v13) / v22;

    return (v18 / v22 + v23 * (float)(numLines - 1) + (float)numLines * a3);
}

void GxuFontInitialize() {
    g_theGxDevicePtr->ShaderCreate(g_fontVertexShader, GxSh_Vertex, "Shaders\\Vertex", "UI", 2);
    g_theGxDevicePtr->ShaderCreate(g_fontPixelShader, GxSh_Pixel, "Shaders\\Pixel", "UI", 1);

    BATCHEDRENDERFONTDESC::Initialize();

    FreeTypeInitialize();

    GxuFontWindowSizeChanged();

    // TODO
    // sub_6BD160();
}

void GxuFontRenderBatch(CGxStringBatch* batch) {
    if (batch) {
        batch->RenderBatch();
    }
}

int32_t GxuFontSetStringColor(CGxString* string, const CImVector& color) {
    STORM_ASSERT(string);

    string->SetColor(color);

    return 1;
}

void GxuFontSetStringPosition(CGxString* string, const C3Vector& position) {
    STORM_ASSERT(string);

    string->SetStringPosition(position);
}

void GxuFontUpdate() {
    for (auto string = g_strings.Head(); string; string = g_strings.Link(string)->Next()) {
        string->Tick();
    }

    // TODO
}

void GxuFontWindowSizeChanged() {
    static CRect s_currentRect = { 0.0f, 0.0f, 0.0f, 0.0f };

    CRect rect = { 0.0f, 0.0f, 0.0f, 0.0f };

    GxCapsWindowSize(rect);

    if (rect.maxY - rect.minY == 0.0f || rect.maxX - rect.minX == 0.0f) {
        rect = { 0.0f, 0.0f, 480.0f, 640.0f };
    }

    if (s_currentRect == rect) {
        return;
    }

    s_currentRect = rect;

    g_widthPixels = rect.maxX - rect.minX;
    g_heightPixels = rect.maxY - rect.minY;
    g_indentPixelWidth = 15.0f;
    g_indentNormWidth = 15.0f / (rect.maxX - rect.minX);

    // TODO
    // - walk s_fonts and trigger HandleScreenSizeChange
}
