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

#include "Foliage.h"
#include "FoliageType.h"
#include "FoliageCluster.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Random.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Content/Deprecated.h"
#if !FOLIAGE_USE_SINGLE_QUAD_TREE
#include "Engine/Threading/JobSystem.h"
#endif
#include "Engine/Level/SceneQuery.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Renderer/GlobalSignDistanceFieldPass.h"
#include "Engine/Renderer/GI/GlobalSurfaceAtlasPass.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Utilities/Encryption.h"

#define FOLIAGE_GET_DRAW_MODES(renderContext, type) (type._drawModes & renderContext.View.Pass & renderContext.View.GetShadowsDrawPassMask(type.ShadowsMode))
#define FOLIAGE_CAN_DRAW(renderContext, type) (type.IsReady() && FOLIAGE_GET_DRAW_MODES(renderContext, type) != DrawPass::None && type.Model->CanBeRendered())

Foliage::Foliage(const SpawnParams& params)
    : Actor(params)
{
    _disableFoliageTypeEvents = false;

    // When using separate quad-tree for each foliage type we can run async job to draw them in separate, otherwise just draw whole foliage in async as one
#if FOLIAGE_USE_SINGLE_QUAD_TREE
    _drawCategory = SceneRendering::SceneDrawAsync;
#else
    _drawCategory = SceneRendering::SceneDraw;
#endif
}

void Foliage::AddToCluster(ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_SIZE>& clusters, FoliageCluster* cluster, FoliageInstance& instance)
{
    ASSERT_LOW_LAYER(instance.Bounds.Radius > ZeroTolerance);

    // Minor clusters don't use bounds intersection but try to find the first free cluster instead
    if (cluster->IsMinor)
    {
        // Insert into the first non-full child cluster or subdivide 1st child
#define CHECK_CHILD(idx) \
        if (cluster->Children[idx]->Instances.Count() < FOLIAGE_CLUSTER_CAPACITY) \
        { \
           cluster->Children[idx]->Instances.Add(&instance); \
           return; \
        }
        CHECK_CHILD(3);
        CHECK_CHILD(2);
        CHECK_CHILD(1);
        cluster = cluster->Children[0];
#undef CHECK_CHILD
    }
    else
    {
        // Find target cluster
        ASSERT(cluster->Bounds.Intersects(instance.Bounds));
        while (cluster->Children[0])
        {
#define CHECK_CHILD(idx) \
			if (cluster->Children[idx]->Bounds.Intersects(instance.Bounds)) \
			{ \
				cluster = cluster->Children[idx]; \
				continue; \
			}
            CHECK_CHILD(0);
            CHECK_CHILD(1);
            CHECK_CHILD(2);
            CHECK_CHILD(3);
#undef CHECK_CHILD
        }
    }

    // Check if it's not full
    if (cluster->Instances.Count() != FOLIAGE_CLUSTER_CAPACITY)
    {
        // Insert into cluster
        cluster->Instances.Add(&instance);
    }
    else
    {
        // Subdivide cluster
        const int32 count = clusters.Count();
        clusters.Resize(count + 4);
        cluster->Children[0] = &clusters[count + 0];
        cluster->Children[1] = &clusters[count + 1];
        cluster->Children[2] = &clusters[count + 2];
        cluster->Children[3] = &clusters[count + 3];

        // Setup children
        const Vector3 min = cluster->Bounds.Minimum;
        const Vector3 max = cluster->Bounds.Maximum;
        const Vector3 size = max - min;
        cluster->Children[0]->Init(BoundingBox(min, min + size * Vector3(0.5f, 1.0f, 0.5f)));
        cluster->Children[1]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.5f), max));
        cluster->Children[2]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.0f), min + size * Vector3(1.0f, 1.0f, 0.5f)));
        cluster->Children[3]->Init(BoundingBox(min + size * Vector3(0.0f, 0.0f, 0.5f), min + size * Vector3(0.5f, 1.0f, 1.0f)));
        if (cluster->IsMinor || size.MinValue() < 1.0f)
        {
            // Mark children as minor to avoid infinite subdivision
            cluster->IsMinor = true;
            cluster->Children[0]->IsMinor = true;
            cluster->Children[1]->IsMinor = true;
            cluster->Children[2]->IsMinor = true;
            cluster->Children[3]->IsMinor = true;
        }

        // Move instances to a proper cells
        for (int32 i = 0; i < cluster->Instances.Count(); i++)
        {
            AddToCluster(clusters, cluster, *cluster->Instances[i]);
        }
        cluster->Instances.Clear();
        AddToCluster(clusters, cluster, instance);
    }
}

#if !FOLIAGE_USE_SINGLE_QUAD_TREE && FOLIAGE_USE_DRAW_CALLS_BATCHING

