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

#include "AnimatedModel.h"
#include "BoneSocket.h"
#include "Engine/Core/Math/Matrix3x4.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Animations/Animations.h"
#include "Engine/Engine/Engine.h"
#if USE_EDITOR
#include "Engine/Core/Math/OrientedBoundingBox.h"
#include "Engine/Core/Math/Matrix3x3.h"
#include "Editor/Editor.h"
#endif
#include "Engine/Content/Deprecated.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/GPUPass.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/Models/MeshAccessor.h"
#include "Engine/Graphics/Models/MeshDeformation.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Level/SceneObjectsFactory.h"
#include "Engine/Profiler/Profiler.h"
#include "Engine/Serialization/Serialization.h"

// Implements efficient skinning data update within a shared GPUMemoryPass with manual resource transitions batched for all animated models.
class AnimatedModelRenderListExtension : public RenderList::IExtension
{
public:
    struct Item
    {
        GPUBuffer* BoneMatrices;
        void* Data;
        int32 Size;
    };

    RenderListBuffer<Item> Items;

    void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) override
    {
        Items.Clear();
    }

    void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) override
    {
        const int32 count = Items.Count();
        if (count == 0)
            return;
        PROFILE_GPU_CPU_NAMED("Update Bones");
        GPUMemoryPass pass(context);
        Item* items = Items.Get();

        // Special case for D3D11 backend that doesn't need transitions
        if (context->GetDevice()->GetRendererType() <= RendererType::DirectX11)
        {
            for (int32 i = 0; i < count; i++)
            {
                Item& item = items[i];
                context->UpdateBuffer(item.BoneMatrices, item.Data, item.Size);
            }
        }
        else
        {
            // Batch resource barriers for buffer update
            for (int32 i = 0; i < count; i++)
                pass.Transition(items[i].BoneMatrices, GPUResourceAccess::CopyWrite);

            // Update all buffers within Memory Pass (no barriers between)
            for (int32 i = 0; i < count; i++)
            {
                Item& item = items[i];
                context->UpdateBuffer(item.BoneMatrices, item.Data, item.Size);
            }

            // Batch resource barriers for reading in Vertex Shader
            for (int32 i = 0; i < count; i++)
                pass.Transition(items[i].BoneMatrices, GPUResourceAccess::ShaderReadGraphics);
        }

#if COMPILE_WITH_PROFILER
        // Insert amount of kilobytes of data updated into profiler trace
        uint32 dataSize = 0;
        for (int32 i = 0; i < count; i++)
            dataSize += items[i].Size;
        ZoneValue(dataSize / 1024);
#endif

        Items.Clear();
    }
};

AnimatedModelRenderListExtension RenderListExtension;

AnimatedModel::AnimatedModel(const SpawnParams& params)
    : ModelInstanceActor(params)
    , _actualMode(AnimationUpdateMode::Never)
    , _counter(0)
    , _lastMinDstSqr(MAX_Real)
    , _lastUpdateFrame(0)
    , SkinnedModel(this)
    , AnimationGraph(this)
{
    _drawCategory = SceneRendering::SceneDrawAsync;
    GraphInstance.Object = this;
    _box = BoundingBox(Vector3::Zero);
    _sphere = BoundingSphere(Vector3::Zero, 0.0f);
}

AnimatedModel::~AnimatedModel()
{
    if (_deformation)
        Delete(_deformation);
}

void AnimatedModel::ResetAnimation()
{
    GraphInstance.ClearState();
}

void AnimatedModel::UpdateAnimation()
{
    // Skip if need to
    if (UpdateMode == AnimationUpdateMode::Never
        || !IsActiveInHierarchy()
        || SkinnedModel == nullptr
        || !SkinnedModel->IsLoaded()
        || _lastUpdateFrame == Engine::UpdateCount
        || _masterPose)
        return;
    _lastUpdateFrame = Engine::UpdateCount;

    if (AnimationGraph && AnimationGraph->IsLoaded() && AnimationGraph->Graph.IsReady())
    {
        // Request an animation update
        Animations::AddToUpdate(this);
    }
}

void AnimatedModel::SetupSkinningData()
{
    ASSERT(SkinnedModel && SkinnedModel->IsLoaded());

    const int32 targetBonesCount = SkinnedModel->Skeleton.Bones.Count();
    const int32 currentBonesCount = _skinningData.BonesCount;

    if (targetBonesCount != currentBonesCount)
    {
        _skinningData.Setup(targetBonesCount);
    }
}

void AnimatedModel::PreInitSkinningData()
{
    if (!SkinnedModel || !SkinnedModel->IsLoaded())
        return;
    PROFILE_CPU();
    PROFILE_MEM(Animations);
    ScopeLock lock(SkinnedModel->Locker);

    SetupSkinningData();
    auto& skeleton = SkinnedModel->Skeleton;
    const int32 bonesCount = skeleton.Bones.Count();
    const int32 nodesCount = skeleton.Nodes.Count();

    // Get nodes global transformations for the initial pose
    GraphInstance.NodesPose.Resize(nodesCount, false);
    auto nodesPose = GraphInstance.NodesPose.Get();
    for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++)
    {
        Matrix localTransform;
        skeleton.Nodes[nodeIndex].LocalTransform.GetWorld(localTransform);
        const int32 parentIndex = skeleton.Nodes[nodeIndex].ParentIndex;
        if (parentIndex != -1)
            nodesPose[nodeIndex] = localTransform * nodesPose[parentIndex];
        else
            nodesPose[nodeIndex] = localTransform;
    }
    GraphInstance.Invalidate();
    GraphInstance.RootTransform = nodesCount > 0 ? skeleton.Nodes[0].LocalTransform : Transform::Identity;

    // Setup bones transformations including bone offset matrix
    Matrix3x4* output = (Matrix3x4*)_skinningData.Data.Get();
    const SkeletonBone* bones = skeleton.Bones.Get();
    for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
    {
        auto& bone = bones[boneIndex];
        Matrix identityMatrix = bone.OffsetMatrix * nodesPose[bone.NodeIndex];
        output[boneIndex].SetMatrixTranspose(identityMatrix);
    }
    _skinningData.OnDataChanged(true);

    UpdateBounds();
    UpdateSockets();
}

