/******************************************************************************
*	File		-	world.cpp
*	Author		-	Joey Pollack
*	Date		-	2022/01/12 (y/m/d)
*	Mod Date	-	2022/01/20 (y/m/d)
*	Description	-	Manages a game "world". A world is made up of regions which
*                   are subdivisions of the world. Each region contains: a set 
*                   of images for the maps layers, a list of objects that spawn
*                   in this region and static collision data.
******************************************************************************/

#include "world.h"
#include <scripting/world_api.h>
#include <LunariumConfig.h>
#include <utils/logger.h>
#include <utils/helpers.h>
#include <assets/types/script.h>
#include <assets/types/image.h>
#include <renderer/renderer2D.h>
#include <renderer/frame_buffer.h>
#include <renderer/orthographic_camera.h>
#include <renderer/texture.h>
#include <box2d/box2d.h>
#include "entity.h"

//#define LOAD_ASSETS_FROM_EDITOR true
#if !BUILD_NO_EDITOR
    #include <editor/contents/content_manager.h>
    #include <editor/contents/script.h>
    #include <editor/contents/image.h>
#endif

namespace lunarium
{
    // World::World(Camera* pCam, Sizei region_size, Sizei world_size)
    //     : mpCamera(pCam), mRegionSize(region_size), mpWorldScript(nullptr), mWorldSize(world_size)
    // {
    //     mActiveRegion = { 0, 0 };
    // }

    World::World(std::string name)
        : mUUID(UUID::GetNewID()), mName(name), mpActiveCamera(nullptr), mFrameBuffer(nullptr), mpPhysicsWorld(nullptr), mDrawDebugObjects(false)
                //mGetAssetsFromEditor(EDITOR_MODE)
    {
        WorldSettings.Physics.PhysicsScaleFactor = 16.0f;
    }

    
    std::string World::GetName() const
    {
        return mName;
    }

    void World::SetName(std::string name)
    {
        mName = name;
    }

    glm::vec2 World::GetGravity() const
    {
        return { WorldSettings.Physics.Gravity.x, WorldSettings.Physics.Gravity.y };
    }
    
    void World::SetGravity(glm::vec2 gravity)
    {
        WorldSettings.Physics.Gravity.x = gravity.x;
        WorldSettings.Physics.Gravity.y = gravity.y;
    }
    
    float World::GetPhysicsScaleFactor() const
    {
        return WorldSettings.Physics.PhysicsScaleFactor;
    }
    
    void World::SetPhysicsScaleFactor(float factor)
    {
        WorldSettings.Physics.PhysicsScaleFactor = factor;
    }
    