void Foliage::DrawInstance(DrawContext& context, FoliageInstance& instance, Model* model, int32 lod, float lodDitherFactor, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const
{
    const auto& meshes = model->LODs.Get()[lod].Meshes;
    for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
    {
        auto& drawCall = drawCallsLists[lod][meshIndex];
        if (!drawCall.Material)
            continue;

        DrawKey key;
        key.Mat = drawCall.Material;
        key.Geo = &meshes.Get()[meshIndex];
        key.Lightmap = instance.Lightmap.TextureIndex;
        auto* e = result.TryGet(key);
        if (!e)
        {
            e = &result.Add(key, BatchedDrawCall(context.RenderContext.List))->Value;
            ASSERT_LOW_LAYER(key.Mat);
            e->DrawCall.Material = key.Mat;
            e->DrawCall.Surface.Lightmap = EnumHasAnyFlags(_staticFlags, StaticFlags::Lightmap) && _scene ? _scene->LightmapsData.GetReadyLightmap(key.Lightmap) : nullptr;
            e->DrawCall.Surface.GeometrySize = key.Geo->GetBox().GetSize();
        }

        // Add instance to the draw batch
        auto& instanceData = e->Instances.AddOne();
        Matrix world;
        const Transform transform = _transform.LocalToWorld(instance.Transform);
        const Float3 translation = transform.Translation - context.ViewOrigin;
        Matrix::Transformation(transform.Scale, transform.Orientation, translation, world);
        constexpr float worldDeterminantSign = 1.0f;
        instanceData.Store(world, world, instance.Lightmap.UVsArea, drawCall.Surface.GeometrySize, instance.Random, worldDeterminantSign, lodDitherFactor);
    }
}

void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const
{
    // Skip clusters that around too far from view
    if (Float3::Distance(context.LodView.Position, cluster->TotalBoundsSphere.Center - context.LodView.Origin) - (float)cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance)
        return;
    //DebugDraw::DrawBox(cluster->Bounds, Color::Red);

    // Draw visible children
    if (cluster->Children[0])
    {
        // Don't store instances in non-leaf nodes
        ASSERT_LOW_LAYER(cluster->Instances.IsEmpty());

        BoundingBox box;
#define DRAW_CLUSTER(idx) \
        box = cluster->Children[idx]->TotalBounds; \
        box.Minimum -= context.ViewOrigin; \
        box.Maximum -= context.ViewOrigin; \
		if (context.RenderContext.View.CullingFrustum.Intersects(box)) \
			DrawCluster(context, cluster->Children[idx], drawCallsLists, result)
        DRAW_CLUSTER(0);
        DRAW_CLUSTER(1);
        DRAW_CLUSTER(2);
        DRAW_CLUSTER(3);
#undef 	DRAW_CLUSTER
    }
    else
    {
        // Draw visible instances
        const auto frame = Engine::FrameCount;
        const auto model = context.FoliageType.Model.Get();
        const auto transitionLOD = context.RenderContext.View.Pass != DrawPass::Depth; // Let the main view pass update LOD transitions
        // TODO: move DrawState to be stored per-view (so shadows can fade objects on their own)
        for (int32 i = 0; i < cluster->Instances.Count(); i++)
        {
            auto& instance = *cluster->Instances.Get()[i];
            BoundingSphere sphere = instance.Bounds;
            sphere.Center -= context.ViewOrigin;
            if (Float3::Distance(context.LodView.Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance &&
                context.RenderContext.View.CullingFrustum.Intersects(sphere) &&
                RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, (float)sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq)
            {
                const auto modelFrame = instance.DrawState.PrevFrame + 1;

                // Select a proper LOD index (model may be culled)
                int32 lodIndex = RenderTools::ComputeModelLOD(model, sphere.Center, (float)sphere.Radius, context.RenderContext);
                if (lodIndex == -1)
                {
                    // Handling model fade-out transition
                    if (modelFrame == frame && instance.DrawState.PrevLOD != -1)
                    {
                        if (transitionLOD)
                        {
                            // Check if start transition
                            if (instance.DrawState.LODTransition == 255)
                            {
                                instance.DrawState.LODTransition = 0;
                            }

                            RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition);

                            // Check if end transition
                            if (instance.DrawState.LODTransition == 255)
                            {
                                instance.DrawState.PrevLOD = lodIndex;
                            }
                            else
                            {
                                const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD);
                                const float normalizedProgress = static_cast<float>(instance.DrawState.LODTransition) * (1.0f / 255.0f);
                                DrawInstance(context, instance, model, prevLOD, normalizedProgress, drawCallsLists, result);
                            }
                        }
                        else if (instance.DrawState.LODTransition < 255)
                        {
                            const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD);
                            const float normalizedProgress = static_cast<float>(instance.DrawState.LODTransition) * (1.0f / 255.0f);
                            DrawInstance(context, instance, model, prevLOD, normalizedProgress, drawCallsLists, result);
                        }
                    }
                    instance.DrawState.PrevFrame = frame;
                    continue;
                }
                lodIndex += context.RenderContext.View.ModelLODBias;
                lodIndex = model->ClampLODIndex(lodIndex);

                if (transitionLOD)
                {
                    // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports)
                    if (modelFrame == frame)
                    {
                        // Check if start transition
                        if (instance.DrawState.PrevLOD != lodIndex && instance.DrawState.LODTransition == 255)
                        {
                            instance.DrawState.LODTransition = 0;
                        }

                        RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition);

                        // Check if end transition
                        if (instance.DrawState.LODTransition == 255)
                        {
                            instance.DrawState.PrevLOD = lodIndex;
                        }
                    }
                    // Check if there was a gap between frames in drawing this model instance
                    else if (modelFrame < frame || instance.DrawState.PrevLOD == -1)
                    {
                        // Reset state
                        instance.DrawState.PrevLOD = lodIndex;
                        instance.DrawState.LODTransition = 0;
                    }
                }

                // Draw
                if (instance.DrawState.PrevLOD == lodIndex)
                {
                    DrawInstance(context, instance, model, lodIndex, 0.0f, drawCallsLists, result);
                }
                else if (instance.DrawState.PrevLOD == -1)
                {
                    const float normalizedProgress = static_cast<float>(instance.DrawState.LODTransition) * (1.0f / 255.0f);
                    DrawInstance(context, instance, model, lodIndex, 1.0f - normalizedProgress, drawCallsLists, result);
                }
                else
                {
                    const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD);
                    const float normalizedProgress = static_cast<float>(instance.DrawState.LODTransition) * (1.0f / 255.0f);
                    DrawInstance(context, instance, model, prevLOD, normalizedProgress, drawCallsLists, result);
                    DrawInstance(context, instance, model, lodIndex, normalizedProgress - 1.0f, drawCallsLists, result);
                }

                //DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen);

                if (transitionLOD)
                    instance.DrawState.PrevFrame = frame;
            }
        }
    }
}

#else