void AnimatedModel::GetCurrentPose(Array<Matrix>& nodesTransformation, bool worldSpace) const
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
    nodesTransformation = GraphInstance.NodesPose;
    if (worldSpace)
    {
        Matrix world;
        GetLocalToWorldMatrix(world);
        for (auto& m : nodesTransformation)
            m = m * world;
    }
}

void AnimatedModel::GetCurrentPose(Span<Matrix>& nodesTransformation) const
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
    nodesTransformation = ToSpan(GraphInstance.NodesPose);
}

void AnimatedModel::SetCurrentPose(const Array<Matrix>& nodesTransformation, bool worldSpace)
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
    CHECK(nodesTransformation.Count() == GraphInstance.NodesPose.Count());
    GraphInstance.NodesPose = nodesTransformation;
    if (worldSpace)
    {
        Matrix world;
        GetLocalToWorldMatrix(world);
        Matrix invWorld;
        Matrix::Invert(world, invWorld);
        for (auto& m : GraphInstance.NodesPose)
            m = m * invWorld;
    }
    OnAnimationUpdated();
}

void AnimatedModel::GetNodeTransformation(int32 nodeIndex, Matrix& nodeTransformation, bool worldSpace) const
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
    if (nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count())
        nodeTransformation = GraphInstance.NodesPose[nodeIndex];
    else
        nodeTransformation = Matrix::Identity;
    if (worldSpace)
    {
        Matrix world;
        GetLocalToWorldMatrix(world);
        nodeTransformation = nodeTransformation * world;
    }
}

void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& nodeTransformation, bool worldSpace) const
{
    GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
}

void AnimatedModel::GetNodeTransformation(Array<NodeTransformation>& nodeTransformations, bool worldSpace) const
{
    for (NodeTransformation& item : nodeTransformations)
    {
        GetNodeTransformation(item.NodeIndex, item.NodeMatrix, worldSpace);
    }
}

void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace)
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
    CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count());
    GraphInstance.NodesPose[nodeIndex] = nodeTransformation;
    if (worldSpace)
    {
        Matrix world;
        GetLocalToWorldMatrix(world);
        Matrix invWorld;
        Matrix::Invert(world, invWorld);
        GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld;
    }
    OnAnimationUpdated();
}

void AnimatedModel::SetNodeTransformation(const Array<NodeTransformation>& nodeTransformations, bool worldSpace)
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return

    // Calculate it once, outside loop
    Matrix invWorld;
    if (worldSpace) 
    {
        Matrix world;
        GetLocalToWorldMatrix(world);
        Matrix::Invert(world, invWorld);
    }

    for (int i = 0; i < nodeTransformations.Count(); i++)
    {
        int nodeIndex = nodeTransformations[i].NodeIndex;
        CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count());
        GraphInstance.NodesPose[nodeIndex] = nodeTransformations[i].NodeMatrix;
        if (worldSpace)
        {
            GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld;
        }
    }
    OnAnimationUpdated();
}

void AnimatedModel::SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace)
{
    SetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
}

int32 AnimatedModel::FindClosestNode(const Vector3& location, bool worldSpace) const
{
    if (GraphInstance.NodesPose.IsEmpty())
        const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
    const Vector3 pos = worldSpace ? _transform.WorldToLocal(location) : location;
    int32 result = -1;
    Real closest = MAX_Real;
    for (int32 nodeIndex = 0; nodeIndex < GraphInstance.NodesPose.Count(); nodeIndex++)
    {
        const Vector3 node = GraphInstance.NodesPose[nodeIndex].GetTranslation();
        const Real dst = Vector3::DistanceSquared(node, pos);
        if (dst < closest)
        {
            closest = dst;
            result = nodeIndex;
        }
    }
    return result;
}

void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose)
{
    if (masterPose == _masterPose)
        return;
    if (_masterPose)
        _masterPose->AnimationUpdated.Unbind<AnimatedModel, &AnimatedModel::OnAnimationUpdated>(this);
    _masterPose = masterPose;
    if (_masterPose)
        _masterPose->AnimationUpdated.Bind<AnimatedModel, &AnimatedModel::OnAnimationUpdated>(this);
}

const Array<AnimGraphTraceEvent>& AnimatedModel::GetTraceEvents() const
{
#if !BUILD_RELEASE
    if (!GetEnableTracing())
    {
        LOG(Warning, "Accessing AnimatedModel.TraceEvents with tracing disabled.");
    }
#endif
    return GraphInstance.TraceEvents;
}

#define CHECK_ANIM_GRAPH_PARAM_ACCESS() \
    if (!AnimationGraph) \
    { \
        LOG(Warning, "Missing animation graph for animated model '{0}'", ToString()); \
        return; \
    } \
    if (AnimationGraph->WaitForLoaded()) \
    { \
        LOG(Warning, "Failed to load animation graph for animated model '{0}'", ToString()); \
        return; \
    }
#define CHECK_ANIM_GRAPH_PARAM_ACCESS_RESULT(result) \
    if (!AnimationGraph) \
    { \
        LOG(Warning, "Missing animation graph for animated model '{0}'", ToString()); \
        return result; \
    } \
    if (AnimationGraph->WaitForLoaded()) \
    { \
        LOG(Warning, "Failed to load animation graph for animated model '{0}'", ToString()); \
        return result; \
    }

AnimGraphParameter* AnimatedModel::GetParameter(const StringView& name)
{
    CHECK_ANIM_GRAPH_PARAM_ACCESS_RESULT(nullptr);
    for (auto& param : GraphInstance.Parameters)
    {
        if (param.Name == name)
            return &param;
    }
    LOG(Warning, "Failed to get animated model '{0}' missing parameter '{1}'", ToString(), name);
    return nullptr;
}

