#include "../gen/Def.h"
#include "Gen.h"
#include "Global.h"
#include "Input.h"
#include "Instance.h"
#include "Level.h"
#include "Log.h"
#include "Puzzle.h"
#include "rendering/Rendering.h"

#include "SDL3/SDL_mouse.h"
#include "bgfx/bgfx.h"
#include "bx/error.h"
#include "bx/file.h"
#include "bx/filepath.h"
#include "bx/string.h"
#include "imgui.h"
#include <SDL3/SDL.h>
#include <bx/math.h>
#include <cstdint>
#include <tracy/Tracy.hpp>

namespace Game
{
    void EntityRenderData::Render(const Model* models, const Material* materials, const Texture* textures)
    {
        if (models == nullptr || materials == nullptr || textures == nullptr) return;
        if (!Generated::IsValid(ModelH) || MaterialHandle == EMaterial::UNDEFINED) return;
        if (!Visible) return;
        auto& rendering = GameRendering::Get();

        Transform.UpdateMatrix();
        bgfx::setTransform(Transform.M.M);

        const Model& currentModel = models[ModelH.ModelIdx];
        const Material& currentMaterial = materials[(uint16_t)MaterialHandle];

        if (!isValid(currentModel.IndexBuffer) || !isValid(currentModel.VertexBuffer)) return;
        if (!isValid(currentMaterial.Shader)) return;

        bgfx::setVertexBuffer(0, currentModel.VertexBuffer);
        bgfx::setIndexBuffer(currentModel.IndexBuffer);
        bgfx::setState(currentMaterial.State);

        float timeValues[4]{0.0f};
        timeValues[0] = GetInstance().Time.Now;

        float texInfo[4]{0.0f};
        texInfo[0] = textures[0].Info.width;
        texInfo[1] = textures[0].Info.height;
        texInfo[2] = rendering.DitherTextures.DitherTexWH;
        texInfo[3] = rendering.DitherTextures.DitherTexDepth;

        bgfx::UniformHandle sampler = rendering.DefaultSampler;
        bgfx::TextureHandle tex = rendering.Textures[0].RenderHandle;
        if (IsValid(TextureHandle))
        {
            sampler = textures[TextureHandle.TextureIdx].SamplerHandle;
            tex = textures[TextureHandle.TextureIdx].RenderHandle;
        }

        bgfx::setTexture(0, sampler, tex);
        bgfx::setTexture(1, rendering.DitherTextures.DitherSampler, rendering.DitherTextures.FinalTex);
        bgfx::setTexture(2, rendering.DitherTextures.RampSampler, rendering.DitherTextures.RampTex);
        bgfx::setUniform(currentMaterial.Uniforms[Material::UTime], timeValues);
        bgfx::setUniform(currentMaterial.Uniforms[Material::UDotColor], &TestColor.x);
        bgfx::setUniform(currentMaterial.Uniforms[Material::UTexInfo], texInfo);
        bgfx::setUniform(currentMaterial.Uniforms[Material::UBaseColor], &BaseColor.x);

        bgfx::submit(currentMaterial.ViewID, currentMaterial.Shader);
    }

    namespace
    {
        void UpdatePlayerInputMode()
        {
            bool IsGaming = GetInstance().Player.InputM == InputMode::Game;
            SDL_SetWindowRelativeMouseMode(GetShared().Window.SDLWindow, IsGaming);
            auto& IO = ImGui::GetIO();
            IO.ConfigFlags = FlagBool(IO.ConfigFlags, ImGuiConfigFlags_NoMouse | ImGuiConfigFlags_NoKeyboard, IsGaming);
            GameRendering::Get().UIVisible = IsGaming ? UIVisibilityState::Game : UIVisibilityState::Debug;
        }
    } // namespace

