#pragma once
#include "../game/Log.h"

#include "Generated.h"
#include "bx/string.h"
#include <bx/file.h>
#include <cassert>
#include <cstdint>

namespace Gen
{
    struct EmbeddedTypeTableBuffer
    {
        uint16_t DefCount = 0;
        Meta::TypeDef Defs[64];

        void AddDef(const Meta::TypeDef& def)
        {
            if (DefCount >= BX_COUNTOF(Defs))
            {
                LOG_WARN("Ran out of type definition space!");
                return;
            }
            for (int32_t i = 0; i < DefCount; ++i)
            {
                if (Defs[i].Hash == def.Hash) return;
            }
            Defs[DefCount] = def;
            ++DefCount;
        }

        int32_t FindHash(uint32_t hash)
        {
            for (int32_t i = 0; i < DefCount; ++i)
            {
                if (Defs[i].Hash == hash)
                {
                    return i;
                }
            }
            return -1;
        }

        const int32_t FindDefByName(const char* name)
        {
            for (int32_t i = 0; i < DefCount; ++i)
            {
                if (bx::strCmp(name, Defs[i].Name) == 0)
                {
                    return i;
                }
            }
            return -1;
        }

        void PatchChildIndices()
        {
            for (int32_t i = 0; i < DefCount; ++i)
            {
                for (int32_t j = 0; j < Defs[i].ChildCount; ++j)
                {
                    int32_t idx = FindHash(Meta::Metadata.TypeDefinitions[Defs[i].ChildIndices[j]].Hash);
                    assert(idx >= 0);
                    Defs[i].ChildIndices[j] = idx;
                }
            }
        }
    };

    struct EmbeddedTypeInfoHeader
    {
        static constexpr uint32_t CurrentHeaderVersion = 1;
        uint32_t HeaderVersion = CurrentHeaderVersion;
        uint32_t BaseTypeHash = 0;
        uint16_t BaseTypeIdx = 0;
        uint16_t TypeCount = 0;
        uint32_t TypeDataSize = 0;
        uint32_t MemberDataSize = 0;
    };

    struct Serializer
    {
        bx::Error Err;
        bx::FilePath Path;
        bx::FileWriter Writer;

        bool Init(const bx::FilePath& path, const char* _4cc)
        {
            if (_4cc == nullptr)
            {
                LOG_ERROR("Provided invalid 4cc!");
                return false;
            }
            if (path.isEmpty())
            {
                LOG_ERROR("Provided empty path!");
                return false;
            }
            Path = path;
            if (!Writer.open(path, false, &Err))
            {
                LOG_ERROR("Failed to open file %s: %s", path.getCPtr(), Err.getMessage().getCPtr());
                return false;
            }
            Writer.write(_4cc, 4, &Err);
            if (!Err.isOk())
            {
                LOG_ERROR("Failed to write to file %s: %s", path.getCPtr(), Err.getMessage().getCPtr());
            }
            return Err.isOk();
        }

        template <typename T> bool WriteT(const T& data)
        {
            const Meta::TypeDef& baseDef = Meta::Metadata.TypeDefinitions[data.TypeIdx];

            EmbeddedTypeTableBuffer typeBuf;
            typeBuf.AddDef(baseDef);

            // DefCount updates dynamically here!
            for (int32_t i = 0; i < typeBuf.DefCount; ++i)
            {
                const Meta::TypeDef& def = typeBuf.Defs[i];
                for (int32_t j = 0; j < def.ChildCount; ++j)
                {
                    typeBuf.AddDef(Meta::Metadata.TypeDefinitions[def.ChildIndices[j]]);
                }
            }
            typeBuf.PatchChildIndices();

            EmbeddedTypeInfoHeader header;
            header.BaseTypeHash = baseDef.Hash;
            header.BaseTypeIdx = 0;
            header.TypeCount = typeBuf.DefCount;
            header.TypeDataSize = header.TypeCount * sizeof(Meta::TypeDef);
            header.MemberDataSize = bx::strLen(Meta::Metadata.MemberNameBuffer);

            if (!Write(&header, sizeof(header))) return false;
            for (int32_t i = 0; i < typeBuf.DefCount; ++i)
            {
                if (!Write(&typeBuf.Defs[i], sizeof(typeBuf.Defs[i]))) return false;
            }
            Write(Meta::Metadata.MemberNameBuffer, header.MemberDataSize);

            if (!Save(&data, 1, *this))
            {
                LOG_ERROR("Failed to save!");
                return false;
            }
            return true;
        }