void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::DrawInfo& draw)
{
    // Skip clusters that around too far from view
    if (Float3::Distance(context.LodView.Position, cluster->TotalBoundsSphere.Center - context.LodView.Origin) - (float)cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance)
        return;
    //DebugDraw::DrawBox(cluster->Bounds, Color::Red);

    // Draw visible children
    if (cluster->Children[0])
    {
        // Don't store instances in non-leaf nodes
        ASSERT_LOW_LAYER(cluster->Instances.IsEmpty());

        BoundingBox box;
#define DRAW_CLUSTER(idx) \
        box = cluster->Children[idx]->TotalBounds; \
        box.Minimum -= context.ViewOrigin; \
        box.Maximum -= context.ViewOrigin; \
		if (context.RenderContext.View.CullingFrustum.Intersects(box)) \
			DrawCluster(context, cluster->Children[idx], draw)
        DRAW_CLUSTER(0);
        DRAW_CLUSTER(1);
        DRAW_CLUSTER(2);
        DRAW_CLUSTER(3);
#undef 	DRAW_CLUSTER
    }
    else
    {
        // Draw visible instances
        const auto frame = Engine::FrameCount;
        for (int32 i = 0; i < cluster->Instances.Count(); i++)
        {
            auto& instance = *cluster->Instances[i];
            auto& type = FoliageTypes[instance.Type];
            BoundingSphere sphere = instance.Bounds;
            sphere.Center -= context.ViewOrigin;

            // Check if can draw this instance
            if (type._canDraw &&
                Float3::Distance(context.LodView.Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance &&
                context.RenderContext.View.CullingFrustum.Intersects(sphere) &&
                RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq)
            {
                Matrix world;
                const Transform transform = _transform.LocalToWorld(instance.Transform);
                const Float3 translation = transform.Translation - context.ViewOrigin;
                Matrix::Transformation(transform.Scale, transform.Orientation, translation, world);

                // Disable motion blur
                instance.DrawState.PrevWorld = world;

                // Draw model
                draw.Lightmap = _scene->LightmapsData.GetReadyLightmap(instance.Lightmap.TextureIndex);
                draw.LightmapUVs = &instance.Lightmap.UVsArea;
                draw.Buffer = &type.Entries;
                draw.World = &world;
                draw.DrawState = &instance.DrawState;
                draw.Bounds = sphere;
                draw.PerInstanceRandom = instance.Random;
                draw.DrawModes = type._drawModes;
                draw.SetStencilValue(_layer);
                type.Model->Draw(context.RenderContext, draw);

                //DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen);

                instance.DrawState.PrevFrame = frame;
            }
        }
    }
}

#endif

#if !FOLIAGE_USE_SINGLE_QUAD_TREE

void Foliage::DrawClusterGlobalSDF(class GlobalSignDistanceFieldPass* globalSDF, const BoundingBox& globalSDFBounds, FoliageCluster* cluster, const FoliageType& type)
{
    if (cluster->Children[0])
    {
        // Draw children recursive
#define DRAW_CLUSTER(idx) \
		if (globalSDFBounds.Intersects(cluster->Children[idx]->TotalBounds)) \
			DrawClusterGlobalSDF(globalSDF, globalSDFBounds, cluster->Children[idx], type)
        DRAW_CLUSTER(0);
        DRAW_CLUSTER(1);
        DRAW_CLUSTER(2);
        DRAW_CLUSTER(3);
#undef 	DRAW_CLUSTER
    }
    else
    {
        // Draw visible instances
        for (int32 i = 0; i < cluster->Instances.Count(); i++)
        {
            auto& instance = *cluster->Instances[i];
            if (CollisionsHelper::BoxIntersectsSphere(globalSDFBounds, instance.Bounds))
            {
                const Transform transform = _transform.LocalToWorld(instance.Transform);
                BoundingBox bounds;
                BoundingBox::FromSphere(instance.Bounds, bounds);
                globalSDF->RasterizeModelSDF(this, type.Model->SDF, transform, bounds);
            }
        }
    }
}

void Foliage::DrawClusterGlobalSA(GlobalSurfaceAtlasPass* globalSA, const Vector4& cullingPosDistance, FoliageCluster* cluster, const FoliageType& type, const BoundingBox& localBounds)
{
    if (cluster->Children[0])
    {
        // Draw children recursive
#define DRAW_CLUSTER(idx) \
        if (CollisionsHelper::DistanceBoxPoint(cluster->Children[idx]->TotalBounds, Vector3(cullingPosDistance)) < cullingPosDistance.W) \
            DrawClusterGlobalSA(globalSA, cullingPosDistance, cluster->Children[idx], type, localBounds)
        DRAW_CLUSTER(0);
        DRAW_CLUSTER(1);
        DRAW_CLUSTER(2);
        DRAW_CLUSTER(3);
#undef 	DRAW_CLUSTER
    }
    else
    {
        // Draw visible instances
        for (int32 i = 0; i < cluster->Instances.Count(); i++)
        {
            auto& instance = *cluster->Instances[i];
            if (CollisionsHelper::DistanceSpherePoint(instance.Bounds, Vector3(cullingPosDistance)) < cullingPosDistance.W)
            {
                const Transform transform = _transform.LocalToWorld(instance.Transform);
                globalSA->RasterizeActor(this, &instance, instance.Bounds, transform, localBounds, MAX_uint32, true, 0.5f);
            }
        }
    }
}

void Foliage::DrawFoliageJob(int32 i)
{
    PROFILE_CPU();
    PROFILE_MEM(Graphics);
    const FoliageType& type = FoliageTypes[i];
    if (type._canDraw)
    {
        DrawCallsList drawCallsLists[MODEL_MAX_LODS];
        for (RenderContext& renderContext : _renderContextBatch->Contexts)
        {
#if !FOLIAGE_USE_SINGLE_QUAD_TREE && FOLIAGE_USE_DRAW_CALLS_BATCHING
            DrawType(renderContext, type, drawCallsLists);
#else
            Mesh::DrawInfo draw;
            draw.Flags = GetStaticFlags();
            draw.DrawModes = (DrawPass)(DrawPass::Default & renderContext.View.Pass);
            draw.LODBias = 0;
            draw.ForcedLOD = -1;
            draw.VertexColors = nullptr;
            draw.Deformation = nullptr;
            DrawType(renderContext, type, draw);
#endif
        }
    }
}

#endif

#if !FOLIAGE_USE_SINGLE_QUAD_TREE && FOLIAGE_USE_DRAW_CALLS_BATCHING
void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, DrawCallsList* drawCallsLists)
#else
void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Mesh::DrawInfo& draw)
#endif
{
    if (!type.Root || !FOLIAGE_CAN_DRAW(renderContext, type))
        return;
    const DrawPass typeDrawModes = FOLIAGE_GET_DRAW_MODES(renderContext, type);
    PROFILE_CPU_ASSET(type.Model);
    DrawContext context
    {
        renderContext,
        renderContext.LodProxyView ? *renderContext.LodProxyView : renderContext.View,
        type,
        renderContext.View.Origin,
        Math::Square(Graphics::Shadows::MinObjectPixelSize),
        renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y,
    };
    if (context.RenderContext.View.Pass != DrawPass::Depth)
        context.MinObjectPixelSizeSq = 0.0f; // Don't use it in main view
#if FOLIAGE_USE_DRAW_CALLS_BATCHING
    // Initialize draw calls for foliage type all LODs meshes
    for (int32 lod = 0; lod < type.Model->LODs.Count(); lod++)
    {
        auto& modelLod = type.Model->LODs[lod];
        DrawCallsList& drawCallsList = drawCallsLists[lod];
        const auto& meshes = modelLod.Meshes;
        drawCallsList.Resize(meshes.Count());
        for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
        {
            const auto& mesh = meshes.Get()[meshIndex];
            auto& drawCall = drawCallsList.Get()[meshIndex];
            drawCall.Material = nullptr; // DrawInstance skips draw calls from meshes with unset material

            // Check entry visibility
            const auto& entry = type.Entries[mesh.GetMaterialSlotIndex()];
            if (!entry.Visible || !mesh.IsInitialized())
                continue;
            const MaterialSlot& slot = type.Model->MaterialSlots[mesh.GetMaterialSlotIndex()];

            // Select material
            MaterialBase* material;
            if (entry.Material && entry.Material->IsLoaded())
                material = entry.Material;
            else if (slot.Material && slot.Material->IsLoaded())
                material = slot.Material;
            else
                material = GPUDevice::Instance->GetDefaultMaterial();
            if (!material || !material->IsSurface())
                continue;

            // Select draw modes
            const auto shadowsMode = entry.ShadowsMode & slot.ShadowsMode;
            const auto drawModes = typeDrawModes & renderContext.View.GetShadowsDrawPassMask(shadowsMode) & material->GetDrawModes();
            if (drawModes == DrawPass::None)
                continue;

            drawCall.Material = material;
            drawCall.Surface.GeometrySize = mesh.GetBox().GetSize();
        }
    }

    // Draw instances of the foliage type
    BatchedDrawCalls result(&renderContext.List->Memory);
    DrawCluster(context, type.Root, drawCallsLists, result);

    // Submit draw calls with valid instances added
    for (auto& e : result)
    {
        auto& batch = e.Value;
        if (batch.Instances.IsEmpty())
            continue;
        const auto& mesh = *e.Key.Geo;
        const auto& entry = type.Entries[mesh.GetMaterialSlotIndex()];
        const MaterialSlot& slot = type.Model->MaterialSlots[mesh.GetMaterialSlotIndex()];
        const auto shadowsMode = entry.ShadowsMode & slot.ShadowsMode;
        const auto drawModes = typeDrawModes & renderContext.View.GetShadowsDrawPassMask(shadowsMode) & batch.DrawCall.Material->GetDrawModes();

        // Setup draw call
        mesh.GetDrawCallGeometry(batch.DrawCall);
        batch.DrawCall.InstanceCount = 1;
        auto& firstInstance = batch.Instances[0];
        firstInstance.Load(batch.DrawCall);
#if USE_EDITOR
        if (renderContext.View.Mode == ViewMode::LightmapUVsDensity)
            batch.DrawCall.Surface.LODDitherFactor = type.ScaleInLightmap; // See LightmapUVsDensityMaterialShader
#endif

        if (EnumHasAnyFlags(drawModes, DrawPass::Forward))
        {
            // Transparency requires sorting by depth so convert back the batched draw call into normal draw calls (RenderList impl will handle this)
            DrawCall drawCall = batch.DrawCall;
            for (int32 j = 0; j < batch.Instances.Count(); j++)
            {
                auto& instance = batch.Instances[j];
                instance.Load(drawCall);
                const int32 drawCallIndex = renderContext.List->DrawCalls.Add(drawCall);
                renderContext.List->DrawCallsLists[(int32)DrawCallsListType::Forward].Indices.Add(drawCallIndex);
            }
        }

        // Add draw call batch
        const int32 batchIndex = renderContext.List->BatchedDrawCalls.Add(MoveTemp(batch));

        // Add draw call to proper draw lists
        if (EnumHasAnyFlags(drawModes, DrawPass::Depth))
        {
            renderContext.List->DrawCallsLists[(int32)DrawCallsListType::Depth].PreBatchedDrawCalls.Add(batchIndex);
        }
        if (EnumHasAnyFlags(drawModes, DrawPass::GBuffer))
        {
            if (entry.ReceiveDecals)
                renderContext.List->DrawCallsLists[(int32)DrawCallsListType::GBuffer].PreBatchedDrawCalls.Add(batchIndex);
            else
                renderContext.List->DrawCallsLists[(int32)DrawCallsListType::GBufferNoDecals].PreBatchedDrawCalls.Add(batchIndex);
        }
        if (EnumHasAnyFlags(drawModes, DrawPass::Distortion))
        {
            renderContext.List->DrawCallsLists[(int32)DrawCallsListType::Distortion].PreBatchedDrawCalls.Add(batchIndex);
        }
        if (EnumHasAnyFlags(drawModes, DrawPass::MotionVectors) && (_staticFlags & StaticFlags::Transform) == StaticFlags::None)
        {
            renderContext.List->DrawCallsLists[(int32)DrawCallsListType::MotionVectors].PreBatchedDrawCalls.Add(batchIndex);
        }
    }
#else
    DrawCluster(context, type.Root, draw);
#endif
}