    void World::OnLoad()
    {
        // INITIALIZE SCRIPTS
        WorldAPI::Initialize(this).LogIfFailed(LogCategory::GAME_SYSTEM, "Failed to initialized the world scripting api");

        // Initialize Physics
        mpPhysicsWorld = new b2World({WorldSettings.Physics.Gravity.x, WorldSettings.Physics.Gravity.y});

        // Iterate all entities with physics components and initialize them
        // Only checking for RigidBody because any physics object needs at least that
        auto group_phys = mECSRegistry.view<RigidBodyComponent, TransformComponent>();
        for (auto entity: group_phys)
        {
            auto& transform = group_phys.get<TransformComponent>(entity);
            auto& rigid_body = group_phys.get<RigidBodyComponent>(entity);
            float scale_factor = WorldSettings.Physics.PhysicsScaleFactor;

            rigid_body.Defintion.position.Set(transform.Position.x / scale_factor, transform.Position.y / scale_factor);
            rigid_body.Defintion.angle = transform.Rotation.z;
            if (!rigid_body.IsStatic)
            {
                rigid_body.Defintion.type = b2_dynamicBody;
            }

            rigid_body.pBody = mpPhysicsWorld->CreateBody(&rigid_body.Defintion);

            // Handle collision components
            if (mECSRegistry.all_of<CollisionComponent>(entity))
            {
                auto& comp = mECSRegistry.get<CollisionComponent>(entity);
                switch (comp.Type)
                {
                    case CollisionComponent::ShapeType::Box:
                    {
                        b2PolygonShape Shape;
                        Shape.SetAsBox(comp.Width / scale_factor, comp.Height / scale_factor);
                        b2FixtureDef fixtureDef;
                        fixtureDef.shape = &Shape;
                        fixtureDef.isSensor = comp.IsSensor;
                        fixtureDef.density = comp.Density;
                        fixtureDef.friction = comp.Friction;
                        fixtureDef.restitution = comp.Restitution;
                        
                        rigid_body.pBody->CreateFixture(&fixtureDef);
                    }  break;

                    case CollisionComponent::ShapeType::Circle:
                    {
                        b2CircleShape Shape;
                        Shape.m_radius = comp.Radius / scale_factor;

                        b2FixtureDef fixtureDef;
                        fixtureDef.shape = &Shape;
                        fixtureDef.isSensor = comp.IsSensor;
                        fixtureDef.density = comp.Density;
                        fixtureDef.friction = comp.Friction;
                        fixtureDef.restitution = comp.Restitution;
                        
                        rigid_body.pBody->CreateFixture(&fixtureDef);
                    }  break;
                    
                }
            }
        }


        // Load scripts from all ScriptComponents in the world
        auto group_scripts = mECSRegistry.view<ScriptComponent>();
        for(auto entity: group_scripts) 
        {
            auto& script_comp = group_scripts.get<ScriptComponent>(entity);

// If were attached to the editor we need to get the script from the editor's content manager
#if !BUILD_NO_EDITOR

            editor::Script* pScript = (editor::Script*) editor::ContentManager::GetInstance().GetAsset(script_comp.ScriptID);
            WrenScript script(pScript->GetScriptFile().filename().string().c_str(), pScript->GetScript());
           // mScriptState.RunScript(&script);
           WorldAPI::RunScript(script);
#else
        // Get Binary assets
#endif
        }

        // Call OnLoad for each registered EntityBehavior class object
        WorldAPI::InvokeEvent(WorldAPI::Event::ON_LOAD);
    }
    
    void World::OnUnload()
    {
        // Call OnUnLoad for each registered EntityBehavior class object
        WorldAPI::InvokeEvent(WorldAPI::Event::ON_UNLOAD);
        WorldAPI::Shutdown();

        delete mpPhysicsWorld;
        mpPhysicsWorld = nullptr;
    }

    void World::SetRunMode(RunMode mode)
    {
        mMode = mode;
    }

    World::RunMode World::GetRunMode()
    {
        return mMode;
    }
    
    void World::Update(float dt)
    {
        // Update all scripts
        // Call Update for each registered EntityBehavior class object
        WorldAPI::InvokeEvent(WorldAPI::Event::ON_UPDATE, dt);


        // Update Transforms for any enity with a velocity
        // NOTE: Might remove the VelocityComponent in favor of using the physics system
        auto group = mECSRegistry.group<>(entt::get<VelocityComponent, TransformComponent>);
        for(auto entity: group) 
        {   
            // Skip any entity that has a physics component
            if (mECSRegistry.any_of<RigidBodyComponent, CollisionComponent>(entity))
            {
                continue;
            }

            auto &transform = group.get<TransformComponent>(entity);
            auto &velocity = group.get<VelocityComponent>(entity);

            transform.Position += velocity.Velocity * dt;
        }

        // Update the physics sim and then use the results to update objects Transforms
        float timeStep = 1.0f / 60.0f;
        int32 velocityIterations = 6;
        int32 positionIterations = 2;

        mpPhysicsWorld->Step(timeStep, velocityIterations, positionIterations);
        auto group_phys = mECSRegistry.view<TransformComponent, RigidBodyComponent>();
        for (auto entity: group_phys)
        {
            auto& transform = group_phys.get<TransformComponent>(entity);
            auto& rigid_body = group_phys.get<RigidBodyComponent>(entity);
            float scale_factor = WorldSettings.Physics.PhysicsScaleFactor;

            transform.Position = glm::vec3(rigid_body.pBody->GetPosition().x * scale_factor, rigid_body.pBody->GetPosition().y * scale_factor, 0.0f);
            transform.Rotation.z = rigid_body.pBody->GetAngle();
        }

    }
    