const Variant& AnimatedModel::GetParameterValue(const StringView& name) const
{
    CHECK_ANIM_GRAPH_PARAM_ACCESS_RESULT(Variant::Null);
    for (auto& param : GraphInstance.Parameters)
    {
        if (param.Name == name)
            return param.Value;
    }
    LOG(Warning, "Failed to get animated model '{0}' missing parameter '{1}'", ToString(), name);
    return Variant::Null;
}

void AnimatedModel::SetParameterValue(const StringView& name, const Variant& value)
{
    CHECK_ANIM_GRAPH_PARAM_ACCESS();
    for (auto& param : GraphInstance.Parameters)
    {
        if (param.Name == name)
        {
            if (param.Value.Type == value.Type)
                param.Value = value;
            else if (Variant::CanCast(value, param.Value.Type))
                param.Value = Variant::Cast(value, param.Value.Type);
            else
                LOG(Warning, "Animation Graph parameter '{0}' in AnimatedModel {1} is type '{2}' and not type '{3}'.", name, ToString(), param.Value.Type, value.Type);
            return;
        }
    }
    LOG(Warning, "Failed to set animated model '{0}' missing parameter '{1}'", ToString(), name);
}

const Variant& AnimatedModel::GetParameterValue(const Guid& id) const
{
    CHECK_ANIM_GRAPH_PARAM_ACCESS_RESULT(Variant::Null);
    for (auto& param : GraphInstance.Parameters)
    {
        if (param.Identifier == id)
            return param.Value;
    }
    LOG(Warning, "Failed to get animated model '{0}' missing parameter '{1}'", ToString(), id.ToString());
    return Variant::Null;
}

void AnimatedModel::SetParameterValue(const Guid& id, const Variant& value)
{
    CHECK_ANIM_GRAPH_PARAM_ACCESS();
    for (auto& param : GraphInstance.Parameters)
    {
        if (param.Identifier == id)
        {
            param.Value = value;
            return;
        }
    }
    LOG(Warning, "Failed to set animated model '{0}' missing parameter '{1}'", ToString(), id.ToString());
}

#undef CHECK_ANIM_GRAPH_PARAM_ACCESS

float AnimatedModel::GetBlendShapeWeight(const StringView& name)
{
    for (auto& e : _blendShapeWeights)
    {
        if (e.First == name)
            return e.Second;
    }
    return 0.0f;
}

void AnimatedModel::SetBlendShapeWeight(const StringView& name, float value)
{
    const auto* model = SkinnedModel.Get();
    CHECK(model);
    model->WaitForLoaded();
    value = Math::Clamp(value, -1.0f, 1.0f);
    const bool isZero = Math::IsZero(value);
    if (!_deformation && !isZero)
        _deformation = New<MeshDeformation>();
    Function<void(const MeshBase*, MeshDeformationData&)> deformer;
    deformer.Bind<AnimatedModel, &AnimatedModel::RunBlendShapeDeformer>(this);
    for (int32 i = 0; i < _blendShapeWeights.Count(); i++)
    {
        auto& e = _blendShapeWeights[i];
        if (e.First == name)
        {
            if (isZero)
            {
                _blendShapeWeights.RemoveAt(i);

                // Remove deformers for meshes using this blend shape
                for (const auto& lod : model->LODs)
                {
                    for (const auto& mesh : lod.Meshes)
                    {
                        for (const auto& blendShape : mesh.BlendShapes)
                        {
                            if (blendShape.Name == name)
                            {
                                for (int32 j = 0; j < _blendShapeMeshes.Count(); j++)
                                {
                                    auto& blendShapeMesh = _blendShapeMeshes[j];
                                    if (blendShapeMesh.LODIndex == mesh.GetLODIndex() && blendShapeMesh.MeshIndex == mesh.GetIndex())
                                    {
                                        blendShapeMesh.Usages--;
                                        if (blendShapeMesh.Usages == 0)
                                        {
                                            _deformation->RemoveDeformer(blendShapeMesh.LODIndex, blendShapeMesh.MeshIndex, MeshBufferType::Vertex0, deformer);
                                            _blendShapeMeshes.RemoveAt(j);
                                        }
                                        break;
                                    }
                                }
                                break;
                            }
                        }
                    }
                }
            }
            else if (Math::NotNearEqual(e.Second, value))
            {
                // Update blend shape weight
                e.Second = value;

                // Dirty deformers for meshes using this blend shape
                for (const auto& lod : model->LODs)
                {
                    for (const auto& mesh : lod.Meshes)
                    {
                        for (const auto& blendShape : mesh.BlendShapes)
                        {
                            if (blendShape.Name == name)
                            {
                                _deformation->Dirty(mesh.GetLODIndex(), mesh.GetIndex(), MeshBufferType::Vertex0);
                                break;
                            }
                        }
                    }
                }
            }
            return;
        }
    }
    if (!isZero)
    {
        // Add blend shape weight
        auto& e = _blendShapeWeights.AddOne();
        e.First = name;
        e.Second = value;

        // Add deformers for meshes using this blend shape
        for (const auto& lod : model->LODs)
        {
            for (const auto& mesh : lod.Meshes)
            {
                for (const auto& blendShape : mesh.BlendShapes)
                {
                    if (blendShape.Name == name)
                    {
                        int32 i = 0;
                        for (; i < _blendShapeMeshes.Count(); i++)
                        {
                            auto& blendShapeMesh = _blendShapeMeshes[i];
                            if (blendShapeMesh.LODIndex == mesh.GetLODIndex() && blendShapeMesh.MeshIndex == mesh.GetIndex())
                            {
                                blendShapeMesh.Usages++;
                                break;
                            }
                        }
                        if (i == _blendShapeMeshes.Count())
                        {
                            auto& blendShapeMesh = _blendShapeMeshes.AddOne();
                            blendShapeMesh.LODIndex = mesh.GetLODIndex();
                            blendShapeMesh.MeshIndex = mesh.GetIndex();
                            blendShapeMesh.Usages = 1;
                            _deformation->AddDeformer(blendShapeMesh.LODIndex, blendShapeMesh.MeshIndex, MeshBufferType::Vertex0, deformer);
                        }
                        break;
                    }
                }
            }
        }
    }
}