void Foliage::InitType(const RenderView& view, FoliageType& type)
{
    const DrawPass drawModes = type._drawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode);
    type._canDraw = type.IsReady() && drawModes != DrawPass::None && type.Model && type.Model->CanBeRendered();
    bool drawModesDirty = false;
    for (int32 j = 0; j < type.Entries.Count(); j++)
    {
        auto& e = type.Entries[j];
        e.ReceiveDecals = type.ReceiveDecals != 0;
        e.ShadowsMode = type.ShadowsMode;
        if (type._drawModesDirty)
        {
            type._drawModesDirty = 0;
            drawModesDirty = true;
        }
    }
    if (drawModesDirty)
        GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::DrawModes);
}

int32 Foliage::GetInstancesCount() const
{
    return Instances.Count();
}

FoliageInstance Foliage::GetInstance(int32 index) const
{
    return Instances[index];
}

int32 Foliage::GetFoliageTypesCount() const
{
    return FoliageTypes.Count();
}

FoliageType* Foliage::GetFoliageType(int32 index)
{
    CHECK_RETURN(index >= 0 && index < FoliageTypes.Count(), nullptr)
    return &FoliageTypes[index];
}

void Foliage::AddFoliageType(Model* model)
{
    PROFILE_CPU();
    PROFILE_MEM(LevelFoliage);

    // Ensure to have unique model
    CHECK(model);
    for (int32 i = 0; i < FoliageTypes.Count(); i++)
    {
        if (FoliageTypes[i].Model == model)
        {
            LOG(Error, "The given model is already used by other foliage type.");
            return;
        }
    }

    // Add
    _disableFoliageTypeEvents = true;
    auto& item = FoliageTypes.AddOne();
    _disableFoliageTypeEvents = false;

    // Setup
    item.Foliage = this;
    item.Index = FoliageTypes.Count() - 1;
    item.Model = model;
}

void Foliage::RemoveFoliageType(int32 index)
{
    PROFILE_CPU();

    // Remove instances using this foliage type
    if (FoliageTypes.Count() != 1)
    {
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            if (i->Type == index)
            {
                Instances.Remove(i);
                --i;
            }
        }

        // Update all instances using foliage types with higher index to point into a valid type
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            if (i->Type > index)
                i->Type--;
        }
    }
    else
    {
        Instances.Clear();
    }

    // Remove foliage instance type
    for (int32 i = index + 1; i < FoliageTypes.Count(); i++)
    {
        FoliageTypes[i].Index--;
    }
    auto& item = FoliageTypes[index];
    item.Model = nullptr;
    item.Entries.Release();
    FoliageTypes.RemoveAtKeepOrder(index);

    RebuildClusters();
}

int32 Foliage::GetFoliageTypeInstancesCount(int32 index) const
{
    PROFILE_CPU();
    int32 result = 0;
    for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
    {
        if (i->Type == index)
            result++;
    }
    return result;
}

