// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.

#include "SceneRendering.h"
#include "Scene.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderView.h"

#define SCENE_RENDERING_USE_PROFILER 0
#define SCENE_RENDERING_USE_SIMD 0

#if SCENE_RENDERING_USE_PROFILER
#include "Engine/Profiler/ProfilerCPU.h"
#endif

#if SCENE_RENDERING_USE_SIMD

#include "Engine/Core/SIMD.h"

ALIGN_BEGIN(16) struct CullDataSIMD
{
    float xs[8];
    float ys[8];
    float zs[8];
    float ds[8];
} ALIGN_END(16);

#endif

int32 SceneRendering::DrawEntries::Add(Actor* obj)
{
    int32 key = 0;
    for (; key < List.Count(); key++)
    {
        if (List[key].Actor == nullptr)
            break;
    }
    if (key == List.Count())
        List.AddOne();
    auto& e = List[key];
    e.Actor = obj;
    e.LayerMask = obj->GetLayerMask();
    e.Bounds = obj->GetSphere();
    return key;
}

void SceneRendering::DrawEntries::Update(Actor* obj, int32 key)
{
    if (List.IsEmpty())
        return;
    auto& e = List[key];
    ASSERT_LOW_LAYER(obj == e.Actor);
    e.LayerMask = obj->GetLayerMask();
    e.Bounds = obj->GetSphere();
}

void SceneRendering::DrawEntries::Remove(Actor* obj, int32 key)
{
    if (List.IsEmpty())
        return;
    auto& e = List[key];
    ASSERT_LOW_LAYER(obj == e.Actor);
    e.Actor = nullptr;
    e.LayerMask = 0;
}

void SceneRendering::DrawEntries::Clear()
{
    List.Clear();
}

void SceneRendering::DrawEntries::CullAndDraw(RenderContext& renderContext)
{
    auto& view = renderContext.View;
    const BoundingFrustum frustum = view.CullingFrustum;
#if SCENE_RENDERING_USE_SIMD
    CullDataSIMD cullData;
    {
        // Near
        auto plane = view.Frustum.GetNear();
        cullData.xs[0] = plane.Normal.X;
        cullData.ys[0] = plane.Normal.Y;
        cullData.zs[0] = plane.Normal.Z;
        cullData.ds[0] = plane.D;

        // Far
        plane = view.Frustum.GetFar();
        cullData.xs[1] = plane.Normal.X;
        cullData.ys[1] = plane.Normal.Y;
        cullData.zs[1] = plane.Normal.Z;
        cullData.ds[1] = plane.D;

        // Left
        plane = view.Frustum.GetLeft();
        cullData.xs[2] = plane.Normal.X;
        cullData.ys[2] = plane.Normal.Y;
        cullData.zs[2] = plane.Normal.Z;
        cullData.ds[2] = plane.D;

        // Right
        plane = view.Frustum.GetRight();
        cullData.xs[3] = plane.Normal.X;
        cullData.ys[3] = plane.Normal.Y;
        cullData.zs[3] = plane.Normal.Z;
        cullData.ds[3] = plane.D;

        // Top
        plane = view.Frustum.GetTop();
        cullData.xs[4] = plane.Normal.X;
        cullData.ys[4] = plane.Normal.Y;
        cullData.zs[4] = plane.Normal.Z;
        cullData.ds[4] = plane.D;

        // Bottom
        plane = view.Frustum.GetBottom();
        cullData.xs[5] = plane.Normal.X;
        cullData.ys[5] = plane.Normal.Y;
        cullData.zs[5] = plane.Normal.Z;
        cullData.ds[5] = plane.D;

        // Extra 0
        cullData.xs[6] = 0;
        cullData.ys[6] = 0;
        cullData.zs[6] = 0;
        cullData.ds[6] = 0;

        // Extra 1
        cullData.xs[7] = 0;
        cullData.ys[7] = 0;
        cullData.zs[7] = 0;
        cullData.ds[7] = 0;
    }

    SimdVector4 px = SIMD::Load(cullData.xs);
    SimdVector4 py = SIMD::Load(cullData.ys);
    SimdVector4 pz = SIMD::Load(cullData.zs);
    SimdVector4 pd = SIMD::Load(cullData.ds);
    SimdVector4 px2 = SIMD::Load(&cullData.xs[4]);
    SimdVector4 py2 = SIMD::Load(&cullData.ys[4]);
    SimdVector4 pz2 = SIMD::Load(&cullData.zs[4]);
    SimdVector4 pd2 = SIMD::Load(&cullData.ds[4]);

    for (int32 i = 0; i < List.Count(); i++)
    {
        auto e = List[i];

        SimdVector4 cx = SIMD::Splat(e.Bounds.Center.X);
        SimdVector4 cy = SIMD::Splat(e.Bounds.Center.Y);
        SimdVector4 cz = SIMD::Splat(e.Bounds.Center.Z);
        SimdVector4 r = SIMD::Splat(-e.Bounds.Radius);

        SimdVector4 t = SIMD::Mul(cx, px);
        t = SIMD::Add(t, SIMD::Mul(cy, py));
        t = SIMD::Add(t, SIMD::Mul(cz, pz));
        t = SIMD::Add(t, pd);
        t = SIMD::Sub(t, r);
        if (SIMD::MoveMask(t))
            continue;

        t = SIMD::Mul(cx, px2);
        t = SIMD::Add(t, SIMD::Mul(cy, py2));
        t = SIMD::Add(t, SIMD::Mul(cz, pz2));
        t = SIMD::Add(t, pd2);
        t = SIMD::Sub(t, r);
        if (SIMD::MoveMask(t))
            continue;

        if (view.RenderLayersMask.Mask & e.LayerMask)
        {
#if SCENE_RENDERING_USE_PROFILER
            PROFILE_CPU();
#if TRACY_ENABLE
            ___tracy_scoped_zone.Name(*e.Actor->GetName(), e.Actor->GetName().Length());
#endif
#endif
            e.Actor->Draw(renderContext);
        }
    }
#else
    for (int32 i = 0; i < List.Count(); i++)
    {
        auto e = List[i];
        if (view.RenderLayersMask.Mask & e.LayerMask && frustum.Intersects(e.Bounds))
        {
#if SCENE_RENDERING_USE_PROFILER
            PROFILE_CPU();
#if TRACY_ENABLE
            ___tracy_scoped_zone.Name(*e.Actor->GetName(), e.Actor->GetName().Length());
#endif
#endif
            e.Actor->Draw(renderContext);
        }
    }
#endif
}