    void Level::Setup(GameData& data)
    {
        LOG("Level setup");
        void* storagePtr = data.EntityStorage;
        bool needReset = false;
        needReset |= Cubes.Setup(storagePtr, needReset);
        needReset |= Tests.Setup(storagePtr, needReset);
        needReset |= PuzzleTiles.Setup(storagePtr, needReset);
        needReset |= UIQuads.Setup(storagePtr, needReset);

        Tests.IsEnabled = false;
        Cubes.IsEnabled = false;

        Puzzle::Setup();

        bx::Error err;
        bx::DirectoryReader dirIter;
        bx::FileInfo info;
        bx::FilePath puzzleDirPath{Puzzle::PuzzleFileDir};
        if (dirIter.open(puzzleDirPath, &err))
        {
            while (true)
            {
                int32_t readCount = dirIter.read(&info, sizeof(info), &err);
                if (readCount == 0) break;
                if (err.isOk())
                {
                    if (info.type != bx::FileType::File) continue;
                    bx::StringView pathEnd = info.filePath.getExt();
                    if (bx::strCmpI(pathEnd, ".pzl") != 0) continue;

                    bx::FilePath fullPath = puzzleDirPath;
                    fullPath.join(info.filePath);
                    LOG("Loading %s", fullPath.getCPtr());

                    Generated::Deserializer ser;
                    Generated::PuzzleData dataBuf;
                    if (ser.Init(fullPath, "PZZL") && ser.ReadT(dataBuf))
                    {
                        if (dataBuf.ID >= BX_COUNTOF(Puzzles))
                        {
                            LOG_ERROR("Puzzle ID out of bounds: %u", dataBuf.ID);
                            ser.Finish();
                            continue;
                        }
                        Puzzles[dataBuf.ID].Data = dataBuf;
                        Puzzles[dataBuf.ID].Setup();
                    }
                    else
                    {
                        LOG_WARN("Failed to load puzzle!");
                    }
                    ser.Finish();
                }
                else
                {
                    LOG_ERROR("Failed parsing file name at %s:\n%s", puzzleDirPath.getCPtr(), err.getMessage());
                    break;
                }
            }
        }
        else
        {
            LOG_ERROR("Failed to open puzzle dir at %s:\n%s", puzzleDirPath.getCPtr(), err.getMessage().getCPtr());
        }

        if (Cubes.Count == 0)
        {
            for (uint32_t yy = 0; yy < 11; ++yy)
            {
                for (uint32_t xx = 0; xx < 11; ++xx)
                {
                    Cube& c = Cubes.Get(Cubes.New());
                    c.TestX = xx;
                    c.TestY = yy;
                    c.Setup();
                }
            }
            Cubes.New(); // Floor
        }
        if (Tests.Count == 0)
        {
            Tests.Get(Tests.New()).Setup();
        }
        UIQuads.Count = 0;
        PuzzleTiles.Count = 0;
        for (int32_t i = 0; i < BX_COUNTOF(Puzzles); ++i)
        {
            if (Puzzles[i].Data.ID != UINT16_MAX)
            {
                Puzzles[i].Setup();
            }
        }
        LOG("Tiles: %u", PuzzleTiles.Count);
        UpdatePlayerInputMode();
    }