    void World::Render(lunarium::Renderer2D* pGraphics)
    {
        RenderBlockouts(pGraphics);
        RenderSprites(pGraphics);

        if (mDrawDebugObjects)
        {
            RenderDebugObjects(pGraphics);
        }
    }    

    
    void World::SetActiveCamera(OrthographicCamera* pCam)
    {
        mpActiveCamera = pCam;
    }

    /////////////////////////////////////////////////////////////////////
    //				ENTITY API
    /////////////////////////////////////////////////////////////////////

    entt::registry* World::GetEntityRegistry()
    {
        return &mECSRegistry;
    }

    LUUID World::CreateEntity()
    {
        //Logger::Error(LogCategory::GAME_SYSTEM, "World::CreateEntity not implemented!");
        Entity* new_ent = new Entity(*this);

        if (mEntitiesByUUID.find(new_ent->GetUUID()) != mEntitiesByUUID.end())
        {
            Logger::Warn(LogCategory::GAME_SYSTEM, "UUID collision when creating new entity! UUID: %d", new_ent->GetUUID());
        }

        mEntitiesByUUID[new_ent->GetUUID()] = new_ent;
        mEntities.push_back(new_ent);
        return new_ent->GetUUID();
    }
    
    void World::RemoveEntity(LUUID id)
    {
        Entity* temp = mEntitiesByUUID[id];
        mEntitiesByUUID.erase(id);
        
        for (auto iter = mEntities.begin(); iter != mEntities.end(); iter++)
        {
            if ((*iter)->GetUUID() == id)
            {
                mEntities.erase(iter);
                break;
            }
        }

        mECSRegistry.destroy(temp->GetEnttHandle());
        delete temp;
    }

    Entity* World::GetEntity(LUUID id)
    {
        if (mEntitiesByUUID.find(id) == mEntitiesByUUID.end())
        {
            Logger::Warn(LogCategory::GAME_SYSTEM, "Entity with id: %d not found.", id);
        }
        return mEntitiesByUUID[id];
    }

    unsigned int World::GetNumEntities() const
    {
        return mEntities.size();
    }
    
    std::vector<Entity*>::iterator World::EntitiesBegin()
    {
        return mEntities.begin();
    }

    bool World::EntitiesIsEnd(std::vector<Entity*>::iterator& iter)
    {
        return iter == mEntities.end();
    }


    
    /////////////////////////////////////////////////////////////////////
    //				RUN MODE HELPERS
    /////////////////////////////////////////////////////////////////////

    void World::RenderBlockouts(lunarium::Renderer2D* pGraphics)
    {
        // First get the group we want
        auto group = mECSRegistry.group<>(entt::get<BlockOutComponent, TransformComponent>);

        // Next sort the group
        group.sort<BlockOutComponent>([](const BlockOutComponent &lhs, const BlockOutComponent &rhs) 
        {
            return lhs.RenderLayer < rhs.RenderLayer;
        });

        // Render the group
        for(auto entity: group) 
        {
            auto &transform = group.get<TransformComponent>(entity);
            auto &blockout = group.get<BlockOutComponent>(entity);

            
            Rectangle rect(transform.Position.x, transform.Position.y, blockout.Size.x, blockout.Size.y);
            Color color(blockout.Color.x, blockout.Color.y, blockout.Color.z, blockout.Color.w);

            // Get and apply parent transforms
            LUUID uuid = mECSRegistry.get<UUIDComponent>(entity).UUID;
            Entity* pEnt = GetEntity(uuid);
            glm::mat4 parent_transform = glm::mat4(1.0f);
            if (pEnt->HasComponent<ParentEntityComponent>())
            {

                parent_transform = GetParentTransform(pEnt->GetComponent<ParentEntityComponent>().Parent);
            }

            pGraphics->DrawQuad(rect, color, nullptr, transform.Rotation.z, Rectangle(-1, -1, -1, -1), parent_transform);
        }
    }

