#include "GdtStream.h"

#include "Utils/Logging/Log.h"

#include <iostream>
#include <sstream>

class GdtConst
{
public:
    static constexpr const char* VERSION_ENTRY_NAME = "version";
    static constexpr const char* VERSION_ENTRY_GDF = "version.gdf";
    static constexpr const char* VERSION_KEY_GAME = "game";
    static constexpr const char* VERSION_KEY_VERSION = "version";
};

void GdtReader::PrintError(const std::string& message) const
{
    con::error("GDT Error at line {}: {}", m_line, message);
}

int GdtReader::PeekChar()
{
    if (m_peeked)
        return m_char;

    int c;
    do
    {
        c = m_stream.get();
    } while (isspace(c));

    m_peeked = true;
    m_char = c;
    return c;
}

int GdtReader::NextChar()
{
    if (m_peeked)
    {
        m_peeked = false;
        return m_char;
    }

    int c;
    do
    {
        c = m_stream.get();
    } while (isspace(c));

    return c;
}

bool GdtReader::ReadStringContent(std::string& str)
{
    std::ostringstream ss;

    if (NextChar() != '"')
    {
        PrintError("Expected string opening tag");
        return false;
    }

    auto c = m_stream.get();
    auto escaped = false;
    while ((escaped || c != '"' && c != '\n') && c != EOF)
    {
        if (escaped)
        {
            switch (c)
            {
            case '\n':
            case 'n':
                ss << '\n';
                break;

            case 'r':
                ss << '\r';
                break;

            default:
                ss << static_cast<char>(c);
                break;
            }
            escaped = false;
        }
        else if (c == '\\')
        {
            escaped = true;
        }
        else
        {
            ss << static_cast<char>(c);
        }
        c = m_stream.get();
    }

    if (c == '"')
    {
        str = ss.str();
        return true;
    }

    return false;
}

GdtEntry* GdtReader::GetEntryByName(const Gdt& gdt, const std::string& name)
{
    for (const auto& entry : gdt.m_entries)
    {
        if (entry->m_name == name)
            return entry.get();
    }

    return nullptr;
}

bool GdtReader::ReadProperties(GdtEntry& entry)
{
    while (PeekChar() == '"')
    {
        std::string propertyKey;
        std::string propertyValue;

        if (!ReadStringContent(propertyKey))
            return false;

        if (PeekChar() != '"' || !ReadStringContent(propertyValue))
        {
            PrintError("Expected value string");
            return false;
        }

        entry.m_properties.emplace(std::move(propertyKey), std::move(propertyValue));
    }

    if (NextChar() != '}')
    {
        PrintError("Expected closing tags");
        return false;
    }

    return true;
}

bool GdtReader::AddEntry(Gdt& gdt, GdtEntry& entry) const
{
    if (entry.m_name == GdtConst::VERSION_ENTRY_NAME && entry.m_gdf_name == GdtConst::VERSION_ENTRY_GDF)
    {
        auto foundEntry = entry.m_properties.find(GdtConst::VERSION_KEY_GAME);
        if (foundEntry == entry.m_properties.end())
        {
            PrintError("Version does not feature game property");
            return false;
        }
        gdt.m_version.m_game = foundEntry->second;

        foundEntry = entry.m_properties.find(GdtConst::VERSION_KEY_VERSION);
        if (foundEntry == entry.m_properties.end())
        {
            PrintError("Version does not feature version property");
            return false;
        }
        gdt.m_version.m_version = strtol(foundEntry->second.c_str(), nullptr, 0);
    }
    else
    {
        gdt.m_entries.emplace_back(std::make_unique<GdtEntry>(std::move(entry)));
    }

    return true;
}

GdtReader::GdtReader(std::istream& stream)
    : m_stream(stream),
      m_char(0),
      m_peeked(false),
      m_line(0)
{
}