    void Level::Update()
    {
        ZoneScopedN("Level update");
        START_PERF();
        PlayerData& player = GetInstance().Player;

        // Input
        float delta = GetInstance().Time.Delta;
        constexpr float moveSpeed = 10.0f;
        constexpr float rotSpeed = 1.6f;

        float forwardInput = (GetKey(ScanCode::W) ? 1.0f : 0.0f) + (GetKey(ScanCode::S) ? -1.0f : 0.0f);
        float rightInput = (GetKey(ScanCode::D) ? 1.0f : 0.0f) + (GetKey(ScanCode::A) ? -1.0f : 0.0f);
        bx::Vec3 moveInput = bx::Vec3{rightInput, forwardInput, 0.0f};
        moveInput = bx::normalize(moveInput);
        bx::Vec3 inputVec = {moveInput.x * delta * moveSpeed, 0.0f, moveInput.y * delta * moveSpeed};
        Vec2 mouseMovement = GetMouseMovement();
        bx::Vec3 rotInput = {mouseMovement.y * delta * rotSpeed, mouseMovement.x * delta * rotSpeed, 0.0f};

        if (GetKeyPressedNow(ScanCode::F1))
        {
            player.CameraM = player.CameraM == CameraMode::Walk ? CameraMode::Freefly : CameraMode::Walk;
        }

        if (GetKeyPressedNow(ScanCode::F2))
        {
            if (player.InputM == InputMode::Game)
            {
                player.InputM = InputMode::UI;
            }
            else
            {
                player.InputM = InputMode::Game;
            }
            UpdatePlayerInputMode();
        }

        if (player.CameraM == CameraMode::Freefly)
        {
            if (GetMouseButton(MouseButton::Left))
            {
                player.FreeflyXRot += rotInput.x;
                player.FreeflyYRot += rotInput.y;
                bx::mtxRotateY(player.FreeflyCamTransform.Rotation.M, player.FreeflyYRot);
                player.FreeflyCamTransform.RotateLocal({player.FreeflyXRot, 0.0f, 0.0f});
            }

            player.FreeflyCamTransform.TranslateLocal({0.0f, 0.0f, -inputVec.z});
            player.FreeflyCamTransform.TranslateLocal({-inputVec.x, 0.0f, 0.0f});
        }
        else if (player.CameraM == CameraMode::Walk)
        {
            player.PlayerCamTransform.TranslateLocal({0.0f, 0.0f, -inputVec.z});
            player.PlayerCamTransform.TranslateLocal({-inputVec.x, 0.0f, 0.0f});
            player.PlayerCamTransform.Position.y = -3.0f;

            player.WalkXRot += rotInput.x;
            player.WalkYRot += rotInput.y;
            bx::mtxRotateY(player.PlayerCamTransform.Rotation.M, player.WalkYRot);
            player.PlayerCamTransform.RotateLocal({player.WalkXRot, 0.0f, 0.0f});
        }

        // Cubes
        for (uint16_t i = 0; i < Cubes.Count; ++i)
        {
            Cubes.Get({i}).Update();
        }

        // Puzzle tiles
        for (int32_t i = 0; i < BX_COUNTOF(Puzzles); ++i)
        {
            Puzzles[i].Update();
        }

        END_PERF(GetShared().Window.PerfCounters, PerfCounterType::GameLevelUpdate, GetShared().Window.FrameCounter);
    }

    void Level::Render(uint16_t viewId, const Model* models, const Material* materials, const Texture* textures)
    {
        ZoneScopedN("Level Render");
        auto& shared = GetShared();

        float proj[16];
        bx::mtxProj(proj,
                    75.0f,
                    float(shared.Window.WindowWidth) / float(shared.Window.WindowHeight),
                    0.1f,
                    1000.0f,
                    bgfx::getCaps()->homogeneousDepth);

        auto& player = GetInstance().Player;
        if (player.CameraM == CameraMode::Freefly)
        {
            player.FreeflyCamTransform.UpdateMatrixForCam();
            bgfx::setViewTransform(viewId, player.FreeflyCamTransform.M.M, proj);
        }
        else
        {
            player.PlayerCamTransform.UpdateMatrixForCam();
            bgfx::setViewTransform(viewId, player.PlayerCamTransform.M.M, proj);
        }

        bgfx::touch(viewId);

        Cubes.Render(models, materials, textures);
        Tests.Render(models, materials, textures);
        PuzzleTiles.Render(models, materials, textures);
        UIQuads.Render(models, materials, textures);
    }

    void Cube::Setup()
    {
        EData.MaterialHandle = EMaterial::Default;
        EData.ModelH = GameRendering::Get().GetModelHandleFromPath("models/cube.gltf");
    }

    void Cube::Update()
    {
        if (TestX >= 0 && TestY >= 0)
        {
            double globalTime = GetInstance().Time.Now;
            double time = TestY <= 5 ? globalTime * 1.0f : 0.0f;
            float scale = 1.0f + TestX * 0.4f;
            EData.Transform.Position = bx::Vec3{TestX * 2.0f, TestY * 2.0f, 0.0f};
            EData.Transform.Scale = {scale, scale, scale};
        }
        else
        {
            EData.Transform.Position = {0.0f, -1.0f, 0.0f};
            EData.Transform.Scale = {100.0f, 1.0f, 100.0f};
        }
    }