    void World::RenderSprites(lunarium::Renderer2D* pGraphics)
    {
        // First get the group we want
        auto group = mECSRegistry.group<>(entt::get<SpriteRendererComponent, TransformComponent>);

        // Next sort the group
        group.sort<SpriteRendererComponent>([](const SpriteRendererComponent &lhs, const SpriteRendererComponent &rhs) 
        {
            return lhs.RenderLayer < rhs.RenderLayer;
        });

        // Render the group
        for(auto entity: group) 
        {
            auto &transform = group.get<TransformComponent>(entity);
            auto &sprite = group.get<SpriteRendererComponent>(entity);

            
            Rectangle rect(transform.Position.x, transform.Position.y, sprite.FrameSize.x, sprite.FrameSize.y);

            // Get and apply parent transforms
            LUUID uuid = mECSRegistry.get<UUIDComponent>(entity).UUID;
            Entity* pEnt = GetEntity(uuid);
            glm::mat4 parent_transform = glm::mat4(1.0f);
            if (pEnt->HasComponent<ParentEntityComponent>())
            {

                parent_transform = GetParentTransform(pEnt->GetComponent<ParentEntityComponent>().Parent);
            }

            // If were attached to the editor we need to get the script from the editor's content manager
#if !BUILD_NO_EDITOR
            editor::Image* i = (editor::Image*)editor::ContentManager::GetInstance().GetAsset(sprite.ImageID);
            if (!i)
                continue;
                
            Texture* t = i->GetImage();
            Rectangle sub_region = Rectangle::MakeFromTopLeft(sprite.FramePos.x, sprite.FramePos.y, sprite.FrameSize.s, sprite.FrameSize.y);


            pGraphics->DrawSprite(t, glm::vec2(transform.Position), sub_region, 
                                    sprite.TintColor, transform.Rotation.z, parent_transform);
#endif

           // pGraphics->DrawQuad(rect, color, nullptr, transform.Rotation.z, Rectangle(-1, -1, -1, -1), parent_transform);
        }
    }
    
    
    void World::RenderDebugObjects(lunarium::Renderer2D* pGraphics)
    {
        auto group_phys = mECSRegistry.view<TransformComponent, RigidBodyComponent, CollisionComponent>();
        for (auto entity : group_phys)
        {
            auto& rigid_body = group_phys.get<RigidBodyComponent>(entity);
            auto& collision = group_phys.get<CollisionComponent>(entity);

            if (!rigid_body.pBody)
                continue;

            if (collision.Type == CollisionComponent::ShapeType::Box)
            {
                b2AABB box;
                b2Transform transform = { rigid_body.pBody->GetTransform().p, b2Rot(0.0f) };
                rigid_body.pBody->GetFixtureList()->GetShape()->ComputeAABB(&box, transform, 0);
                float scale_factor = WorldSettings.Physics.PhysicsScaleFactor;

                float angle =  rigid_body.pBody->GetAngle();
                Rectangle rect(box);
                rect.Scale(scale_factor, scale_factor);
                rect.X *= scale_factor;
                rect.Y *= scale_factor;
                pGraphics->DrawBox(rect, Color(0.1f, 0.2f, 1.0f, 1.0f), 1.5f, angle);
            }
        }
    }

    glm::mat4 World::GetParentTransform(LUUID parent)
    {
        glm::mat4 transform(1.0f);
        Entity* p = GetEntity(parent);

        if (p->HasComponent<TransformComponent>())
        {
            

            if (p->HasComponent<ParentEntityComponent>())
            {
                transform = GetParentTransform(p->GetComponent<ParentEntityComponent>().Parent);
            }

            TransformComponent t = p->GetComponent<TransformComponent>();
            transform = glm::translate(transform, t.Position);
            transform = glm::rotate(transform, t.Rotation.z * -1.0f, glm::vec3(0.0f, 0.0f, 1.0f));

        }

        return transform;
    }
    
