#include "../../engine/Shared.h"
#include "../Global.h"
#include "../Instance.h"
#include "../Log.h"
#include "../Mesh.h"
#include "../Tools.h"
#include "Rendering.h"

#include "SDL3/SDL_events.h"
#include "backends/imgui_impl_sdl3.h"
#include "bgfx/c99/bgfx.h"
#include "bgfx/defines.h"
#include "bgfx/platform.h"
#include "bx/bx.h"
#include "bx/constants.h"
#include "bx/debug.h"
#include "bx/filepath.h"
#include "bx/math.h"
#include "bx/string.h"
#include "bx/timer.h"
#include <bgfx/bgfx.h>
#include <bimg/bimg.h>
#include <bx/file.h>
#include <cstdint>
#include <cstdio>
#include <thread>
#include <tracy/Tracy.hpp>

#include "imgui-helper.h"
#include "imgui-style.h"
#include "imgui.h"

using namespace std::chrono_literals;

namespace Game
{
    namespace
    {
        const bgfx::Memory* loadFile(const char* path, bool appendZero = false, int32_t retryCount = 1)
        {
            FILE* file;
            for (int32_t i = 0; i < retryCount; ++i)
            {
                file = fopen(path, "rb");
                if (file == nullptr && i < retryCount - 1)
                {
                    std::this_thread::sleep_for(100ms);
                    break;
                }

                fseek(file, 0, SEEK_END);
                long fileSize = ftell(file);
                fseek(file, 0, SEEK_SET);

                long fileSizeX = appendZero ? fileSize + 1 : fileSize;
                void* rawMem = AllocateScratch(fileSizeX);
                if (rawMem == nullptr)
                {
                    LOG_ERROR("Failed to load file, exceeded scratch memory! %s", path);
                }
                const bgfx::Memory* mem = bgfx::makeRef(rawMem, fileSizeX);
                fread(mem->data, 1, fileSize, file);
                if (appendZero)
                {
                    mem->data[mem->size - 1] = '\0';
                }
                fclose(file);

                return mem;
            }

            LOG_WARN("File inaccessible! %s", path);
            return nullptr;
        }

        bgfx::ShaderHandle loadShader(const char* FILENAME)
        {
            const char* shaderPath = "???";

            switch (bgfx::getRendererType())
            {
            case bgfx::RendererType::Agc:
            case bgfx::RendererType::Nvn:
            case bgfx::RendererType::Count:
            case bgfx::RendererType::Noop:
                break;
            case bgfx::RendererType::Direct3D11:
            case bgfx::RendererType::Direct3D12:
                shaderPath = "game/compiled-shaders/dx11/";
                break;
            case bgfx::RendererType::Gnm:
                shaderPath = "game/compiled-shaders/pssl/";
                break;
            case bgfx::RendererType::Metal:
                shaderPath = "game/compiled-shaders/metal/";
                break;
            case bgfx::RendererType::OpenGL:
                shaderPath = "game/compiled-shaders/glsl/";
                break;
            case bgfx::RendererType::OpenGLES:
                shaderPath = "game/compiled-shaders/essl/";
                break;
            case bgfx::RendererType::Vulkan:
                shaderPath = "game/compiled-shaders/spirv/";
                break;
            }

            char buffer[512]{0};
            bx::strCopy(buffer, sizeof(buffer), shaderPath);
            bx::strCat(buffer, sizeof(buffer), FILENAME);
            bx::strCat(buffer, sizeof(buffer), ".bin");

            LOG("Loading shader at %s", buffer);
            const bgfx::Memory* mem = loadFile(buffer, true, 3);

            if (mem == nullptr)
            {
                LOG_WARN("Failed to load shader %s", FILENAME);
                return {};
            }
            return bgfx::createShader(mem);
        }

