#include <AssetManager/AssetManager.hpp>
#include <Font/Font.hpp>
#include <Log/Log.hpp>
#include <Material/Material.hpp>
#include <Utilities/Utilities.hpp>
#include <atomic>
#include <fstream>
#include <memory>
#include <regex>
#include <sstream>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Vk/ShaderConst.hpp>

#define DECL_FONT(name) Font Font::name
#include <Font/FontList.hpp>

int Font::tryParseSheetAttr(const std::string &attr)
{
    if (attr == "fixedColor")
    {
        return GLSL_MAT_FIXED_COLOR;
    }
    else
    {
        return 0;
    }
}

bool Font::tryParseRangeStatement(const std::string &line)
{
    std::regex pattern(R"(range ([0-9a-fA-F]+)\.\.([0-9a-fA-F]+) at (\d+),(\d+) width=(\d+))");
    std::smatch match;

    if (!std::regex_search(line, match, pattern))
    {
        return false;
    }

    try
    {
        uint64_t baseCodePoint = std::stoull(match[1], nullptr, 16);
        uint64_t lastCodePoint = std::stoull(match[2], nullptr, 16);

        glm::ivec2 sheetBase(std::stoi(match[3]), std::stoi(match[4]));
        int glyphWidth = std::stoi(match[5]);

        if (currentSheet == nullptr)
        {
            crash("Glyph range defined before a sheet was selected.");
        }

        for (uint64_t codepoint = baseCodePoint; codepoint <= lastCodePoint; codepoint++)
        {
            Glyph &glyph = codepointGlyphs[codepoint];
            glyph.sheet = currentSheet;
            glyph.sheetBase = sheetBase;
            glyph.width = glyphWidth;

            sheetBase.x += glyphWidth;
        }
    }
    catch (const std::exception &e)
    {
        crash(formatString("Invalid values in this line: '$+'", line));
    }

    return true;
}

bool Font::tryParseGlyphHeightStatement(const std::string &line)
{
    std::regex pattern(R"(glyphHeight (\d+))");
    std::smatch match;

    if (!std::regex_search(line, match, pattern))
    {
        return false;
    }

    try
    {
        glyphHeight = std::stoi(match[1]);
    }
    catch (const std::exception &e)
    {
        crash(formatString("Invalid glyph height: '$+'", line));
    }

    return true;
}

bool Font::tryParseIncludeStatement(const std::string &line)
{
    std::regex pattern(R"(include (\w+))");
    std::smatch match;

    if (!std::regex_search(line, match, pattern))
    {
        return false;
    }

    std::string moduleName;
    try
    {
        moduleName = match[1];
    }
    catch (const std::exception &e)
    {
        crash(formatString("Invalid include statement: '$+'", line));
    }

    if (includedModules.contains(moduleName))
    {
        crash(formatString("Module '$+' included multiple times!", moduleName));
    }

    includedModules.insert(moduleName);

    std::string modulePath = assets.resolve(formatString("fonts/$+/$+.fdef", this->name, moduleName));
    includeDefFile(modulePath);

    return true;
}

bool Font::tryParseSheetStatement(const std::string &line)
{
    std::istringstream iss(line);
    std::string token;

    if (!(iss >> token))
    {
        return false;
    }

    if (token != "sheet")
    {
        return false;
    }

    std::string sheetName;
    if (!(iss >> sheetName))
    {
        return false;
    }

    auto &sheet = sheets[sheetName];
    if (sheet != nullptr)
    {
        crash(formatString("Multiple definitions of sheet '$+'.", sheetName));
    }

    sheet = std::make_unique<Sheet>();
    currentSheet = sheet.get();

    while (iss >> token)
    {
        int attr = tryParseSheetAttr(token);
        if (attr == 0)
        {
            crash(formatString("Invalid sheet attribute: '$+'.", token));
        }

        sheet->mat.flags |= attr;
    }

    return true;
}

void Font::includeDefFile(const std::string &defFilePath)
{
    std::ifstream ifs(defFilePath);
    if (!ifs.good())
    {
        crash(formatString("Failed to open font definition file '$+'", defFilePath));
    }

    std::string line;
    while (std::getline(ifs, line))
    {
        if (line.size() == 0)
        {
            // Ignore empty lines.
            continue;
        }

        if (line[0] == '#')
        {
            // Ignore comment lines.
            continue;
        }

        if (tryParseGlyphHeightStatement(line))
        {
            // Parsed a 'glyphHeight' statement.
            continue;
        }

        if (tryParseRangeStatement(line))
        {
            // Parsed a 'range' statement.
            continue;
        }

        if (tryParseIncludeStatement(line))
        {
            // Parsed an 'include' statement.
            continue;
        }

        if (tryParseSheetStatement(line))
        {
            // Parsed a 'sheet' statement.
            continue;
        }

        crash(formatString("Failed to parse the following line in '$+': '$+'", defFilePath, line));
    }
    
    ifs.close();
}

void Font::load(const char *name, std::atomic<unsigned int> &tasksDone)
{
    static std::atomic<unsigned int> dummyTaskCounter;

    Log::info(formatString("Loading font '$+'...", name));

    this->name = name;

    std::string defFilePath = assets.resolve(formatString("fonts/$+/$+.def", name, name));

    includeDefFile(defFilePath);

    for (auto &[sheetName, sheet] : sheets)
    {
        std::string sheetFilePath = assets.resolve(formatString("fonts/$+/$+.png", name, sheetName));

        std::optional<Bitmap> sheetOpt = Bitmap::fromFile(sheetFilePath);
        if (!sheetOpt.has_value())
        {
            crash(formatString("Failed to read font sheet bitmap from '$+'", sheetFilePath));
        }

        sheet->bitmap = *sheetOpt;
        sheet->mat.load(sheetFilePath, dummyTaskCounter);
    }

    GraphicsContext::onNextFrame([&tasksDone](GraphicsContext &)
    {
        tasksDone++;
    });
}