    void World::RenderEditor(lunarium::Renderer2D* pGraphics) const
    {
        // NOTE: MAY BE REMOVED
    }

    
    WorldState World::GetState()
    {
        return { AsJSON() };
    }

    void World::ResetState(WorldState& state)
    {
        if (!IsValidNode(state.State))
        {
            Logger::Warn(LogCategory::GAME_SYSTEM, "World state could not be set - state is invalid");
            return;
        }
        mECSRegistry.clear();
        mEntities.clear();
        mEntitiesByUUID.clear();

        Deserialize(state.State).LogIfFailed(LogCategory::GAME_SYSTEM, "Failed to deserialize the world state");
    }

    /////////////////////////////////////////////////////////////////////
    //				SERIALIZING
    /////////////////////////////////////////////////////////////////////
    OpRes World::Serialize(nlohmann::ordered_json& node)
    {
        #if !BUILD_NO_EDITOR // Only does this when this is an editor build

        // General data
        node["UUID"] = mUUID;
        node["Name"] = mName;

        // Game entities
        node["NumberOfEntities"] = mEntities.size();
        auto& ents = node["Entities"];
        for (int i = 0; i < mEntities.size(); i++)
        {
            nlohmann::ordered_json e;
            mEntities[i]->Serialize(e).LogIfFailed(LogCategory::GAME_SYSTEM);
            ents.emplace_back(e);
        }

        // World settings
        auto& settings = node["Settings"];

        auto& physics = settings["Physics"];
        physics["scale_factor"] = WorldSettings.Physics.PhysicsScaleFactor;

        auto& gravity = physics["gravity"];
        gravity["x"] = WorldSettings.Physics.Gravity.x;
        gravity["y"] = WorldSettings.Physics.Gravity.y;
        gravity["z"] = WorldSettings.Physics.Gravity.z;

        #endif
        return OpRes::OK();
    }

    OpRes World::Deserialize(nlohmann::ordered_json& node)
    {
        #if !BUILD_NO_EDITOR // Only does this when this is an editor build

        // General Data
        mUUID = node["UUID"].get<u64>();
        mName = node["Name"].get<std::string>();

        // Game entities
        int num_entities = node["NumberOfEntities"].get<int>();
        auto& ents = node["Entities"];
        for (auto it = ents.begin(); it != ents.end(); ++it)
        {
            auto& ent = (*it);
            Entity* new_ent = new Entity(*this);

            if (!new_ent->IsValidNode(ent))
            {
                delete new_ent;
                return OpRes::Fail("Invalid entity node");
            }

            new_ent->Deserialize(ent).LogIfFailed(LogCategory::GAME_SYSTEM);
            mEntities.push_back(new_ent);
            mEntitiesByUUID[new_ent->GetUUID()] = new_ent;
        }

        // World Settings
        if (!node["Settings"].is_null())
        {
            auto& settings = node["Settings"];

            auto& physics = settings["Physics"];
            WorldSettings.Physics.PhysicsScaleFactor = physics["scale_factor"].get<float>();
            auto& gravity = physics["gravity"];
            WorldSettings.Physics.Gravity = { gravity["x"].get<float>(), gravity["y"].get<float>(), gravity["z"].get<float>() };
        }
        

        #endif
        return OpRes::OK();
    }
    
    bool World::IsValidNode(nlohmann::ordered_json& node)
    {
        if (node["UUID"].is_null()) { return false; }
        if (!node["UUID"].is_number()) { return false; }

        if (node["NumberOfEntities"].is_null()) { return false; }
        if (!node["NumberOfEntities"].is_number()) { return false; }

        if (node["Entities"].is_null()) { return false; }


        return true;
    }
    
    nlohmann::ordered_json World::AsJSON()
    {
        #if !BUILD_NO_EDITOR // Only does this when this is an editor build
        nlohmann::ordered_json node;

        Serialize(node).LogIfFailed(LogCategory::GAME_SYSTEM);

        return node;
        #endif

        return nlohmann::ordered_json();
    }

}