bool GdtReader::Read(Gdt& gdt)
{
    if (NextChar() != '{')
    {
        PrintError("Expected opening tag");
        return false;
    }

    while (PeekChar() == '"')
    {
        GdtEntry entry;

        if (!ReadStringContent(entry.m_name))
        {
            PrintError("Failed to read string");
            return false;
        }

        if (PeekChar() == '(')
        {
            NextChar();
            if (!ReadStringContent(entry.m_gdf_name))
            {
                PrintError("Expected gdf name string");
                return false;
            }
            if (NextChar() != ')')
            {
                PrintError("Expected closing parenthesis");
                return false;
            }
        }
        else if (PeekChar() == '[')
        {
            NextChar();
            std::string parentName;
            if (!ReadStringContent(parentName))
            {
                PrintError("Expected parent name string");
                return false;
            }
            if (NextChar() != ']')
            {
                PrintError("Expected closing square brackets");
                return false;
            }
            entry.m_parent = GetEntryByName(gdt, parentName);
            if (entry.m_parent == nullptr)
            {
                PrintError("Could not find parent with name");
                return false;
            }
            auto* currentParentEntry = entry.m_parent;
            while (currentParentEntry->m_parent)
                currentParentEntry = currentParentEntry->m_parent;
            entry.m_gdf_name = currentParentEntry->m_gdf_name;
        }
        else
        {
            PrintError("Expected gdf or parent opening");
            return false;
        }

        if (NextChar() != '{')
        {
            PrintError("Expected opening tag for entries");
            return false;
        }

        if (!ReadProperties(entry))
            return false;

        if (!AddEntry(gdt, entry))
            return false;
    }

    if (NextChar() != '}')
    {
        PrintError("Expected closing tags");
        return false;
    }

    return true;
}

GdtOutputStream::GdtOutputStream(std::ostream& stream)
    : m_stream(stream),
      m_open(false),
      m_intendation_level(0)
{
}

void GdtOutputStream::BeginStream()
{
    if (!m_open)
    {
        m_stream << "{\n";
        m_intendation_level++;
        m_open = true;
    }
}

void GdtOutputStream::DoIntendation() const
{
    for (auto i = 0u; i < m_intendation_level; i++)
        m_stream << "\t";
}

void GdtOutputStream::WriteVersion(const GdtVersion& gdtVersion)
{
    GdtEntry versionEntry;
    versionEntry.m_name = GdtConst::VERSION_ENTRY_NAME;
    versionEntry.m_gdf_name = GdtConst::VERSION_ENTRY_GDF;
    versionEntry.m_properties[GdtConst::VERSION_KEY_GAME] = gdtVersion.m_game;
    versionEntry.m_properties[GdtConst::VERSION_KEY_VERSION] = std::to_string(gdtVersion.m_version);

    WriteEntry(versionEntry);
}

void GdtOutputStream::WriteEscaped(const std::string& str) const
{
    auto wroteBefore = false;
    for (auto i = 0u; i < str.size(); i++)
    {
        auto needsEscape = false;
        auto c = str[i];
        switch (c)
        {
        case '\r':
            needsEscape = true;
            c = 'r';
            break;

        case '\n':
            needsEscape = true;
            c = 'n';
            break;

        case '\\':
            needsEscape = true;
            break;

        default:
            break;
        }

        if (needsEscape)
        {
            if (!wroteBefore)
            {
                wroteBefore = true;
                m_stream << std::string(str, 0, i);
            }

            m_stream << '\\' << c;
        }
        else if (wroteBefore)
        {
            m_stream << c;
        }
    }

    if (!wroteBefore)
    {
        m_stream << str;
    }
}

void GdtOutputStream::WriteEntry(const GdtEntry& entry)
{
    DoIntendation();
    m_stream << "\"" << entry.m_name << "\" ";
    if (entry.m_parent)
        m_stream << "[ \"" << entry.m_parent->m_name << "\" ]\n";
    else
        m_stream << "( \"" << entry.m_gdf_name << "\" )\n";
    DoIntendation();
    m_stream << "{\n";

    m_intendation_level++;

    for (const auto& [propertyKey, propertyValue] : entry.m_properties)
    {
        DoIntendation();
        m_stream << "\"" << propertyKey << "\" \"";
        WriteEscaped(propertyValue);
        m_stream << "\"\n";
    }

    m_intendation_level--;
    DoIntendation();
    m_stream << "}\n";
}

void GdtOutputStream::EndStream()
{
    if (m_open)
    {
        m_intendation_level--;
        m_stream << "}";
        m_open = false;
    }
}

void GdtOutputStream::WriteGdt(const Gdt& gdt, std::ostream& stream)
{
    GdtOutputStream out(stream);
    out.BeginStream();
    out.WriteVersion(gdt.m_version);

    for (const auto& entry : gdt.m_entries)
        out.WriteEntry(*entry);

    out.EndStream();
}