void Foliage::AddInstance(const FoliageInstance& instance)
{
    PROFILE_MEM(LevelFoliage);
    ASSERT(instance.Type >= 0 && instance.Type < FoliageTypes.Count());
    auto type = &FoliageTypes[instance.Type];

    // Add instance
    auto data = Instances.Add(instance);
    data->Bounds = BoundingSphere::Empty;
    data->Random = Random::Rand();
    data->CullDistance = type->CullDistance + type->CullDistanceRandomRange * data->Random;

    // Validate foliage type model
    if (!type->IsReady())
        return;

    // Update bounds
    Vector3 corners[8];
    auto& meshes = type->Model->LODs[0].Meshes;
    const Transform transform = _transform.LocalToWorld(data->Transform);
    for (int32 j = 0; j < meshes.Count(); j++)
    {
        meshes[j].GetBox().GetCorners(corners);

        for (int32 k = 0; k < 8; k++)
        {
            Vector3::Transform(corners[k], transform, corners[k]);
        }
        BoundingSphere meshBounds;
        BoundingSphere::FromPoints(corners, 8, meshBounds);
        ASSERT(meshBounds.Radius > ZeroTolerance);

        BoundingSphere::Merge(data->Bounds, meshBounds, data->Bounds);
    }
    data->Bounds.Radius += ZeroTolerance;
}

void Foliage::RemoveInstance(ChunkedArray<FoliageInstance, FOLIAGE_INSTANCE_CHUNKS_SIZE>::Iterator i)
{
    Instances.Remove(i);
}

void Foliage::SetInstanceTransform(int32 index, const Transform& value)
{
    auto& instance = Instances[index];
    auto type = &FoliageTypes[instance.Type];

    // Change transform
    instance.Transform = value;

    // Update bounds
    instance.Bounds = BoundingSphere::Empty;
    if (!type->IsReady())
        return;
    Vector3 corners[8];
    auto& meshes = type->Model->LODs[0].Meshes;
    const Transform transform = _transform.LocalToWorld(instance.Transform);
    for (int32 j = 0; j < meshes.Count(); j++)
    {
        meshes[j].GetBox().GetCorners(corners);

        for (int32 k = 0; k < 8; k++)
        {
            Vector3::Transform(corners[k], transform, corners[k]);
        }
        BoundingSphere meshBounds;
        BoundingSphere::FromPoints(corners, 8, meshBounds);
        ASSERT(meshBounds.Radius > ZeroTolerance);

        BoundingSphere::Merge(instance.Bounds, meshBounds, instance.Bounds);
    }
    instance.Bounds.Radius += ZeroTolerance;
}

void Foliage::OnFoliageTypeModelLoaded(int32 index)
{
    if (_disableFoliageTypeEvents)
        return;
    PROFILE_CPU();
    PROFILE_MEM(LevelFoliage);
    auto& type = FoliageTypes[index];
    ASSERT(type.IsReady());

    // Update bounds for instances using this type
    bool hasAnyInstance = false;
#if !FOLIAGE_USE_SINGLE_QUAD_TREE
    BoundingBox totalBoundsType, box;
#endif
    {
        PROFILE_CPU_NAMED("Update Bounds");
        Vector3 corners[8];
        auto& meshes = type.Model->LODs[0].Meshes;
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            if (instance.Type != index)
                continue;
            instance.Bounds = BoundingSphere::Empty;
            const Transform transform = _transform.LocalToWorld(instance.Transform);

            // Include all meshes
            for (int32 j = 0; j < meshes.Count(); j++)
            {
                // TODO: cache bounds for all model meshes and reuse later
                meshes[j].GetBox().GetCorners(corners);

                // TODO: use SIMD
                for (int32 k = 0; k < 8; k++)
                {
                    Vector3::Transform(corners[k], transform, corners[k]);
                }
                BoundingSphere meshBounds;
                BoundingSphere::FromPoints(corners, 8, meshBounds);
                BoundingSphere::Merge(instance.Bounds, meshBounds, instance.Bounds);
            }

#if !FOLIAGE_USE_SINGLE_QUAD_TREE
            // TODO: use SIMD
            BoundingBox::FromSphere(instance.Bounds, box);
            if (hasAnyInstance)
                BoundingBox::Merge(totalBoundsType, box, totalBoundsType);
            else
                totalBoundsType = box;
#endif
            hasAnyInstance = true;
        }
    }
    if (!hasAnyInstance)
        return;

    // Refresh quad-tree
#if FOLIAGE_USE_SINGLE_QUAD_TREE
    RebuildClusters();
#else
    {
        PROFILE_CPU_NAMED("Setup");

        // Setup first and topmost cluster
        type.Clusters.Resize(1);
        type.Root = &type.Clusters[0];
        type.Root->Init(totalBoundsType);

        // Update bounds of the foliage
        _box = totalBoundsType;
        for (auto& e : FoliageTypes)
        {
            if (e.Index != index && e.Root)
                BoundingBox::Merge(_box, e.Root->Bounds, _box);
        }
        BoundingSphere::FromBox(_box, _sphere);
        if (_sceneRenderingKey != -1)
            GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::Bounds);
    }
    {
        PROFILE_CPU_NAMED("Create Clusters");

        // Create clusters for foliage type quad tree
        const float globalDensityScale = GetGlobalDensityScale();
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            const float densityScale = type.UseDensityScaling ? globalDensityScale * type.DensityScalingScale : 1.0f;
            if (instance.Type == index && instance.Random < densityScale)
            {
                AddToCluster(type.Clusters, type.Root, instance);
            }
        }
    }
    {
        PROFILE_CPU_NAMED("Update Cache");
        type.Root->UpdateTotalBoundsAndCullDistance();
    }
#endif
}