void AnimatedModel::ClearBlendShapeWeights()
{
    if (_deformation)
    {
        Function<void(const MeshBase*, MeshDeformationData&)> deformer;
        deformer.Bind<AnimatedModel, &AnimatedModel::RunBlendShapeDeformer>(this);
        for (auto e : _blendShapeMeshes)
            _deformation->RemoveDeformer(e.LODIndex, e.MeshIndex, MeshBufferType::Vertex0, deformer);
    }
    _blendShapeWeights.Clear();
    _blendShapeMeshes.Clear();
}

void AnimatedModel::PlaySlotAnimation(const StringView& slotName, Animation* anim, float speed, float blendInTime, float blendOutTime, int32 loopCount)
{
    CHECK(anim);
    for (auto& slot : GraphInstance.Slots)
    {
        if (slot.Animation == anim && slot.Name == slotName)
        {
            slot.Pause = false;
            slot.BlendInTime = blendInTime;
            slot.LoopCount = loopCount;
            return;
        }
    }
    int32 index = 0;
    for (; index < GraphInstance.Slots.Count(); index++)
    {
        if (GraphInstance.Slots[index].Animation == nullptr)
            break;
    }
    if (index == GraphInstance.Slots.Count())
        GraphInstance.Slots.AddOne();
    auto& slot = GraphInstance.Slots[index];
    slot.Name = slotName;
    slot.Animation = anim;
    slot.Speed = speed;
    slot.BlendInTime = blendInTime;
    slot.BlendOutTime = blendOutTime;
    slot.LoopCount = loopCount;
}

void AnimatedModel::StopSlotAnimation()
{
    GraphInstance.Slots.Clear();
}

void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* anim)
{
    for (auto& slot : GraphInstance.Slots)
    {
        if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName)
        {
            //slot.Animation = nullptr; // TODO: make an immediate version of this method and set the animation to nullptr.
            if (slot.Animation != nullptr)
                slot.Reset = true;
            break;
        }
    }
}

void AnimatedModel::PauseSlotAnimation()
{
    for (auto& slot : GraphInstance.Slots)
        slot.Pause = true;
}

void AnimatedModel::PauseSlotAnimation(const StringView& slotName, Animation* anim)
{
    for (auto& slot : GraphInstance.Slots)
    {
        if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName)
        {
            slot.Pause = true;
            break;
        }
    }
}

bool AnimatedModel::IsPlayingSlotAnimation()
{
    for (auto& slot : GraphInstance.Slots)
    {
        if (slot.Animation && !slot.Pause)
            return true;
    }
    return false;
}

bool AnimatedModel::IsPlayingSlotAnimation(const StringView& slotName, Animation* anim)
{
    for (auto& slot : GraphInstance.Slots)
    {
        if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName && !slot.Pause)
            return true;
    }
    return false;
}

void AnimatedModel::ApplyRootMotion(const Transform& rootMotionDelta)
{
    // Skip if no motion
    if (rootMotionDelta.Translation.IsZero() && rootMotionDelta.Orientation.IsIdentity())
        return;

    // Transform translation from actor space into world space
    const Vector3 translation = Vector3::Transform(rootMotionDelta.Translation * GetScale(), GetOrientation());

    // Apply movement
    Actor* target = RootMotionTarget ? RootMotionTarget.Get() : this;
    target->AddMovement(translation, rootMotionDelta.Orientation);
}

void AnimatedModel::SyncParameters()
{
    const int32 targetCount = AnimationGraph ? AnimationGraph->Graph.Parameters.Count() : 0;
    //const int32 currentCount = GraphInstance.Parameters.Count();

    //if (targetCount != currentCount)
    {
        if (targetCount == 0)
        {
            // Clear the data
            GraphInstance.Clear();
        }
        else
        {
            PROFILE_MEM(Animations);
            ScopeLock lock(AnimationGraph->Locker);

            // Clone the parameters
            GraphInstance.Parameters.Resize(AnimationGraph->Graph.Parameters.Count(), false);
            for (int32 i = 0; i < GraphInstance.Parameters.Count(); i++)
            {
                const auto src = &AnimationGraph->Graph.Parameters.At(i);
                auto& dst = GraphInstance.Parameters[i];

                dst.Type = src->Type;
                dst.Identifier = src->Identifier;
                dst.Name = src->Name;
                dst.IsPublic = src->IsPublic;
                dst.Value = src->Value;
#if USE_EDITOR
                dst.Meta = src->Meta;
#endif
            }
        }
    }
}

