#include "Global.h"
#include "Log.h"
#include "Mesh.h"
#include "bx/bx.h"
#include "bx/error.h"
#include "bx/file.h"
#include "bx/filepath.h"
#include "bx/hash.h"
#include "bx/string.h"
#include "rendering/Rendering.h"

#include "Instance.h"

#define TINYGLTF_IMPLEMENTATION
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../dependency/tinygltf/tiny_gltf.h"

namespace Game
{
    bool LoadMesh(Model& mesh, const char* path, bool isBinary)
    {
        mesh.VertLayout.begin()
            .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
            .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float)
            .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Uint8, true)
            .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
            .end();

        tinygltf::Model model;
        tinygltf::TinyGLTF loader;
        std::string warn;
        std::string err;

        bool loadSuccess;
        if (isBinary)
        {
            loadSuccess = loader.LoadBinaryFromFile(&model, &err, &warn, path);
        }
        else
        {
            loadSuccess = loader.LoadASCIIFromFile(&model, &err, &warn, path);
        }

        if (!warn.empty()) LOG_WARN("WARN: %s", warn.c_str());
        if (!err.empty()) LOG_ERROR("ERR: %s", err.c_str());
        if (!loadSuccess)
        {
            LOG_ERROR("Model load failed!");
            return false;
        }
        if (model.meshes.empty() || model.meshes[0].primitives.empty())
        {
            LOG_ERROR("Model is empty!");
            return false;
        }

        tinygltf::Primitive primitive = model.meshes[0].primitives[0];
        {
            tinygltf::Accessor accessor = model.accessors.at(primitive.indices);
            tinygltf::BufferView bufferView = model.bufferViews.at(accessor.bufferView);
            tinygltf::Buffer buffer = model.buffers[bufferView.buffer];
            const bgfx::Memory* ibMem = bgfx::alloc(bufferView.byteLength);
            bx::memCopy(ibMem->data, &buffer.data.at(bufferView.byteOffset), bufferView.byteLength);
            mesh.IndexBuffer = bgfx::createIndexBuffer(ibMem);
        }
        {
            tinygltf::Accessor posAccessor = model.accessors.at(primitive.attributes.at("POSITION"));
            tinygltf::Accessor normalAccessor = model.accessors.at(primitive.attributes.at("NORMAL"));
            tinygltf::Accessor uvAccessor = model.accessors.at(primitive.attributes.at("TEXCOORD_0"));
            tinygltf::BufferView posBufferView = model.bufferViews[posAccessor.bufferView];
            tinygltf::BufferView normalBufferView = model.bufferViews[normalAccessor.bufferView];
            tinygltf::BufferView uvBufferView = model.bufferViews[uvAccessor.bufferView];
            int posStride = posAccessor.ByteStride(posBufferView);
            int normalStride = normalAccessor.ByteStride(normalBufferView);
            int uvStride = uvAccessor.ByteStride(uvBufferView);
            tinygltf::Buffer posBuffer = model.buffers[posBufferView.buffer];
            tinygltf::Buffer normalBuffer = model.buffers[normalBufferView.buffer];
            tinygltf::Buffer uvBuffer = model.buffers[uvBufferView.buffer];

            uint32_t vertexCount = posBufferView.byteLength / posStride;
            const bgfx::Memory* vbMem = bgfx::alloc(vertexCount * sizeof(PosColorVertex));

            for (uint32_t i = 0; i < vertexCount; ++i)
            {
                PosColorVertex& v = *reinterpret_cast<PosColorVertex*>(vbMem->data + i * sizeof(PosColorVertex));
                bx::memCopy(&v.x, &posBuffer.data.at(posBufferView.byteOffset + i * posStride), posStride);
                bx::memCopy(
                    &v.n_x, &normalBuffer.data.at(normalBufferView.byteOffset + i * normalStride), normalStride);
                v.abgr = 0;
                bx::memCopy(&v.uv_x, &uvBuffer.data.at(uvBufferView.byteOffset + i * uvStride), uvStride);
            }
            mesh.VertexBuffer = bgfx::createVertexBuffer(vbMem, mesh.VertLayout);
        }
        return true;
    }

    void LoadModels(Model* models, uint32_t& outCount)
    {
        bx::Error err;
        bx::DirectoryReader reader{};
        if (!reader.open("models", &err) || !err.isOk())
        {
            LOG_ERROR("Failed to read models dir: %s", err.getMessage());
        }
        bx::FileInfo info;
        int32_t modelFilePathCount = 0;
        bx::FilePath modelFilePaths[GameRendering::MaxModels];
        bool modelFileIsBinary[GameRendering::MaxModels]{false};
        while (err.isOk())
        {
            int32_t res = reader.read(&info, sizeof(info), &err);
            if (res == 0) break; // EOF
            if (res != sizeof(info))
            {
                LOG_ERROR("Dir iter error: %s", err.getMessage());
                break;
            }
            const bx::StringView ext = info.filePath.getExt();
            bool isBinary = bx::strCmp(ext, ".glb") == 0;
            bool isText = bx::strCmp(ext, ".gltf") == 0;
            if ((isBinary || isText) && !ext.isEmpty() && info.type == bx::FileType::File)
            {
                if (modelFilePathCount >= GameRendering::MaxModels)
                {
                    LOG_ERROR("Model limit reached!");
                    break;
                }
                modelFilePaths[modelFilePathCount] = info.filePath;
                modelFileIsBinary[modelFilePathCount] = isBinary;
                modelFilePathCount++;
            }
        }
        LOG("Found %u models!", modelFilePathCount);

        auto& inst = GetInstance();
        int32_t writeI = 0;
        for (int32_t i = 0; i < modelFilePathCount; ++i)
        {
            bx::FilePath fullPath = bx::FilePath{"models"};
            fullPath.join(modelFilePaths[i].getCPtr());
            Model& mod = models[writeI];
            if (LoadMesh(mod, fullPath.getCPtr(), modelFileIsBinary[i]))
            {
                mod.Handle.Asset.Idx = CrcPath(fullPath.getCPtr());
                mod.Handle.ModelIdx = writeI;
                if (inst.DebugData.AssetCount < inst.DebugData.MaxAssets)
                {
                    inst.DebugData.AssetHandles[inst.DebugData.AssetCount] = mod.Handle.Asset;
                    bx::strCopy(inst.DebugData.AssetHandlePaths[inst.DebugData.AssetCount],
                                sizeof(inst.DebugData.AssetHandlePaths[inst.DebugData.AssetCount]),
                                fullPath.getCPtr());
                    ++inst.DebugData.AssetCount;
                }
                ++writeI;
            }
            else
            {
                LOG_ERROR("Failed to load model: %s", fullPath.getCPtr());
            }
        }
        outCount = writeI;
    }

    uint32_t CrcPath(const char* path)
    {
        bx::HashCrc32 hash;
        hash.begin();
        hash.add(path);
        return hash.end();
    }
} // namespace Game