void Foliage::RebuildClusters()
{
    PROFILE_CPU();
    PROFILE_MEM(LevelFoliage);

    // Faster path if foliage is empty or no types is ready
    bool anyTypeReady = false;
    for (auto& type : FoliageTypes)
        anyTypeReady |= type.IsReady();
    if (!anyTypeReady || Instances.IsEmpty())
    {
#if FOLIAGE_USE_SINGLE_QUAD_TREE
        Root = nullptr;
        Clusters.Clear();
#else
        for (auto& type : FoliageTypes)
        {
            type.Root = nullptr;
            type.Clusters.Clear();
        }
#endif
        _box = BoundingBox(_transform.Translation, _transform.Translation);
        _sphere = BoundingSphere(_transform.Translation, 0.0f);
        if (_sceneRenderingKey != -1)
            GetSceneRendering()->UpdateActor(this, _sceneRenderingKey);
        return;
    }

    // Clear clusters and initialize root
    {
        PROFILE_CPU_NAMED("Init Root");

        BoundingBox totalBounds, box;
#if FOLIAGE_USE_SINGLE_QUAD_TREE
        {
            // Calculate total bounds of all instances
            auto i = Instances.Begin();
            for (; i.IsNotEnd(); ++i)
            {
                if (!FoliageTypes[i->Type].IsReady())
                    continue;
                BoundingBox::FromSphere(i->Bounds, box);
                totalBounds = box;
                break;
            }
            ++i;
            // TODO: inline code and use SIMD
            for (; i.IsNotEnd(); ++i)
            {
                if (!FoliageTypes[i->Type].IsReady())
                    continue;
                BoundingBox::FromSphere(i->Bounds, box);
                BoundingBox::Merge(totalBounds, box, totalBounds);
            }
        }

        // Setup first and topmost cluster
        Clusters.Resize(1);
        Root = &Clusters[0];
        Root->Init(totalBounds);
#else
        bool hasTotalBounds = false;
        for (auto& type : FoliageTypes)
        {
            if (!type.IsReady())
            {
                type.Root = nullptr;
                type.Clusters.Clear();
                continue;
            }

            // Calculate total bounds of all instances of this type
            BoundingBox totalBoundsType;
            auto i = Instances.Begin();
            for (; i.IsNotEnd(); ++i)
            {
                if (i->Type == type.Index)
                {
                    BoundingBox::FromSphere(i->Bounds, box);
                    totalBoundsType = box;
                    break;
                }
            }
            ++i;
            // TODO: inline code and use SIMD
            for (; i.IsNotEnd(); ++i)
            {
                if (i->Type == type.Index)
                {
                    BoundingBox::FromSphere(i->Bounds, box);
                    BoundingBox::Merge(totalBoundsType, box, totalBoundsType);
                }
            }

            // Setup first and topmost cluster
            type.Clusters.Resize(1);
            type.Root = &type.Clusters[0];
            type.Root->Init(totalBoundsType);
            if (hasTotalBounds)
            {
                BoundingBox::Merge(totalBounds, totalBoundsType, totalBounds);
            }
            else
            {
                totalBounds = totalBoundsType;
                hasTotalBounds = true;
            }
        }
        ASSERT(hasTotalBounds);
#endif
        ASSERT(!totalBounds.Minimum.IsNanOrInfinity() && !totalBounds.Maximum.IsNanOrInfinity());
        _box = totalBounds;
        BoundingSphere::FromBox(_box, _sphere);
        if (_sceneRenderingKey != -1)
            GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::Bounds);
    }

    // Insert all instances to the clusters
    {
        PROFILE_CPU_NAMED("Create Clusters");
        const float globalDensityScale = GetGlobalDensityScale();
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            auto& type = FoliageTypes[instance.Type];
            const float densityScale = type.UseDensityScaling ? globalDensityScale * type.DensityScalingScale : 1.0f;

            if (type.IsReady() && instance.Random < densityScale)
            {
#if FOLIAGE_USE_SINGLE_QUAD_TREE
                AddToCluster(Clusters, Root, instance);
#else
                AddToCluster(type.Clusters, type.Root, instance);
#endif
            }
        }
    }

#if FOLIAGE_USE_SINGLE_QUAD_TREE
    if (Root)
    {
        PROFILE_CPU_NAMED("Update Cache");
        Root->UpdateTotalBoundsAndCullDistance();
    }
#else
    for (auto& type : FoliageTypes)
    {
        if (type.Root)
        {
            PROFILE_CPU_NAMED("Update Cache");
            type.Root->UpdateTotalBoundsAndCullDistance();
        }
    }
#endif
}

void Foliage::UpdateCullDistance()
{
    PROFILE_CPU();

    {
        PROFILE_CPU_NAMED("Instances");
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            auto& type = FoliageTypes[instance.Type];
            instance.CullDistance = type.CullDistance + type.CullDistanceRandomRange * instance.Random;
        }
    }

#if FOLIAGE_USE_SINGLE_QUAD_TREE
    if (Root)
    {
        PROFILE_CPU_NAMED("Clusters");
        Root->UpdateCullDistance();
    }
#else
    for (auto& type : FoliageTypes)
    {
        if (type.Root)
        {
            PROFILE_CPU_NAMED("Clusters");
            type.Root->UpdateCullDistance();
        }
    }
#endif
}

void Foliage::RemoveAllInstances()
{
    Instances.Clear();
    RebuildClusters();
}

void Foliage::RemoveLightmap()
{
    for (auto& e : Instances)
        e.RemoveLightmap();
}

static float GlobalDensityScale = 1.0f;

float Foliage::GetGlobalDensityScale()
{
    return GlobalDensityScale;
}

bool UpdateFoliageDensityScaling(Actor* actor)
{
    if (auto* foliage = dynamic_cast<Foliage*>(actor))
    {
        foliage->RebuildClusters();
    }

    return true;
}

void Foliage::SetGlobalDensityScale(float value)
{
    value = Math::Saturate(value);
    if (Math::NearEqual(value, GlobalDensityScale))
        return;

    PROFILE_CPU();

    GlobalDensityScale = value;

    Function<bool(Actor*)> f(UpdateFoliageDensityScaling);
    SceneQuery::TreeExecute(f);
}

bool Foliage::Intersects(const Ray& ray, Real& distance, Vector3& normal, int32& instanceIndex)
{
    PROFILE_CPU();

    instanceIndex = -1;
    normal = Vector3::Up;
    distance = MAX_Real;

    FoliageInstance* instance = nullptr;
#if FOLIAGE_USE_SINGLE_QUAD_TREE
    if (Root)
        Root->Intersects(this, ray, distance, normal, instance);
#else
    Real tmpDistance;
    Vector3 tmpNormal;
    FoliageInstance* tmpInstance;
    for (const auto& type : FoliageTypes)
    {
        if (type.Root && type.Root->Intersects(this, ray, tmpDistance, tmpNormal, tmpInstance) && tmpDistance < distance)
        {
            distance = tmpDistance;
            normal = tmpNormal;
            instance = tmpInstance;
        }
    }
#endif
    if (instance != nullptr)
    {
        int32 j = 0;
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            if (&*i == instance)
            {
                instanceIndex = j;
                return true;
            }
            j++;
        }
    }
    return false;
}