void AnimatedModel::RunBlendShapeDeformer(const MeshBase* mesh, MeshDeformationData& deformation)
{
    PROFILE_CPU_NAMED("BlendShapes");
    auto* skinnedMesh = (const SkinnedMesh*)mesh;

    // Estimate the range of the vertices to modify by the currently active blend shapes
    uint32 minVertexIndex = MAX_uint32, maxVertexIndex = 0;
    bool useNormals = false;
    Array<Pair<const BlendShape&, const float>, InlinedAllocation<32>> blendShapes;
    for (const BlendShape& blendShape : skinnedMesh->BlendShapes)
    {
        for (auto& q : _blendShapeWeights)
        {
            if (q.First == blendShape.Name)
            {
                float weight = q.Second;
                if (!Math::IsZero(blendShape.Weight))
                    weight *= blendShape.Weight;
                if (Math::IsZero(weight))
                    break;
                blendShapes.Add(Pair<const BlendShape&, const float>(blendShape, weight));
                minVertexIndex = Math::Min(minVertexIndex, blendShape.MinVertexIndex);
                maxVertexIndex = Math::Max(maxVertexIndex, blendShape.MaxVertexIndex);
                useNormals |= blendShape.UseNormals;
                break;
            }
        }
    }

    // Blend all blend shapes
    auto vertexCount = (uint32)mesh->GetVertexCount();
    MeshAccessor accessor;
    if (deformation.LoadMeshAccessor(accessor))
        return;
    auto positionStream = accessor.Position();
    auto normalStream = accessor.Normal();
    CHECK(positionStream.IsValid());
    useNormals &= normalStream.IsValid();
    for (const auto& q : blendShapes)
    {
        for (int32 i = 0; i < q.First.Vertices.Count(); i++)
        {
            const BlendShapeVertex& blendShapeVertex = q.First.Vertices[i];
            ASSERT_LOW_LAYER(blendShapeVertex.VertexIndex < vertexCount);

            Float3 position = positionStream.GetFloat3(blendShapeVertex.VertexIndex);
            position = position + blendShapeVertex.PositionDelta * q.Second;
            positionStream.SetFloat3(blendShapeVertex.VertexIndex, position);
        }
        if (useNormals)
        {
            for (int32 i = 0; i < q.First.Vertices.Count(); i++)
            {
                const BlendShapeVertex& blendShapeVertex = q.First.Vertices[i];

                Float3 normal = normalStream.GetFloat3(blendShapeVertex.VertexIndex);
                MeshAccessor::UnpackNormal(normal);
                normal = normal + blendShapeVertex.NormalDelta * q.Second;
                MeshAccessor::PackNormal(normal); // TODO: optimize unpacking and packing to just apply it to the normal delta
                normalStream.SetFloat3(blendShapeVertex.VertexIndex, normal);
            }
        }
    }

    if (useNormals)
    {
        // Normalize normal vectors and rebuild tangent frames (tangent frame is in range [-1;1] but packed to [0;1] range)
        auto tangentStream = accessor.Tangent();
        for (uint32 vertexIndex = minVertexIndex; vertexIndex <= maxVertexIndex; vertexIndex++)
        {
            Float3 normal = normalStream.GetFloat3(vertexIndex);
            MeshAccessor::UnpackNormal(normal);
            normal.Normalize();
            MeshAccessor::PackNormal(normal);
            normalStream.SetFloat3(vertexIndex, normal);

            if (tangentStream.IsValid())
            {
                Float4 tangentRaw = normalStream.GetFloat4(vertexIndex);
                Float3 tangent = Float3(tangentRaw);
                MeshAccessor::UnpackNormal(tangent);
                tangent = tangent - ((tangent | normal) * normal);
                tangent.Normalize();
                MeshAccessor::PackNormal(tangent);
                tangentRaw = Float4(tangent, tangentRaw.W);
                tangentStream.SetFloat4(vertexIndex, tangentRaw);
            }
        }
    }

    // Mark as dirty to be cleared before next rendering
    deformation.DirtyMinIndex = Math::Min(minVertexIndex, deformation.DirtyMinIndex);
    deformation.DirtyMaxIndex = Math::Max(maxVertexIndex, deformation.DirtyMaxIndex);
}

void AnimatedModel::BeginPlay(SceneBeginData* data)
{
    PreInitSkinningData();

    // Base
    ModelInstanceActor::BeginPlay(data);
}

void AnimatedModel::EndPlay()
{
    Animations::RemoveFromUpdate(this);
    SetMasterPoseModel(nullptr);

    // Base
    ModelInstanceActor::EndPlay();
}

void AnimatedModel::OnEnable()
{
    GetScene()->Ticking.Update.AddTick<AnimatedModel, &AnimatedModel::Update>(this);

    // Base
    ModelInstanceActor::OnEnable();
}

void AnimatedModel::OnDisable()
{
    GetScene()->Ticking.Update.RemoveTick(this);

    // Base
    ModelInstanceActor::OnDisable();
}

void AnimatedModel::OnActiveInTreeChanged()
{
    GraphInstance.Invalidate();

    // Base
    ModelInstanceActor::OnActiveInTreeChanged();
}

void AnimatedModel::UpdateBounds()
{
    const auto model = SkinnedModel.Get();
    BoundingSphere prevSphere = _sphere;
    if (CustomBounds.GetSize().LengthSquared() > 0.01f)
    {
        BoundingBox::Transform(CustomBounds, _transform, _box);
    }
    else if (model && model->IsLoaded() && model->LODs.Count() != 0)
    {
        Matrix world;
        GetLocalToWorldMatrix(world);
        const BoundingBox modelBox = model->GetBox(world);
        BoundingBox box = modelBox;
        if (GraphInstance.NodesPose.Count() != 0)
        {
            // Per-bone bounds estimated from positions
            auto& skeleton = model->Skeleton;
            const int32 bonesCount = skeleton.Bones.Count();
            for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
                box.Merge(_transform.LocalToWorld(GraphInstance.NodesPose[skeleton.Bones.Get()[boneIndex].NodeIndex].GetTranslation()));
        }

        // Apply margin based on model dimensions
        const Vector3 modelBoxSize = modelBox.GetSize();
        const Vector3 center = box.GetCenter();
        const Vector3 sizeHalf = Vector3::Max(box.GetSize() + modelBoxSize * 0.2f, modelBoxSize) * (0.5f * BoundsScale);
        _box = BoundingBox(center - sizeHalf, center + sizeHalf);
    }
    else
    {
        _box = BoundingBox(_transform.Translation);
    }
    BoundingSphere::FromBox(_box, _sphere);
    if (_sceneRenderingKey != -1 && prevSphere != _sphere)
        GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::Bounds);
}

void AnimatedModel::UpdateSockets()
{
    for (int32 i = 0; i < Children.Count(); i++)
    {
        auto socket = dynamic_cast<BoneSocket*>(Children[i]);
        if (socket)
            socket->UpdateTransformation();
    }
}