        bgfx::TextureHandle loadTexture(const bx::FilePath& _filePath,
                                        uint64_t _flags,
                                        uint8_t _skip,
                                        bgfx::TextureInfo* _info,
                                        bimg::Orientation::Enum* _orientation)
        {
            BX_UNUSED(_skip);
            bgfx::TextureHandle handle = BGFX_INVALID_HANDLE;
            bx::Error err;

            const bgfx::Memory* data = loadFile(_filePath.getCPtr());
            if (data == nullptr || data->data == nullptr || data->size == 0)
            {
                LOG_WARN("Failed to find image %s", _filePath.getCPtr());
                return handle;
            }
            bimg::ImageContainer imageContainer;
            if (!bimg::imageParse(imageContainer, data->data, data->size, &err))
            {
                LOG_WARN("Failed to load image %s", _filePath.getCPtr());
                return handle;
            }
            if (NULL != _orientation)
            {
                *_orientation = imageContainer.m_orientation;
            }

            const bgfx::Memory* mem =
                bgfx::makeRef(data->data + imageContainer.m_offset, data->size - imageContainer.m_offset);

            if (NULL != _info)
            {
                bgfx::calcTextureSize(*_info,
                                      uint16_t(imageContainer.m_width),
                                      uint16_t(imageContainer.m_height),
                                      uint16_t(imageContainer.m_depth),
                                      imageContainer.m_cubeMap,
                                      1 < imageContainer.m_numMips,
                                      imageContainer.m_numLayers,
                                      bgfx::TextureFormat::Enum(imageContainer.m_format));
            }

            if (imageContainer.m_cubeMap)
            {
                handle = bgfx::createTextureCube(uint16_t(imageContainer.m_width),
                                                 1 < imageContainer.m_numMips,
                                                 imageContainer.m_numLayers,
                                                 bgfx::TextureFormat::Enum(imageContainer.m_format),
                                                 _flags,
                                                 mem);
            }
            else if (1 < imageContainer.m_depth)
            {
                handle = bgfx::createTexture3D(uint16_t(imageContainer.m_width),
                                               uint16_t(imageContainer.m_height),
                                               uint16_t(imageContainer.m_depth),
                                               1 < imageContainer.m_numMips,
                                               bgfx::TextureFormat::Enum(imageContainer.m_format),
                                               _flags,
                                               mem);
            }
            else if (bgfx::isTextureValid(0,
                                          false,
                                          imageContainer.m_numLayers,
                                          bgfx::TextureFormat::Enum(imageContainer.m_format),
                                          _flags))
            {
                handle = bgfx::createTexture2D(uint16_t(imageContainer.m_width),
                                               uint16_t(imageContainer.m_height),
                                               1 < imageContainer.m_numMips,
                                               imageContainer.m_numLayers,
                                               bgfx::TextureFormat::Enum(imageContainer.m_format),
                                               _flags,
                                               mem);
            }

            if (bgfx::isValid(handle))
            {
                const bx::StringView name(_filePath);
                bgfx::setName(handle, name.getPtr(), name.getLength());
            }

            return handle;
        }

        GameRendering* Instance = nullptr;
    } // namespace

    GameRendering& GameRendering::Get()
    {
        assert(Instance != nullptr);
        return *Instance;
    }

    void GameRendering::Setup()
    {
        LOG("--- RENDERING STARTUP ---");
        ZoneScopedN("Setup");

        if (Instance != nullptr) LOG_WARN("old rendering wasn't destroyed!");
        Instance = this;
        SharedData& shared = GetShared();

        bgfx::Init init;
        init.type = bgfx::RendererType::Direct3D12;
#ifdef _DEBUG
        // init.debug = true;
#endif
        init.platformData.nwh = shared.Window.Handle;
        init.platformData.ndt = nullptr;
        init.platformData.type = bgfx::NativeWindowHandleType::Default;
        init.resolution.width = shared.Window.WindowWidth;
        init.resolution.height = shared.Window.WindowHeight;
        init.resolution.reset = ResetFlags;

        LastWidth = shared.Window.WindowWidth;
        LastHeight = shared.Window.WindowHeight;

        LOG("%i by %i", init.resolution.width, init.resolution.height);

        if (!bgfx::init(init))
        {
            LOG_ERROR("BGFX setup failed!");
        }
        else
        {
            LOG("BGFX setup succeded!");
        }
        bgfx::setViewClear(MainViewID, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x3399FFff, 1.0f, 0);
        bgfx::setViewRect(MainViewID, 0, 0, shared.Window.WindowWidth, shared.Window.WindowHeight);

        DefaultSampler = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler);
        LoadTextures();
        LoadModels(Models, ModelCount);

        ReloadShaders();
        imguiCreate();
        SetImguiStyle();

