// Copyright (c) Wojciech Figat. All rights reserved.

#define SCENE_RENDERING_USE_PROFILER_PER_ACTOR 0

#include "SceneRendering.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderView.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Threading/JobSystem.h"
#include "Engine/Physics/Actors/IPhysicsDebug.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#if !BUILD_RELEASE
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Core/Log.h"
#endif

#if BUILD_RELEASE
#define CHECK_SCENE_EDIT_ACCESS()
#else
#define CHECK_SCENE_EDIT_ACCESS() \
    if (_isRendering && IsInMainThread() && GPUDevice::Instance && GPUDevice::Instance->IsRendering()) \
    { \
        LOG(Error, "Adding/removing actors during rendering is not supported ({}, '{}').", a->ToString(), a->GetNamePath()); \
        return; \
    }
#endif

ISceneRenderingListener::~ISceneRenderingListener()
{
    for (SceneRendering* scene : _scenes)
    {
        scene->_listeners.Remove(this);
    }
}

void ISceneRenderingListener::ListenSceneRendering(SceneRendering* scene)
{
    if (!_scenes.Contains(scene))
    {
        _scenes.Add(scene);
        scene->_listeners.Add(this);
    }
}

FORCE_INLINE bool FrustumsListCull(const BoundingSphere& bounds, const Array<BoundingFrustum>& frustums)
{
    const int32 count = frustums.Count();
    const BoundingFrustum* data = frustums.Get();
    for (int32 i = 0; i < count; i++)
    {
        if (data[i].Intersects(bounds))
            return true;
    }
    return false;
}

void SceneRendering::Draw(RenderContextBatch& renderContextBatch, DrawCategory category)
{
    PROFILE_MEM(Graphics);
    if (category == PreRender)
    {
        // Add additional lock during scene rendering (prevents any Actors cache modifications on content streaming threads - eg. when model residency changes)
        Locker.ReadLock();
        _isRendering = true;

        // Register scene
        for (const auto& renderContext : renderContextBatch.Contexts)
            renderContext.List->Scenes.Add(this);
    }
    else if (category == PostRender)
    {
        // Release additional lock
        _isRendering = false;
        Locker.ReadUnlock();
    }
    auto& view = renderContextBatch.GetMainContext().View;
    auto& list = Actors[(int32)category];
    _drawListData = list.Get();
    _drawListSize = list.Count();
    _drawBatch = &renderContextBatch;

    // Setup frustum data
    const int32 frustumsCount = renderContextBatch.Contexts.Count();
    _drawFrustumsData.Resize(frustumsCount);
    for (int32 i = 0; i < frustumsCount; i++)
        _drawFrustumsData.Get()[i] = renderContextBatch.Contexts.Get()[i].View.CullingFrustum;

    // Draw all visual components
    _drawListIndex = -1;
#if PLATFORM_THREADS_LIMIT > 1
    if (_drawListSize >= 64 && category == SceneDrawAsync && renderContextBatch.EnableAsync)
    {
        // Run in async via Job System
        Function<void(int32)> func;
        func.Bind<SceneRendering, &SceneRendering::DrawActorsJob>(this);
        const int64 waitLabel = JobSystem::Dispatch(func, JobSystem::GetThreadsCount());
        renderContextBatch.WaitLabels.Add(waitLabel);
    }
    else
#endif
    {
        // Scene is small so draw on a main-thread
        DrawActorsJob(0);
    }

#if USE_EDITOR
    if (EnumHasAnyFlags(view.Pass, DrawPass::GBuffer) && category == SceneDraw)
    {
        // Draw physics shapes
        if (EnumHasAnyFlags(view.Flags, ViewFlags::PhysicsDebug) || view.Mode == ViewMode::PhysicsColliders)
        {
            PROFILE_CPU_NAMED("PhysicsDebug");
            const auto* physicsDebugData = PhysicsDebug.Get();
            for (int32 i = 0; i < PhysicsDebug.Count(); i++)
            {
                physicsDebugData[i]->DrawPhysicsDebug(view);
            }
        }

        // Draw light shapes
        if (EnumHasAnyFlags(view.Flags, ViewFlags::LightsDebug))
        {
            PROFILE_CPU_NAMED("LightsDebug");
            const LightsDebugCallback* lightsDebugData = LightsDebug.Get();
            for (int32 i = 0; i < LightsDebug.Count(); i++)
            {
                lightsDebugData[i](view);
            }
        }
    }
#endif
}

void SceneRendering::CollectPostFxVolumes(RenderContext& renderContext)
{
#if SCENE_RENDERING_USE_PROFILER
    PROFILE_CPU();
#endif
    for (int32 i = 0; i < PostFxProviders.Count(); i++)
    {
        PostFxProviders.Get()[i]->Collect(renderContext);
    }
}

void SceneRendering::Clear()
{
    ScopeWriteLock lock(Locker);
    for (auto* listener : _listeners)
    {
        listener->OnSceneRenderingClear(this);
        listener->_scenes.Remove(this);
    }
    _listeners.Clear();
    for (auto& e : Actors)
        e.Clear();
    for (auto& e : FreeActors)
        e.Clear();
#if USE_EDITOR
    PhysicsDebug.Clear();
#endif
}