void SceneRendering::DrawEntries::CullAndDrawOffline(RenderContext& renderContext)
{
    auto& view = renderContext.View;
    const BoundingFrustum frustum = view.CullingFrustum;
#if SCENE_RENDERING_USE_SIMD
    CullDataSIMD cullData;
    {
        // Near
        auto plane = view.Frustum.GetNear();
        cullData.xs[0] = plane.Normal.X;
        cullData.ys[0] = plane.Normal.Y;
        cullData.zs[0] = plane.Normal.Z;
        cullData.ds[0] = plane.D;

        // Far
        plane = view.Frustum.GetFar();
        cullData.xs[1] = plane.Normal.X;
        cullData.ys[1] = plane.Normal.Y;
        cullData.zs[1] = plane.Normal.Z;
        cullData.ds[1] = plane.D;

        // Left
        plane = view.Frustum.GetLeft();
        cullData.xs[2] = plane.Normal.X;
        cullData.ys[2] = plane.Normal.Y;
        cullData.zs[2] = plane.Normal.Z;
        cullData.ds[2] = plane.D;

        // Right
        plane = view.Frustum.GetRight();
        cullData.xs[3] = plane.Normal.X;
        cullData.ys[3] = plane.Normal.Y;
        cullData.zs[3] = plane.Normal.Z;
        cullData.ds[3] = plane.D;

        // Top
        plane = view.Frustum.GetTop();
        cullData.xs[4] = plane.Normal.X;
        cullData.ys[4] = plane.Normal.Y;
        cullData.zs[4] = plane.Normal.Z;
        cullData.ds[4] = plane.D;

        // Bottom
        plane = view.Frustum.GetBottom();
        cullData.xs[5] = plane.Normal.X;
        cullData.ys[5] = plane.Normal.Y;
        cullData.zs[5] = plane.Normal.Z;
        cullData.ds[5] = plane.D;

        // Extra 0
        cullData.xs[6] = 0;
        cullData.ys[6] = 0;
        cullData.zs[6] = 0;
        cullData.ds[6] = 0;

        // Extra 1
        cullData.xs[7] = 0;
        cullData.ys[7] = 0;
        cullData.zs[7] = 0;
        cullData.ds[7] = 0;
    }

    SimdVector4 px = SIMD::Load(cullData.xs);
    SimdVector4 py = SIMD::Load(cullData.ys);
    SimdVector4 pz = SIMD::Load(cullData.zs);
    SimdVector4 pd = SIMD::Load(cullData.ds);
    SimdVector4 px2 = SIMD::Load(&cullData.xs[4]);
    SimdVector4 py2 = SIMD::Load(&cullData.ys[4]);
    SimdVector4 pz2 = SIMD::Load(&cullData.zs[4]);
    SimdVector4 pd2 = SIMD::Load(&cullData.ds[4]);

    for (int32 i = 0; i < List.Count(); i++)
    {
        auto e = List[i];

        SimdVector4 cx = SIMD::Splat(e.Bounds.Center.X);
        SimdVector4 cy = SIMD::Splat(e.Bounds.Center.Y);
        SimdVector4 cz = SIMD::Splat(e.Bounds.Center.Z);
        SimdVector4 r = SIMD::Splat(-e.Bounds.Radius);

        SimdVector4 t = SIMD::Mul(cx, px);
        t = SIMD::Add(t, SIMD::Mul(cy, py));
        t = SIMD::Add(t, SIMD::Mul(cz, pz));
        t = SIMD::Add(t, pd);
        t = SIMD::Sub(t, r);
        if (SIMD::MoveMask(t))
            continue;

        t = SIMD::Mul(cx, px2);
        t = SIMD::Add(t, SIMD::Mul(cy, py2));
        t = SIMD::Add(t, SIMD::Mul(cz, pz2));
        t = SIMD::Add(t, pd2);
        t = SIMD::Sub(t, r);
        if (SIMD::MoveMask(t))
            continue;

        if (view.RenderLayersMask.Mask & e.LayerMask && e.Actor->GetStaticFlags() & renderContext.View.StaticFlagsMask)
        {
#if SCENE_RENDERING_USE_PROFILER
            PROFILE_CPU();
#if TRACY_ENABLE
            ___tracy_scoped_zone.Name(*e.Actor->GetName(), e.Actor->GetName().Length());
#endif
#endif
            e.Actor->Draw(renderContext);
        }
    }
#else
    for (int32 i = 0; i < List.Count(); i++)
    {
        auto e = List[i];
        if (view.RenderLayersMask.Mask & e.LayerMask && frustum.Intersects(e.Bounds) && e.Actor->GetStaticFlags() & view.StaticFlagsMask)
        {
#if SCENE_RENDERING_USE_PROFILER
            PROFILE_CPU();
#if TRACY_ENABLE
            ___tracy_scoped_zone.Name(*e.Actor->GetName(), e.Actor->GetName().Length());
#endif
#endif
            e.Actor->Draw(renderContext);
        }
    }
#endif
}