        bool Write(const void* data, uint32_t size)
        {
            Writer.write(data, size, &Err);
            if (!Err.isOk()) LOG_ERROR("Write error: %s", Err.getMessage().getCPtr());
            return Err.isOk();
        }

        void Finish()
        {
            Writer.close();
        }

        ~Serializer()
        {
            Writer.close();
        }
    };

    struct Deserializer
    {
        bx::Error Err;
        bx::FilePath Path;
        bx::FileReader Reader;

        char MemberNameBuf[64 * 64 * 64]{0};
        EmbeddedTypeTableBuffer TypeBuf;

        bool Init(const bx::FilePath& path, const char* _4cc)
        {
            Path = path;
            if (!Reader.open(path, &Err))
            {
                LOG_ERROR("Failed to open file %s: %s", path.getCPtr(), Err.getMessage().getCPtr());
                return false;
            }
            char read4cc[4]{0};
            int32_t readAmount = Reader.read(read4cc, 4, &Err);
            if (!Err.isOk())
            {
                LOG_ERROR("Failed to read from file %s: %s", path.getCPtr(), Err.getMessage().getCPtr());
                return false;
            }
            if (readAmount < 4)
            {
                LOG_ERROR("Failed to read enough bytes from file %s", path.getCPtr());
                return false;
            }
            if (read4cc[0] != _4cc[0] || read4cc[1] != _4cc[1] || read4cc[2] != _4cc[2] || read4cc[3] != _4cc[3])
            {
                LOG_ERROR("4CC mismatch! %.4s != %.4s", read4cc, _4cc);
                return false;
            }
            return true;
        }

        bool Read(void* data, uint32_t size)
        {
            Reader.read(data, size, &Err);
            if (!Err.isOk()) LOG_ERROR("Read error: %s", Err.getMessage().getCPtr());
            return Err.isOk();
        }

        template <typename T> bool ReadT(T& data)
        {
            EmbeddedTypeInfoHeader header;
            if (!Read(&header, sizeof(header))) return false;

            if (header.HeaderVersion != EmbeddedTypeInfoHeader::CurrentHeaderVersion)
            {
                // TODO: upgrade
                LOG_ERROR("Header version mismatch: %u != %u",
                          header.HeaderVersion,
                          EmbeddedTypeInfoHeader::CurrentHeaderVersion);
                return false;
            }

            const Meta::TypeDef& baseTypeDef = Meta::Metadata.TypeDefinitions[data.TypeIdx];
            if (header.BaseTypeHash != baseTypeDef.Hash)
            {
                LOG_WARN("Hash mismatch! %u != %u", header.BaseTypeHash, baseTypeDef.Hash);
            }

            TypeBuf.DefCount = header.TypeCount;
            for (int32_t i = 0; i < TypeBuf.DefCount; ++i)
            {
                Read(&TypeBuf.Defs[i], sizeof(TypeBuf.Defs[i]));
            }

            Read(MemberNameBuf, header.MemberDataSize);

            if (!Load(&data, 1, *this))
            {
                LOG_ERROR("Failed to load: %s", Err.getMessage().getCPtr());
                return false;
            }

            return true;
        }

        void Skip(uint32_t byteCount)
        {
            Reader.seek(byteCount);
        }

        void Finish()
        {
            Reader.close();
        }

        ~Deserializer()
        {
            Reader.close();
        }
    };

    inline uint32_t FindNameInMetadata(const Meta::MetadataTable& meta, const char* name)
    {
        for (int32_t i = 0; i < BX_COUNTOF(meta.TypeDefinitions); ++i)
        {
            if (bx::strCmp(meta.TypeDefinitions[i].Name, name) == 0)
            {
                return i;
            }
        }
        return -1;
    }

    template <typename T> bool Save(const T* obj, uint32_t count, Serializer& serializer);
    template <typename T> bool Load(T* obj, uint32_t count, Deserializer& serializer);
} // namespace Gen