void SceneRendering::AddActor(Actor* a, int32& key)
{
    if (key != -1)
        return;
    PROFILE_MEM(Graphics);
    CHECK_SCENE_EDIT_ACCESS();
    const int32 category = a->_drawCategory;
    ScopeWriteLock lock(Locker);
    auto& list = Actors[category];
    if (FreeActors[category].HasItems())
    {
        // Use existing item
        key = FreeActors[category].Pop();
    }
    else
    {
        // Add a new item
        key = list.Count();
        list.AddOne();
    }
    auto& e = list[key];
    e.Actor = a;
    e.LayerMask = a->GetLayerMask();
    e.Bounds = a->GetSphere();
    e.NoCulling = a->_drawNoCulling;
    for (auto* listener : _listeners)
        listener->OnSceneRenderingAddActor(a);
}

void SceneRendering::UpdateActor(Actor* a, int32& key, ISceneRenderingListener::UpdateFlags flags)
{
    const int32 category = a->_drawCategory;
    bool lock = !_isRendering || ((int32)flags & (int32)ISceneRenderingListener::AutoDelayDuringRendering) == 0; // Allow updating actors during rendering
    if (lock)
        Locker.ReadLock(); // Read-access only as list doesn't get resized (like Add/Remove do) so allow updating actors from different threads at once
    auto& list = Actors[category];
    if (list.Count() > key && key >= 0) // Ignore invalid key softly
    {
        auto& e = list[key];
        if (e.Actor == a)
        {
            for (auto* listener : _listeners)
                listener->OnSceneRenderingUpdateActor(a, e.Bounds, flags);
            if (flags & ISceneRenderingListener::Layer)
                e.LayerMask = a->GetLayerMask();
            if (flags & ISceneRenderingListener::Bounds)
                e.Bounds = a->GetSphere();
        }
    }
    if (lock)
        Locker.ReadUnlock();
}

void SceneRendering::RemoveActor(Actor* a, int32& key)
{
    CHECK_SCENE_EDIT_ACCESS();
    const int32 category = a->_drawCategory;
    ScopeWriteLock lock(Locker);
    auto& list = Actors[category];
    if (list.Count() > key || key < 0) // Ignore invalid key softly (eg. list after batch clear during scene unload)
    {
        auto& e = list.Get()[key];
        if (e.Actor == a)
        {
            for (auto* listener : _listeners)
                listener->OnSceneRenderingRemoveActor(a);
            e.Actor = nullptr;
            e.LayerMask = 0;
            FreeActors[category].Add(key);
        }
    }
    key = -1;
}

#define FOR_EACH_BATCH_ACTOR const int64 count = _drawListSize; while (true) { const int64 index = Platform::InterlockedIncrement(&_drawListIndex); if (index >= count) break; auto e = _drawListData[index];
#define CHECK_ACTOR ((view.RenderLayersMask.Mask & e.LayerMask) && (e.NoCulling || FrustumsListCull(e.Bounds, _drawFrustumsData)))
#define CHECK_ACTOR_SINGLE_FRUSTUM ((view.RenderLayersMask.Mask & e.LayerMask) && (e.NoCulling || view.CullingFrustum.Intersects(e.Bounds)))
#if SCENE_RENDERING_USE_PROFILER_PER_ACTOR
#define DRAW_ACTOR(mode) PROFILE_CPU_ACTOR(e.Actor); e.Actor->Draw(mode)
#else
#define DRAW_ACTOR(mode) e.Actor->Draw(mode)
#endif

void SceneRendering::DrawActorsJob(int32)
{
    PROFILE_CPU();
    PROFILE_MEM(Graphics);
    auto& mainContext = _drawBatch->GetMainContext();
    const auto& view = mainContext.View;
    if (view.StaticFlagsMask != StaticFlags::None)
    {
        // Static-flags culling
        FOR_EACH_BATCH_ACTOR
            e.Bounds.Center -= view.Origin;
            if (CHECK_ACTOR && (e.Actor->GetStaticFlags() & view.StaticFlagsMask) == view.StaticFlagsCompare)
            {
                DRAW_ACTOR(*_drawBatch);
            }
        }
    }
    else if (view.Origin.IsZero() && _drawFrustumsData.Count() == 1)
    {
        // Fast path for no origin shifting with a single context
        FOR_EACH_BATCH_ACTOR
            if (CHECK_ACTOR_SINGLE_FRUSTUM)
            {
                DRAW_ACTOR(mainContext);
            }
        }
    }
    else if (view.Origin.IsZero())
    {
        // Fast path for no origin shifting
        FOR_EACH_BATCH_ACTOR
            if (CHECK_ACTOR)
            {
                DRAW_ACTOR(*_drawBatch);
            }
        }
    }
    else
    {
        // Generic case
        FOR_EACH_BATCH_ACTOR
            e.Bounds.Center -= view.Origin;
            if (CHECK_ACTOR)
            {
                DRAW_ACTOR(*_drawBatch);
            }
        }
    }
}

#undef FOR_EACH_BATCH_ACTOR
#undef CHECK_ACTOR
#undef DRAW_ACTOR