void AnimatedModel::OnAnimationUpdated_Async()
{
    // Update asynchronous stuff
    const auto& skeleton = SkinnedModel->Skeleton;

    // Copy pose from the master
    // TODO: support retargetting master pose to current pose
    if (_masterPose && _masterPose->SkinnedModel->Skeleton.Nodes.Count() == skeleton.Nodes.Count())
    {
        ANIM_GRAPH_PROFILE_EVENT("Copy Master Pose");
        const auto& masterInstance = _masterPose->GraphInstance;
        GraphInstance.NodesPose = masterInstance.NodesPose;
        GraphInstance.RootTransform = masterInstance.RootTransform;
        GraphInstance.RootMotion = masterInstance.RootMotion;
    }

    // Calculate the final bones transformations and update skinning
    {
        ANIM_GRAPH_PROFILE_EVENT("Final Pose");
        const int32 bonesCount = skeleton.Bones.Count();
        Matrix3x4* output = (Matrix3x4*)_skinningData.Data.Get();
        ASSERT(GraphInstance.NodesPose.Count() == skeleton.Nodes.Count());
        ASSERT(_skinningData.Data.Count() == bonesCount * sizeof(Matrix3x4));
        for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
        {
            const SkeletonBone& bone = skeleton.Bones[boneIndex];
            Matrix matrix;
            Matrix::Multiply(bone.OffsetMatrix, GraphInstance.NodesPose.Get()[bone.NodeIndex], matrix);
            output[boneIndex].SetMatrixTranspose(matrix);
        }
        _skinningData.OnDataChanged(!PerBoneMotionBlur);
    }

    //if (UpdateWhenOffscreen)
    {
        UpdateBounds();
    }
}

void AnimatedModel::OnAnimationUpdated_Sync()
{
    // Update synchronous stuff
    UpdateSockets();
    ApplyRootMotion(GraphInstance.RootMotion);
    if (!_isDuringUpdateEvent)
    {
        // Prevent stack-overflow when gameplay modifies the pose within the event
        _isDuringUpdateEvent = true;
        AnimationUpdated();
        _isDuringUpdateEvent = false;
    }
}

void AnimatedModel::OnAnimationUpdated()
{
    ANIM_GRAPH_PROFILE_EVENT("OnAnimationUpdated");
    OnAnimationUpdated_Async();
    OnAnimationUpdated_Sync();
}

void AnimatedModel::OnSkinnedModelChanged()
{
    Entries.Release();
    if (SkinnedModel && !SkinnedModel->IsLoaded())
    {
        UpdateBounds();
        GraphInstance.Invalidate();
    }
    if (_deformation)
        _deformation->Clear();
    GraphInstance.NodesSkeleton = SkinnedModel;
}

void AnimatedModel::OnSkinnedModelLoaded()
{
    Entries.SetupIfInvalid(SkinnedModel);
    GraphInstance.Invalidate();
    PreInitSkinningData();
}

void AnimatedModel::OnGraphChanged()
{
    // Cleanup parameters
    GraphInstance.Clear();
}

void AnimatedModel::OnGraphLoaded()
{
    // Prepare parameters and instance data
    GraphInstance.ClearState();
    SyncParameters();
}

void AnimatedModel::OnAssetChanged(Asset* asset, void* caller)
{
    if (caller == &SkinnedModel)
        OnSkinnedModelChanged();
    else if (caller == &AnimationGraph)
        OnGraphChanged();
}

void AnimatedModel::OnAssetLoaded(Asset* asset, void* caller)
{
    if (caller == &SkinnedModel)
        OnSkinnedModelLoaded();
    else if (caller == &AnimationGraph)
        OnGraphLoaded();
}

void AnimatedModel::OnAssetUnloaded(Asset* asset, void* caller)
{
}

bool AnimatedModel::HasContentLoaded() const
{
    return (SkinnedModel == nullptr || SkinnedModel->IsLoaded()) && Entries.HasContentLoaded();
}

void AnimatedModel::Update()
{
    // Update the mode
    _actualMode = UpdateMode;
    if (_actualMode == AnimationUpdateMode::Auto)
    {
        // TODO: handle low performance platforms

        if (_lastMinDstSqr < 3000.0f * 3000.0f)
            _actualMode = AnimationUpdateMode::EveryUpdate;
        else if (_lastMinDstSqr < 6000.0f * 6000.0f)
            _actualMode = AnimationUpdateMode::EverySecondUpdate;
        else if (_lastMinDstSqr < 10000.0f * 10000.0f)
            _actualMode = AnimationUpdateMode::EveryFourthUpdate;
        else
            _actualMode = AnimationUpdateMode::Manual;
    }

    // Check if update during this tick
    bool updateAnim = false;
    switch (_actualMode)
    {
    case AnimationUpdateMode::EveryFourthUpdate:
        updateAnim = _counter++ % 4 == 0;
        break;
    case AnimationUpdateMode::EverySecondUpdate:
        updateAnim = _counter++ % 2 == 0;
        break;
    case AnimationUpdateMode::EveryUpdate:
        updateAnim = true;
        break;
    default:
        break;
    }
    if (updateAnim && (UpdateWhenOffscreen || _lastMinDstSqr < MAX_Real))
        UpdateAnimation();

    _lastMinDstSqr = MAX_Real;
}

void AnimatedModel::Draw(RenderContext& renderContext)
{
    if (!SkinnedModel || !SkinnedModel->IsLoaded())
        return;
    if (renderContext.View.Pass == DrawPass::GlobalSDF)
        return;
    if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas)
        return; // Not supported
    ACTOR_GET_WORLD_MATRIX(this, view, world);
    GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, world);

    _lastMinDstSqr = Math::Min(_lastMinDstSqr, Vector3::DistanceSquared(_transform.Translation, renderContext.View.WorldPosition));
    if (_skinningData.IsReady())
    {
        // Flush skinning data with GPU
        if (_skinningData.IsDirty())
        {
            RenderListExtension.Items.Add({ _skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count() });
            _skinningData.OnFlush();
        }

        SkinnedMesh::DrawInfo draw;
        draw.Buffer = &Entries;
        draw.Skinning = &_skinningData;
        draw.World = &world;
        draw.DrawState = &_drawState;
        draw.Deformation = _deformation;
        PRAGMA_DISABLE_DEPRECATION_WARNINGS
        draw.DrawModes = DrawModes & renderContext.View.GetShadowsDrawPassMask(ShadowsMode);
        PRAGMA_ENABLE_DEPRECATION_WARNINGS
        draw.Bounds = _sphere;
        draw.Bounds.Center -= renderContext.View.Origin;
        draw.PerInstanceRandom = GetPerInstanceRandom();
        draw.LODBias = LODBias;
        draw.ForcedLOD = ForcedLOD;
        draw.SortOrder = SortOrder;
        draw.SetStencilValue(_layer);

        SkinnedModel->Draw(renderContext, draw);
    }

    GEOMETRY_DRAW_STATE_EVENT_END(_drawState, world);
}