    void TestEntity::Setup()
    {
        EData.MaterialHandle = EMaterial::Default;
        EData.ModelH = GameRendering::Get().GetModelHandleFromPath("models/zurg.gltf");

        EData.Transform.Position = {0.0f, 0.0f, 0.0f};
    }

    void WorldPuzzle::Setup()
    {
        auto& level = GetInstance().GameLevel;
        for (int32_t i = 0; i < Puzzle::Config::MaxCardsInPuzzle; ++i)
        {
            TileHandles[i] = level.PuzzleTiles.New();
            auto& tile = level.PuzzleTiles.Get(TileHandles[i]);
            tile.EData.MaterialHandle = EMaterial::Default;
            tile.EData.Visible = false;

            UIPlacedCards[i] = level.UIQuads.New();
            auto& quad = level.UIQuads.Get(UIPlacedCards[i]);
            quad.EData.ModelH = GameRendering::Get().GetModelHandleFromPath("models/plane.glb");
            quad.EData.MaterialHandle = EMaterial::UI;
            quad.EData.Visible = false;
        }
        IsSetup = true;
        LOG("finished setup!");
    }

    void WorldPuzzle::Update()
    {
        Transform& camTransform = GetInstance().Player.PlayerCamTransform;
        camTransform.UpdateMatrixForCam();
        Vec3 cameraPos = camTransform.GetPosition() * -1;
        Level& level = GetInstance().GameLevel;
        auto& staticCards = Puzzle::GetStaticPuzzleData().Cards;

        Transform boardTransform;
        boardTransform.Rotation = camTransform.Rotation.Inverse();
        Vec3 fw = {camTransform.M.M[2], camTransform.M.M[6], camTransform.M.M[10]};
        Vec3 pos = camTransform.GetPosition() * -1;
        pos += fw;
        boardTransform.SetPosition(pos);

        for (int8_t y = 0; y < Data.HeightTiles / Puzzle::Config::CardSize; ++y)
        {
            for (int8_t x = 0; x < Data.WidthTiles / Puzzle::Config::CardSize; ++x)
            {
                Generated::PlacedPuzzleCard& card = Data.PlacedCards[y * Puzzle::Config::MaxPuzzleSizeCards + x];
                auto& tile = level.PuzzleTiles.Get(TileHandles[y * Puzzle::Config::MaxPuzzleSizeCards + x]);
                auto& quad = level.UIQuads.Get(UIPlacedCards[y * Puzzle::Config::MaxPuzzleSizeCards + x]);

                bool IsValid = Puzzle::IsValid(card.RefCard);
                tile.EData.Visible = true;
                quad.EData.Visible = IsValid;

                tile.EData.ModelH = IsValid ? staticCards[card.RefCard.Idx].ModelHandle : staticCards[0].ModelHandle;
                quad.EData.TextureHandle =
                    IsValid ? staticCards[card.RefCard.Idx].BoardTextureHandle : Generated::TextureHandle{};
                Vec3 cardPos = {
                    (float)card.Position.X * Puzzle::Config::CardScaleWorld,
                    -5.0f,
                    (float)card.Position.Y * Puzzle::Config::CardScaleWorld,
                };
                if (!IsValid)
                {
                    cardPos = {x * Puzzle::Config::CardScaleWorld, -5.0f, y * Puzzle::Config::CardScaleWorld};
                }
                tile.EData.Transform.SetPosition(cardPos);
                bx::mtxRotateY(tile.EData.Transform.Rotation.M, card.Rotation * bx::kPi * 0.5f);

                quad.EData.Transform = boardTransform;
                quad.EData.Transform.TranslateLocal(Vec3{(float)card.Position.X, (float)card.Position.Y, 0.0f} * 0.21f);
                quad.EData.Transform.Scale = {0.1f, 0.1f, 0.1f};
                quad.EData.Transform.Rotate(Vec3{bx::kPi * 0.5f, 0.0f, 0.0f});
            }
        }
    }
} // namespace Game