        if (!ImGui_ImplSDL3_InitForOther(shared.Window.SDLWindow))
        {
            LOG_ERROR("Failed to set up imgui implementation!");
            return;
        }

        // ImGui::GetIO().BackendFlags |= ImGuiBackendFlags_RendererHasViewports;
        ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
        // ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
        // auto& platIO = ImGui::GetPlatformIO();
        // platIO.Renderer_CreateWindow = TODO;
        // platIO.Platform_DestroyWindow = TODO;
        // platIO.Platform_SetWindowSize = TODO;
        // platIO.Platform_RenderWindow = TODO;

        GameInstance& inst = GetInstance();
        if (!inst.IsInitialized)
        {
            inst.Time.StartTime = bx::getHPCounter();
        }

        if (inst.DebugData.ImguiIniSize > 0)
        {
            ImGui::LoadIniSettingsFromMemory(inst.DebugData.ImguiIni, inst.DebugData.ImguiIniSize);
        }
        else
        {
            ImGui::LoadIniSettingsFromDisk("imgui.ini");
        }
        DitherGen(DitherTextures, DitherRecursion);
    }

    void GameRendering::HandleEvents()
    {
        ZoneScopedN("Handle Events");
        SharedData& shared = GetShared();

        for (uint16_t i = 0; i < shared.Window.SDLEventCount; ++i)
        {
            ImGui_ImplSDL3_ProcessEvent(&shared.Window.SDLEvents[i]);
        }
        shared.Window.SDLEventCount = 0;

        // Resize if necessary
        if (shared.Window.WindowWidth != LastWidth || shared.Window.WindowHeight != LastHeight)
        {
            bgfx::reset(shared.Window.WindowWidth, shared.Window.WindowHeight, ResetFlags);
            bgfx::setViewRect(MainViewID, 0, 0, shared.Window.WindowWidth, shared.Window.WindowHeight);

            LastWidth = shared.Window.WindowWidth;
            LastHeight = shared.Window.WindowHeight;
        }

        // Reload shaders if necessary
        FileChangeNotification* shaderChange = nullptr;
        if (shared.Dev.ChangedShaderCount > 0)
        {
            char buf[1024]{0};
            for (int32_t i = 0; i < shared.Dev.ChangedShaderCount; ++i)
            {
                wcstombs(buf, shared.Dev.ChangedShaders[i].FileName, sizeof(buf));
                LOG("Changed: %s", buf);
            }
            shared.Dev.ChangedShaderCount = 0;

            // TODO: when to destroy shader?
            // TODO: only reload changed shaders
            ReloadShaders();
        }
    }

    void GameRendering::LoadTextures()
    {
        for (int32_t i = 0; i < MaxTextures; ++i)
        {
            Textures[i] = {};
        }

        bx::Error err;
        bx::DirectoryReader reader{};
        if (!reader.open("textures", &err) || !err.isOk())
        {
            LOG_ERROR("Failed to read textures dir: %s", err.getMessage());
        }
        bx::FileInfo info;
        int32_t textureFilePathCount = 0;
        bx::FilePath textureFilePaths[GameRendering::MaxTextures];
        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 isDDS = bx::strCmp(ext, ".dds") == 0;
            bool isKTX = bx::strCmp(ext, ".ktx") == 0;
            if ((isDDS || isKTX) && !ext.isEmpty() && info.type == bx::FileType::File)
            {
                if (textureFilePathCount >= GameRendering::MaxTextures)
                {
                    LOG_ERROR("Texture limit reached!");
                    break;
                }
                textureFilePaths[textureFilePathCount] = info.filePath;
                textureFilePathCount++;
            }
        }
        LOG("Found %u textures!", textureFilePathCount);

        for (int32_t i = 0; i < textureFilePathCount; ++i)
        {
            bx::FilePath fullPath{"textures"};
            fullPath.join(textureFilePaths[i]);
            Textures[i].RenderHandle =
                loadTexture(fullPath, BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE, 0, &Textures[i].Info, nullptr);
            Textures[i].SamplerHandle = DefaultSampler;
            Textures[i].TexHandle.TextureIdx = i;
            Textures[i].TexHandle.Asset.Idx = CrcPath(fullPath.getCPtr());
            auto& debug = GetInstance().DebugData;
            if (debug.AssetCount < debug.MaxAssets)
            {
                debug.AssetHandles[debug.AssetCount] = Textures[i].TexHandle.Asset;
                bx::strCopy(debug.AssetHandlePaths[debug.AssetCount],
                            sizeof(debug.AssetHandlePaths[debug.AssetCount]),
                            fullPath.getCPtr());
                ++debug.AssetCount;
            }
        }
    }

    void GameRendering::ReloadShaders()
    {
        Materials[(uint16_t)EMaterial::Default] = Material::LoadFromShader("dither/vert", "dither/frag", MainViewID);
        Materials[(uint16_t)EMaterial::UI] = Material::LoadFromShader("normal/vert", "normal/frag", MainViewID);
    }

    void GameRendering::Update()
    {
        ZoneScopedN("Rendering");
        SharedData& shared = GetShared();
        auto& time = GetInstance().Time;

        HandleEvents();

        // Start Rendering
        {
            ZoneScopedN("Imgui Start Frame");
            imguiBeginFrame(20);
            ImGui_ImplSDL3_NewFrame();
            ImGui::DockSpaceOverViewport(0, 0, ImGuiDockNodeFlags_PassthruCentralNode);
        }

        Tools::RenderDebugUI(*this);

        GetInstance().GameLevel.Update();
        GetInstance().GameLevel.Render(MainViewID, Models, Materials, Textures);

        // Finish Frame
        {
            ZoneScopedN("Imgui End Frame");
            imguiEndFrame();
        }

        if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
        {
            ZoneScopedN("Imgui Platform Update");
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
        }

        {
            ZoneScopedN("BGFX Frame");
            START_PERF();
            bgfx::frame();
            END_PERF(shared.Window.PerfCounters, PerfCounterType::Submit, shared.Window.FrameCounter);
        }
    }

    void GameRendering::Shutdown()
    {
        ZoneScopedN("Shutdown");
        LOG("--- RENDERING_SHUTDOWN ---");
        ImGui::SaveIniSettingsToDisk("imgui.ini");
        auto& debug = GetInstance().DebugData;
        const char* iniData = ImGui::SaveIniSettingsToMemory(reinterpret_cast<uint64_t*>(&debug.ImguiIniSize));
        assert(debug.ImguiIniSize <= BX_COUNTOF(InstanceDebugData::ImguiIni));
        bx::memCopy(debug.ImguiIni, iniData, bx::min(debug.ImguiIniSize, BX_COUNTOF(InstanceDebugData::ImguiIni)));
        ImGui_ImplSDL3_Shutdown();
        imguiDestroy();
        bgfx::shutdown();
        Instance = nullptr;
    }

    Material Material::LoadFromShader(const char* vertPath, const char* fragPath, uint16_t view)
    {
        BX_ASSERT(vertPath != nullptr && fragPath != nullptr, "Invalid shader path!");
        bgfx::ShaderHandle vertexShader = loadShader(vertPath);
        bgfx::ShaderHandle fragmentShader = loadShader(fragPath);

        Material mat;
        mat.Shader = bgfx::createProgram(vertexShader, fragmentShader, true);
        mat.State = 0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS |
                    BGFX_STATE_CULL_CCW | BGFX_STATE_MSAA;
        mat.Uniforms[Material::UTime] = bgfx::createUniform("u_time", bgfx::UniformType::Vec4);
        mat.Uniforms[Material::UDotColor] = bgfx::createUniform("u_testColor", bgfx::UniformType::Vec4);
        mat.Uniforms[Material::UTexInfo] = bgfx::createUniform("u_texInfo", bgfx::UniformType::Vec4);
        mat.Uniforms[Material::UBaseColor] = bgfx::createUniform("u_baseColor", bgfx::UniformType::Vec4);
        mat.ViewID = view;
        return mat;
    }

    Generated::ModelHandle GameRendering::GetModelHandleFromPath(const char* path)
    {
        uint32_t AssetHandle = CrcPath(path);
        for (int32_t i = 0; i < ModelCount; ++i)
        {
            if (Models[i].Handle.Asset.Idx == AssetHandle)
            {
                return Models[i].Handle;
            }
        }
        LOG_WARN("Failed to find model for path %s", path);
        return {};
    }

} // namespace Game