void Foliage::Draw(RenderContext& renderContext)
{
    if (Instances.IsEmpty())
        return;
    PROFILE_CPU();
    const RenderView& view = renderContext.View;

    // Cache data per foliage instance type
    for (auto& type : FoliageTypes)
    {
        InitType(renderContext.View, type);
    }

    if (renderContext.View.Pass == DrawPass::GlobalSDF)
    {
        auto globalSDF = GlobalSignDistanceFieldPass::Instance();
        BoundingBox globalSDFBounds;
        globalSDF->GetCullingData(globalSDFBounds);
#if FOLIAGE_USE_SINGLE_QUAD_TREE
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            auto& type = FoliageTypes[instance.Type];
            if (type._canDraw && CollisionsHelper::BoxIntersectsSphere(globalSDFBounds, instance.Bounds))
            {
                const Transform transform = _transform.LocalToWorld(instance.Transform);
                BoundingBox bounds;
                BoundingBox::FromSphere(instance.Bounds, bounds);
                globalSDF->RasterizeModelSDF(this, type.Model->SDF, transform, bounds);
            }
        }
#else
        for (auto& type : FoliageTypes)
        {
            if (type.Root && FOLIAGE_CAN_DRAW(renderContext, type))
                DrawClusterGlobalSDF(globalSDF, globalSDFBounds, type.Root, type);
        }
#endif
        return;
    }

    if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas)
    {
        // Draw foliage instances into Global Surface Atlas
        auto globalSA = GlobalSurfaceAtlasPass::Instance();
        Vector4 cullingPosDistance;
        globalSA->GetCullingData(cullingPosDistance);
#if FOLIAGE_USE_SINGLE_QUAD_TREE
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            auto& type = FoliageTypes[instance.Type];
            if (type._canDraw && CollisionsHelper::DistanceSpherePoint(instance.Bounds, Vector3(cullingPosDistance)) < cullingPosDistance.W)
            {
                const Transform transform = _transform.LocalToWorld(instance.Transform);
                BoundingBox localBounds = type.Model->LODs.Last().GetBox();
                globalSA->RasterizeActor(this, &instance, instance.Bounds, transform, localBounds, MAX_uint32, true, 0.5f);
            }
        }
#else
        for (auto& type : FoliageTypes)
        {
            if (type.Root && FOLIAGE_CAN_DRAW(renderContext, type))
            {
                BoundingBox localBounds = type.Model->LODs.Last().GetBox();
                DrawClusterGlobalSA(globalSA, cullingPosDistance, type.Root, type, localBounds);
            }
        }
#endif
        return;
    }
    if (EnumHasAnyFlags(renderContext.View.Pass, DrawPass::GlobalSurfaceAtlas))
    {
        // Draw single foliage instance projection into Global Surface Atlas
        auto& instance = *(FoliageInstance*)GlobalSurfaceAtlasPass::Instance()->GetCurrentActorObject();
        auto& type = FoliageTypes[instance.Type];
        InitType(renderContext.View, type);
        Matrix world;
        const Transform transform = _transform.LocalToWorld(instance.Transform);
        renderContext.View.GetWorldMatrix(transform, world);
        Mesh::DrawInfo draw;
        draw.Flags = GetStaticFlags();
        draw.LODBias = 0;
        draw.ForcedLOD = -1;
        draw.SortOrder = 0;
        draw.VertexColors = nullptr;
        draw.Lightmap = _scene ? _scene->LightmapsData.GetReadyLightmap(instance.Lightmap.TextureIndex) : nullptr;
        draw.LightmapUVs = &instance.Lightmap.UVsArea;
        draw.Buffer = &type.Entries;
        draw.World = &world;
        draw.DrawState = &instance.DrawState;
        draw.Deformation = nullptr;
        draw.Bounds = instance.Bounds;
        draw.PerInstanceRandom = instance.Random;
        draw.DrawModes = type._drawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode);
        draw.SetStencilValue(_layer);
        type.Model->Draw(renderContext, draw);
        return;
    }

    // Draw visible clusters
#if FOLIAGE_USE_SINGLE_QUAD_TREE || !FOLIAGE_USE_DRAW_CALLS_BATCHING
    Mesh::DrawInfo draw;
    draw.Flags = GetStaticFlags();
    draw.DrawModes = (DrawPass)(DrawPass::Default & view.Pass);
    draw.LODBias = 0;
    draw.ForcedLOD = -1;
    draw.VertexColors = nullptr;
    draw.Deformation = nullptr;
#else
    DrawCallsList draw[MODEL_MAX_LODS];
#endif
#if FOLIAGE_USE_SINGLE_QUAD_TREE
    if (Root)
        DrawCluster(renderContext, Root, draw);
#else
    for (auto& type : FoliageTypes)
    {
        DrawType(renderContext, type, draw);
    }
#endif
}

void Foliage::Draw(RenderContextBatch& renderContextBatch)
{
    if (Instances.IsEmpty())
        return;

#if !FOLIAGE_USE_SINGLE_QUAD_TREE
    // Run async job for each foliage type
    const RenderView& view = renderContextBatch.GetMainContext().View;
    if (EnumHasAnyFlags(view.Pass, DrawPass::GBuffer) && !(view.Pass & (DrawPass::GlobalSDF | DrawPass::GlobalSurfaceAtlas)) && renderContextBatch.EnableAsync)
    {
        // Cache data per foliage instance type
        for (FoliageType& type : FoliageTypes)
            InitType(view, type);

        // Run async job for each foliage type
        _renderContextBatch = &renderContextBatch;
        Function<void(int32)> func;
        func.Bind<Foliage, &Foliage::DrawFoliageJob>(this);
        const int64 waitLabel = JobSystem::Dispatch(func, FoliageTypes.Count());
        renderContextBatch.WaitLabels.Add(waitLabel);
        return;
    }
#endif

    // Fallback to default rendering
    Actor::Draw(renderContextBatch);
}

bool Foliage::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)
{
    int32 instanceIndex;
    return Intersects(ray, distance, normal, instanceIndex);
}

// Layout for encoded instance data (serialized as Base64 string)

static constexpr int32 GetInstanceBase64Size(int32 size)
{
    // 4 * (n / 3) -> align up to 4
    return (size * 4 / 3 + 3) & ~3;
}

// [Deprecated on 30.11.2019, expires on 30.11.2021]
struct InstanceEncoded1
{
    int32 Type;
    float Random;
    Float3 Translation;
    Quaternion Orientation;
    Float3 Scale;

    static constexpr int32 Size = 48;
    static constexpr int32 Base64Size = GetInstanceBase64Size(Size);
};

struct InstanceEncoded2
{
    int32 Type;
    float Random;
    Float3 Translation;
    Quaternion Orientation;
    Float3 Scale;
    LightmapEntry Lightmap;

    static const int32 Size = 68;
    static const int32 Base64Size = GetInstanceBase64Size(Size);
};

typedef InstanceEncoded2 InstanceEncoded;
static_assert(InstanceEncoded::Size == sizeof(InstanceEncoded), "Please update base64 buffer size to match the encoded instance buffer.");
static_assert(InstanceEncoded::Base64Size == GetInstanceBase64Size(sizeof(InstanceEncoded)), "Please update base64 buffer size to match the encoded instance buffer.");