void AnimatedModel::Draw(RenderContextBatch& renderContextBatch)
{
    if (!SkinnedModel || !SkinnedModel->IsLoaded())
        return;
    const RenderContext& renderContext = renderContextBatch.GetMainContext();
    Matrix world;
    const Float3 translation = _transform.Translation - renderContext.View.Origin;
    Matrix::Transformation(_transform.Scale, _transform.Orientation, translation, world);
    GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, world);

    _lastMinDstSqr = Math::Min(_lastMinDstSqr, Vector3::DistanceSquared(_transform.Translation, renderContext.View.WorldPosition));
    if (_skinningData.IsReady())
    {
        // Flush skinning data with GPU
        if (_skinningData.IsDirty())
        {
            RenderListExtension.Items.Add({ _skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count() });
            _skinningData.OnFlush();
        }

        SkinnedMesh::DrawInfo draw;
        draw.Buffer = &Entries;
        draw.Skinning = &_skinningData;
        draw.World = &world;
        draw.DrawState = &_drawState;
        draw.Deformation = _deformation;
        draw.DrawModes = DrawModes;
        draw.Bounds = _sphere;
        draw.Bounds.Center -= renderContext.View.Origin;
        draw.PerInstanceRandom = GetPerInstanceRandom();
        draw.LODBias = LODBias;
        draw.ForcedLOD = ForcedLOD;
        draw.SortOrder = SortOrder;
        draw.SetStencilValue(_layer);

        PRAGMA_DISABLE_DEPRECATION_WARNINGS
        if (ShadowsMode != ShadowsCastingMode::All)
        {
            // To handle old ShadowsMode option for all meshes we need to call per-context drawing (no batching opportunity)
            // TODO: maybe deserialize ShadowsMode into ModelInstanceBuffer entries options?
            for (auto& e : renderContextBatch.Contexts)
            {
                draw.DrawModes = DrawModes & e.View.GetShadowsDrawPassMask(ShadowsMode);
                SkinnedModel->Draw(e, draw);
            }
        }
        else
        {
            SkinnedModel->Draw(renderContextBatch, draw);
        }
        PRAGMA_ENABLE_DEPRECATION_WARNINGS
    }

    GEOMETRY_DRAW_STATE_EVENT_END(_drawState, world);
}

#if USE_EDITOR

#include "Engine/Debug/DebugDraw.h"

void AnimatedModel::OnDebugDrawSelected()
{
    DEBUG_DRAW_WIRE_BOX(_box, Color::Violet.RGBMultiplied(0.8f), 0, true);

    // Base
    ModelInstanceActor::OnDebugDrawSelected();
}

void AnimatedModel::OnDebugDraw()
{
    if (ShowDebugDrawSkeleton && SkinnedModel && AnimationGraph)
    {
        if (GraphInstance.NodesPose.IsEmpty())
            PreInitSkinningData();
        Matrix world;
        GetLocalToWorldMatrix(world);

        // Draw bounding box at the node locations
        const float boxSize = Math::Min(1.0f, (float)_sphere.Radius / 100.0f);
        OrientedBoundingBox localBox(Vector3(-boxSize), Vector3(boxSize));
        for (int32 nodeIndex = 0; nodeIndex < GraphInstance.NodesPose.Count(); nodeIndex++)
        {
            Matrix transform = GraphInstance.NodesPose[nodeIndex] * world;
            Float3 scale, translation;
            Matrix3x3 rotation;
            transform.Decompose(scale, rotation, translation);
            transform = Matrix::Invert(Matrix::Scaling(scale)) * transform;
            OrientedBoundingBox box = localBox * transform;
            DEBUG_DRAW_WIRE_BOX(box, Color::Green, 0, false);
        }

        // Nodes connections
        for (int32 nodeIndex = 0; nodeIndex < SkinnedModel->Skeleton.Nodes.Count(); nodeIndex++)
        {
            int32 parentIndex = SkinnedModel->Skeleton.Nodes[nodeIndex].ParentIndex;
            if (parentIndex != -1)
            {
                Float3 parentPos = (GraphInstance.NodesPose[parentIndex] * world).GetTranslation();
                Float3 bonePos = (GraphInstance.NodesPose[nodeIndex] * world).GetTranslation();
                DEBUG_DRAW_LINE(parentPos, bonePos, Color::Green, 0, false);
            }
        }
    }

    ModelInstanceActor::OnDebugDraw();
}

BoundingBox AnimatedModel::GetEditorBox() const
{
    if (SkinnedModel)
        SkinnedModel->WaitForLoaded(100);
    return BoundingBox::MakeScaled(_box, 1.0f / BoundsScale);
}

#endif