SceneRendering::SceneRendering(::Scene* scene)
    : Scene(scene)
{
}

void SceneRendering::Draw(RenderContext& renderContext)
{
    // Skip if disabled
    if (!Scene->GetIsActive())
        return;
    auto& view = renderContext.View;

    // Draw all visual components
    if (view.IsOfflinePass)
    {
        Geometry.CullAndDrawOffline(renderContext);
        if (view.Pass & DrawPass::GBuffer)
        {
            Common.CullAndDrawOffline(renderContext);
            for (int32 i = 0; i < CommonNoCulling.Count(); i++)
            {
                auto actor = CommonNoCulling[i];
                if (actor->GetStaticFlags() & view.StaticFlagsMask && view.RenderLayersMask.HasLayer(actor->GetLayer()))
                    actor->Draw(renderContext);
            }
        }
    }
    else
    {
        Geometry.CullAndDraw(renderContext);
        if (view.Pass & DrawPass::GBuffer)
        {
            Common.CullAndDraw(renderContext);
            for (int32 i = 0; i < CommonNoCulling.Count(); i++)
            {
                auto actor = CommonNoCulling[i];
                if (view.RenderLayersMask.HasLayer(actor->GetLayer()))
                {
#if SCENE_RENDERING_USE_PROFILER
                    PROFILE_CPU();
#if TRACY_ENABLE
                    ___tracy_scoped_zone.Name(*actor->GetName(), actor->GetName().Length());
#endif
#endif
                    actor->Draw(renderContext);
                }
            }
        }
    }
#if USE_EDITOR
    if (view.Pass & DrawPass::GBuffer)
    {
        // Draw physics shapes
        if (view.Flags & ViewFlags::PhysicsDebug || view.Mode == ViewMode::PhysicsColliders)
        {
            for (int32 i = 0; i < PhysicsDebug.Count(); i++)
            {
                PhysicsDebug[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[i]->Collect(renderContext);
    }
}

void SceneRendering::Clear()
{
    Geometry.Clear();
    Common.Clear();
    CommonNoCulling.Clear();
#if USE_EDITOR
    PhysicsDebug.Clear();
#endif
}