void Foliage::Serialize(SerializeStream& stream, const void* otherObj)
{
    // Base
    Actor::Serialize(stream, otherObj);

    SERIALIZE_GET_OTHER_OBJ(Foliage);

    if (FoliageTypes.IsEmpty())
        return;

    PROFILE_CPU();

    stream.JKEY("Foliage");
    stream.StartArray();
    for (auto& type : FoliageTypes)
    {
        stream.StartObject();
        type.Serialize(stream, nullptr);
        stream.EndObject();
    }
    stream.EndArray();

    stream.JKEY("Instances");
    stream.StartArray();
    InstanceEncoded enc;
    char base64[InstanceEncoded::Base64Size + 2];
    base64[0] = '\"';
    base64[InstanceEncoded::Base64Size + 1] = '\"';
    for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
    {
        auto& instance = *i;

        enc.Type = instance.Type;
        enc.Random = instance.Random;
        enc.Translation = instance.Transform.Translation;
        enc.Orientation = instance.Transform.Orientation;
        enc.Scale = instance.Transform.Scale;
        enc.Lightmap = instance.Lightmap;

        Encryption::Base64Encode((const byte*)&enc, sizeof(enc), base64 + 1);

        stream.RawValue(base64, InstanceEncoded::Base64Size + 2);
    }
    stream.EndArray();
}

void Foliage::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
    // Base
    Actor::Deserialize(stream, modifier);

    PROFILE_CPU();
    PROFILE_MEM(LevelFoliage);

    // Clear
#if FOLIAGE_USE_SINGLE_QUAD_TREE
    Root = nullptr;
    Clusters.Release();
#endif
    Instances.Release();
    FoliageTypes.Resize(0, false);

    // Deserialize foliage types
    int32 foliageTypesCount = 0;
    const auto& foliageTypesMember = stream.FindMember("Foliage");
    if (foliageTypesMember != stream.MemberEnd() && foliageTypesMember->value.IsArray())
    {
        foliageTypesCount = foliageTypesMember->value.Size();
    }
    if (foliageTypesCount)
    {
        const DeserializeStream& items = foliageTypesMember->value;;
        FoliageTypes.Resize(foliageTypesCount, false);
        for (int32 i = 0; i < foliageTypesCount; i++)
        {
            FoliageTypes[i].Foliage = this;
            FoliageTypes[i].Index = i;
            FoliageTypes[i].Deserialize((DeserializeStream&)items[i], modifier);
        }
    }

    // Skip if no foliage
    if (FoliageTypes.IsEmpty())
        return;

    // Deserialize foliage instances
    int32 foliageInstancesCount = 0;
    const auto& foliageInstancesMember = stream.FindMember("Instances");
    if (foliageInstancesMember != stream.MemberEnd() && foliageInstancesMember->value.IsArray())
    {
        foliageInstancesCount = foliageInstancesMember->value.Size();
    }
    if (foliageInstancesCount)
    {
        const DeserializeStream& items = foliageInstancesMember->value;
        Instances.Resize(foliageInstancesCount);

        if (modifier->EngineBuild <= 6189)
        {
            // [Deprecated on 30.11.2019, expires on 30.11.2021]
            MARK_CONTENT_DEPRECATED();
            InstanceEncoded1 enc;
            for (int32 i = 0; i < foliageInstancesCount; i++)
            {
                auto& instance = Instances[i];
                auto& item = items[i];

                const int32 length = item.GetStringLength();
                if (length != InstanceEncoded1::Base64Size)
                {
                    LOG(Warning, "Invalid foliage instance data size.");
                    continue;
                }
                Encryption::Base64Decode(item.GetString(), length, (byte*)&enc);

                instance.Type = enc.Type;
                instance.Random = enc.Random;
                instance.Transform.Translation = enc.Translation;
                instance.Transform.Orientation = enc.Orientation;
                instance.Transform.Scale = enc.Scale;
                instance.Lightmap = LightmapEntry();
            }
        }
        else
        {
            InstanceEncoded enc;
            for (int32 i = 0; i < foliageInstancesCount; i++)
            {
                auto& instance = Instances[i];
                auto& item = items[i];

                const int32 length = item.GetStringLength();
                if (length != InstanceEncoded::Base64Size)
                {
                    LOG(Warning, "Invalid foliage instance data size.");
                    continue;
                }
                Encryption::Base64Decode(item.GetString(), length, (byte*)&enc);

                instance.Type = enc.Type;
                instance.Random = enc.Random;
                instance.Transform.Translation = enc.Translation;
                instance.Transform.Orientation = enc.Orientation;
                instance.Transform.Scale = enc.Scale;
                instance.Lightmap = enc.Lightmap;
            }
        }

#if BUILD_DEBUG
        // Remove invalid instances
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            if (i->Type < 0 || i->Type >= FoliageTypes.Count())
            {
                LOG(Warning, "Removing invalid foliage instance.");
                Instances.Remove(i);
                --i;
            }
        }
#endif

        // Update cull distance
        for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
        {
            auto& instance = *i;
            auto& type = FoliageTypes[instance.Type];
            instance.CullDistance = type.CullDistance + type.CullDistanceRandomRange * instance.Random;
        }
    }
}

void Foliage::OnLayerChanged()
{
    if (_sceneRenderingKey != -1)
        GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::Layer);
}

void Foliage::OnEnable()
{
    GetSceneRendering()->AddActor(this, _sceneRenderingKey);

    // Base
    Actor::OnEnable();
}

void Foliage::OnDisable()
{
    GetSceneRendering()->RemoveActor(this, _sceneRenderingKey);

    // Base
    Actor::OnDisable();
}

void Foliage::OnTransformChanged()
{
    // Base
    Actor::OnTransformChanged();

    PROFILE_CPU();

    // Update instances matrices and cached world bounds
    Vector3 corners[8];
    Matrix world;
    GetLocalToWorldMatrix(world);
    for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
    {
        auto& instance = *i;
        auto type = &FoliageTypes[instance.Type];

        // Update bounds
        instance.Bounds = BoundingSphere::Empty;
        if (!type->IsReady())
            continue;
        auto& meshes = type->Model->LODs[0].Meshes;
        const Transform transform = _transform.LocalToWorld(instance.Transform);
        for (int32 j = 0; j < meshes.Count(); j++)
        {
            meshes[j].GetBox().GetCorners(corners);

            for (int32 k = 0; k < 8; k++)
            {
                Vector3::Transform(corners[k], transform, corners[k]);
            }
            BoundingSphere meshBounds;
            BoundingSphere::FromPoints(corners, 8, meshBounds);

            BoundingSphere::Merge(instance.Bounds, meshBounds, instance.Bounds);
        }
    }

    RebuildClusters();
}