bool AnimatedModel::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)
{
    bool result = false;

    if (SkinnedModel != nullptr && SkinnedModel->IsLoaded())
    {
        SkinnedMesh* mesh;
        result |= SkinnedModel->Intersects(ray, _transform, distance, normal, &mesh);
    }

    return result;
}

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

    SERIALIZE_GET_OTHER_OBJ(AnimatedModel);

    SERIALIZE(SkinnedModel);
    SERIALIZE(AnimationGraph);
    SERIALIZE(PerBoneMotionBlur);
    SERIALIZE(UseTimeScale);
    SERIALIZE(UpdateWhenOffscreen);
    SERIALIZE(UpdateSpeed);
    SERIALIZE(UpdateMode);
    SERIALIZE(BoundsScale);
    SERIALIZE(CustomBounds);
    SERIALIZE(LODBias);
    SERIALIZE(ForcedLOD);
    SERIALIZE(SortOrder);
    SERIALIZE(DrawModes);
    PRAGMA_DISABLE_DEPRECATION_WARNINGS
    SERIALIZE(ShadowsMode);
    PRAGMA_ENABLE_DEPRECATION_WARNINGS
    SERIALIZE(RootMotionTarget);
    SERIALIZE_MEMBER(Buffer, Entries);
}

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

    DESERIALIZE(SkinnedModel);
    DESERIALIZE(AnimationGraph);
    DESERIALIZE(PerBoneMotionBlur);
    DESERIALIZE(UseTimeScale);
    DESERIALIZE(UpdateWhenOffscreen);
    DESERIALIZE(UpdateSpeed);
    DESERIALIZE(UpdateMode);
    DESERIALIZE(BoundsScale);
    DESERIALIZE(CustomBounds);
    DESERIALIZE(LODBias);
    DESERIALIZE(ForcedLOD);
    DESERIALIZE(SortOrder);
    DESERIALIZE(DrawModes);
    PRAGMA_DISABLE_DEPRECATION_WARNINGS
    DESERIALIZE(ShadowsMode);
    PRAGMA_ENABLE_DEPRECATION_WARNINGS
    DESERIALIZE(RootMotionTarget);
    DESERIALIZE_MEMBER(Buffer, Entries);

    // [Deprecated on 07.02.2022, expires on 07.02.2024]
    if (modifier->EngineBuild <= 6330)
    {
        MARK_CONTENT_DEPRECATED();
        DrawModes |= DrawPass::GlobalSDF;
    }
    // [Deprecated on 27.04.2022, expires on 27.04.2024]
    if (modifier->EngineBuild <= 6331)
    {
        MARK_CONTENT_DEPRECATED();
        DrawModes |= DrawPass::GlobalSurfaceAtlas;
    }
}

const Span<MaterialSlot> AnimatedModel::GetMaterialSlots() const
{
    const auto model = SkinnedModel.Get();
    if (model && !model->WaitForLoaded())
        return ToSpan(model->MaterialSlots);
    return Span<MaterialSlot>();
}

MaterialBase* AnimatedModel::GetMaterial(int32 entryIndex)
{
    if (SkinnedModel)
        SkinnedModel->WaitForLoaded();
    else
        return nullptr;
    CHECK_RETURN(entryIndex >= 0 && entryIndex < SkinnedModel->MaterialSlots.Count(), nullptr);
    MaterialBase* material = entryIndex < Entries.Count() ? Entries[entryIndex].Material.Get() : nullptr;
    if (!material)
    {
        material = SkinnedModel->MaterialSlots[entryIndex].Material.Get();
        if (!material)
            material = GPUDevice::Instance->GetDefaultMaterial();
    }
    return material;
}

ModelBase* AnimatedModel::GetModel()
{
    return SkinnedModel.Get();
}

bool AnimatedModel::IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal)
{
    auto model = SkinnedModel.Get();
    if (!model || !model->IsInitialized() || model->GetLoadedLODs() == 0)
        return false;

    // Find mesh in the highest loaded LOD that is using the given material slot index and ray hits it
    auto& meshes = model->LODs[model->HighestResidentLODIndex()].Meshes;
    for (int32 i = 0; i < meshes.Count(); i++)
    {
        const auto& mesh = meshes[i];
        if (mesh.GetMaterialSlotIndex() == entryIndex && mesh.Intersects(ray, _transform, distance, normal))
            return true;
    }

    distance = 0;
    normal = Vector3::Up;
    return false;
}

bool AnimatedModel::IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex)
{
    auto model = SkinnedModel.Get();
    if (!model || !model->IsInitialized() || model->GetLoadedLODs() == 0)
        return false;

    // Find mesh in the highest loaded LOD that is using the given material slot index and ray hits it
    bool result = false;
    Real closest = MAX_Real;
    Vector3 closestNormal = Vector3::Up;
    int32 closestEntry = -1;
    auto& meshes = model->LODs[model->HighestResidentLODIndex()].Meshes;
    for (int32 i = 0; i < meshes.Count(); i++)
    {
        // Test intersection with mesh and check if is closer than previous
        const auto& mesh = meshes[i];
        Real dst;
        Vector3 nrm;
        if (mesh.Intersects(ray, _transform, dst, nrm) && dst < closest)
        {
            result = true;
            closest = dst;
            closestNormal = nrm;
            closestEntry = mesh.GetMaterialSlotIndex();
        }
    }

    distance = closest;
    normal = closestNormal;
    entryIndex = closestEntry;
    return result;
}

bool AnimatedModel::GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const
{
    count = 0;
    if (ref.LODIndex < 0 || ref.MeshIndex < 0)
        return true;
    const auto model = SkinnedModel.Get();
    if (!model || model->WaitForLoaded())
        return true;
    auto& lod = model->LODs[Math::Min(ref.LODIndex, model->LODs.Count() - 1)];
    auto& mesh = lod.Meshes[Math::Min(ref.MeshIndex, lod.Meshes.Count() - 1)];
    return mesh.DownloadDataCPU(type, result, count, layout);
}

MeshBase* AnimatedModel::GetMesh(const MeshReference& ref) const
{
    const auto model = SkinnedModel.Get();
    if (!model || model->WaitForLoaded())
        return nullptr;
    auto& lod = model->LODs[Math::Min(ref.LODIndex, model->LODs.Count() - 1)];
    auto& mesh = lod.Meshes[Math::Min(ref.MeshIndex, lod.Meshes.Count() - 1)];
    return &mesh;
}

MeshDeformation* AnimatedModel::GetMeshDeformation() const
{
    if (!_deformation)
        _deformation = New<MeshDeformation>();
    return _deformation;
}

void AnimatedModel::OnDeleteObject()
{
    // Ensure this object is no longer referenced for anim update
    Animations::RemoveFromUpdate(this);

    ModelInstanceActor::OnDeleteObject();
}

void AnimatedModel::WaitForModelLoad()
{
    if (SkinnedModel)
        SkinnedModel->WaitForLoaded();
}
