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

#include "VisualScript.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Types/DataContainer.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Scripting/MException.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MMethod.h"
#include "Engine/Scripting/ManagedCLR/MField.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
#include "Engine/Scripting/ManagedCLR/MType.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Serialization/JsonWriter.h"
#include "Engine/Utilities/StringConverter.h"
#include "FlaxEngine.Gen.h"

namespace
{
    struct VisualScriptThread
    {
        uint32 StackFramesCount;
        VisualScripting::StackFrame* Stack;
    };

    ThreadLocal<VisualScriptThread, 32> ThreadStacks;
    VisualScriptingBinaryModule VisualScriptingModule;
    VisualScriptExecutor VisualScriptingExecutor;

    void PrintStack(LogType type)
    {
        const String stack = VisualScripting::GetStackTrace();
        Log::Logger::Write(type, TEXT("Visual Script stack trace:"));
        Log::Logger::Write(type, stack);
        Log::Logger::Write(type, TEXT(""));
    }
}

#if VISUAL_SCRIPT_DEBUGGING
Action VisualScripting::DebugFlow;
#endif

static_assert(TIsPODType<VisualScripting::StackFrame>::Value, "VisualScripting::StackFrame must be POD type.");
static_assert(TIsPODType<VisualScriptThread>::Value, "VisualScriptThread must be POD type.");

bool VisualScriptGraph::onNodeLoaded(Node* n)
{
    switch (n->GroupID)
    {
        // Function
    case 16:
        switch (n->TypeID)
        {
            // Invoke Method
        case 4:
            n->Data.InvokeMethod.Method = nullptr;
            break;
            // Get/Set Field
        case 7:
        case 8:
            n->Data.GetSetField.Field = nullptr;
            break;
        }
    }

    // Base
    return VisjectGraph<VisualScriptGraphNode, VisjectGraphBox, VisjectGraphParameter>::onNodeLoaded(n);
}

VisualScriptExecutor::VisualScriptExecutor()
{
    _perGroupProcessCall[2] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupConstants;
    _perGroupProcessCall[4] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupPacking;
    _perGroupProcessCall[6] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupParameters;
    _perGroupProcessCall[7] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupTools;
    _perGroupProcessCall[16] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupFunction;
    _perGroupProcessCall[17] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupFlow;
}

VisjectExecutor::Value VisualScriptExecutor::eatBox(Node* caller, Box* box)
{
    // Check if graph is looped or is too deep
    auto& stack = ThreadStacks.Get();
    if (stack.StackFramesCount >= VISUAL_SCRIPT_GRAPH_MAX_CALL_STACK)
    {
        OnError(caller, box, TEXT("Graph is looped or too deep!"));
        return Value::Zero;
    }
#if !BUILD_RELEASE
    if (box == nullptr)
    {
        OnError(caller, box, TEXT("Null graph box!"));
        return Value::Zero;
    }
#endif
    const auto parentNode = box->GetParent<Node>();

    // Add to the calling stack
    VisualScripting::StackFrame frame = *stack.Stack;
    frame.Node = parentNode;
    frame.Box = box;
    frame.PreviousFrame = stack.Stack;
    stack.Stack = &frame;
    stack.StackFramesCount++;

#if VISUAL_SCRIPT_DEBUGGING
    // Debugger event
    VisualScripting::DebugFlow();
#endif

    // Call per group custom processing event
    Value value;
    const ProcessBoxHandler func = _perGroupProcessCall[parentNode->GroupID];
    (this->*func)(box, parentNode, value);

    // Remove from the calling stack
    stack.StackFramesCount--;
    stack.Stack = frame.PreviousFrame;

    return value;
}

VisjectExecutor::Graph* VisualScriptExecutor::GetCurrentGraph() const
{
    auto& stack = ThreadStacks.Get();
    return stack.Stack && stack.Stack->Script ? &stack.Stack->Script->Graph : nullptr;
}

void VisualScriptExecutor::ProcessGroupConstants(Box* box, Node* node, Value& value)
{
    switch (node->TypeID)
    {
        // Enum
    case 11:
    {
        value = node->Values[0];
        break;
    }
    default:
        VisjectExecutor::ProcessGroupConstants(box, node, value);
        break;
    }
}

void VisualScriptExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
{
    switch (node->TypeID)
    {
        // Pack Structure
    case 26:
    {
        // Find type
        const StringView typeName(node->Values[0]);
        const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
        const StringAnsiView typeNameAnsiView(typeNameAnsi.Get(), typeName.Length());
        const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeNameAnsiView);
        if (!typeHandle)
        {
            const auto mclass = Scripting::FindClass(typeNameAnsiView);
            if (mclass)
            {
                // Fallback to C#-only types
                bool failed = false;
                auto instance = mclass->CreateInstance();
                value = instance;
                auto& layoutCache = node->Values[1];
                CHECK(layoutCache.Type.Type == VariantType::Blob);
                MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
                const byte version = stream.ReadByte();
                if (version == 1)
                {
                    int32 fieldsCount;
                    stream.ReadInt32(&fieldsCount);
                    for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
                    {
                        box = &node->Boxes[boxId];
                        String fieldName;
                        stream.ReadString(&fieldName, 11);
                        VariantType fieldType;
                        stream.ReadVariantType(&fieldType);
                        if (box && box->HasConnection())
                        {
                            StringAsANSI<40> fieldNameAnsi(*fieldName, fieldName.Length());
                            auto field = mclass->GetField(fieldNameAnsi.Get());
                            if (field)
                            {
                                Variant fieldValue = eatBox(node, box->FirstConnection());
                                field->SetValue(instance, MUtils::VariantToManagedArgPtr(fieldValue, field->GetType(), failed));
                            }
                        }
                    }
                }
            }
            else if (typeName.HasChars())
            {
                LOG(Error, "Missing type '{0}'", typeName);
                PrintStack(LogType::Error);
            }
            return;
        }
        const ScriptingType& type = typeHandle.GetType();

        // Allocate structure data and initialize it with native constructor
        value.SetType(VariantType(VariantType::Structure, typeNameAnsiView));

        // Setup structure fields
        auto& layoutCache = node->Values[1];
        CHECK(layoutCache.Type.Type == VariantType::Blob);
        MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
        const byte version = stream.ReadByte();
        if (version == 1)
        {
            int32 fieldsCount;
            stream.ReadInt32(&fieldsCount);
            for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
            {
                box = &node->Boxes[boxId];
                String fieldName;
                stream.ReadString(&fieldName, 11);
                VariantType fieldType;
                stream.ReadVariantType(&fieldType);
                if (box && box->HasConnection())
                {
                    const Variant fieldValue = eatBox(node, box->FirstConnection());
                    type.Struct.SetField(value.AsBlob.Data, fieldName, fieldValue);
                }
            }
        }
        break;
    }
        // Unpack Structure
    case 36:
    {
        // Get value with structure data
        const Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
        if (!node->GetBox(0)->HasConnection())
            return;

        // Find type
        const StringView typeName(node->Values[0]);
        const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
        const StringAnsiView typeNameAnsiView(typeNameAnsi.Get(), typeName.Length());
        const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeNameAnsiView);
        if (!typeHandle)
        {
            const auto mclass = Scripting::FindClass(typeNameAnsiView);
            if (mclass)
            {
                // Fallback to C#-only types
                auto instance = mono_gchandle_get_target(structureValue.AsUint);
                CHECK(instance);
                if (structureValue.Type.Type != VariantType::ManagedObject || mono_object_get_class(instance) != mclass->GetNative())
                {
                    LOG(Error, "Cannot unpack value of type {0} to structure of type {1}", String(MUtils::GetClassFullname(instance)), typeName);
                    PrintStack(LogType::Error);
                    return;
                }
                auto& layoutCache = node->Values[1];
                CHECK(layoutCache.Type.Type == VariantType::Blob);
                MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
                const byte version = stream.ReadByte();
                if (version == 1)
                {
                    int32 fieldsCount;
                    stream.ReadInt32(&fieldsCount);
                    for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
                    {
                        String fieldName;
                        stream.ReadString(&fieldName, 11);
                        VariantType fieldType;
                        stream.ReadVariantType(&fieldType);
                        if (box->ID == boxId)
                        {
                            StringAsANSI<40> fieldNameAnsi(*fieldName, fieldName.Length());
                            auto field = mclass->GetField(fieldNameAnsi.Get());
                            if (field)
                                value = MUtils::UnboxVariant(field->GetValueBoxed(instance));
                            break;
                        }
                    }
                }
            }
            else if (typeName.HasChars())
            {
                LOG(Error, "Missing type '{0}'", typeName);
                PrintStack(LogType::Error);
            }
            return;
        }
        const ScriptingType& type = typeHandle.GetType();
        if (structureValue.Type.Type != VariantType::Structure || StringUtils::Compare(typeNameAnsi.Get(), structureValue.Type.TypeName) != 0)
        {
            LOG(Error, "Cannot unpack value of type {0} to structure of type {1}", structureValue.Type, typeName);
            PrintStack(LogType::Error);
            return;
        }

        // Read structure field
        auto& layoutCache = node->Values[1];
        CHECK(layoutCache.Type.Type == VariantType::Blob);
        MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
        const byte version = stream.ReadByte();
        if (version == 1)
        {
            int32 fieldsCount;
            stream.ReadInt32(&fieldsCount);
            for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
            {
                String fieldName;
                stream.ReadString(&fieldName, 11);
                VariantType fieldType;
                stream.ReadVariantType(&fieldType);
                if (box->ID == boxId)
                {
                    type.Struct.GetField(structureValue.AsBlob.Data, fieldName, value);
                    break;
                }
            }
        }
        break;
    }
    default:
        VisjectExecutor::ProcessGroupPacking(box, node, value);
        break;
    }
}

void VisualScriptExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value)
{
    switch (node->TypeID)
    {
        // Get
    case 3:
    {
        int32 paramIndex;
        auto& stack = ThreadStacks.Get();
        if (!stack.Stack->Instance)
        {
            LOG(Error, "Cannot access Visual Script parameter without instance.");
            PrintStack(LogType::Error);
            break;
        }
        const auto param = stack.Stack->Script->Graph.GetParameter((Guid)node->Values[0], paramIndex);
        const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
        if (param && instanceParams)
        {
            value = instanceParams->Value[paramIndex];
        }
        else
        {
            LOG(Error, "Failed to access Visual Script parameter for {0}.", stack.Stack->Instance->ToString());
            PrintStack(LogType::Error);
        }
        break;
    }
        // Set
    case 4:
    {
        int32 paramIndex;
        auto& stack = ThreadStacks.Get();
        if (!stack.Stack->Instance)
        {
            LOG(Error, "Cannot access Visual Script parameter without instance.");
            PrintStack(LogType::Error);
            break;
        }
        const auto param = stack.Stack->Script->Graph.GetParameter((Guid)node->Values[0], paramIndex);
        const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
        if (param && instanceParams)
        {
            instanceParams->Value[paramIndex] = tryGetValue(node->GetBox(1), 1, Value::Zero);
        }
        else
        {
            LOG(Error, "Failed to access Visual Script parameter for {0}.", stack.Stack->Instance->ToString());
            PrintStack(LogType::Error);
        }
        if (node->Boxes[2].HasConnection())
            eatBox(node, node->Boxes[2].FirstConnection());
        break;
    }
    default:
        break;
    }
}

void VisualScriptExecutor::ProcessGroupTools(Box* box, Node* node, Value& value)
{
    switch (node->TypeID)
    {
        // This Instance
    case 19:
    {
        value = ThreadStacks.Get().Stack->Instance;
        break;
    }
        // As
    case 22:
    {
        value = Value::Null;
        const auto obj = (ScriptingObject*)tryGetValue(node->GetBox(1), Value::Null);
        if (obj)
        {
            const StringView typeName(node->Values[0]);
            const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
            const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
            const auto objClass = obj->GetClass();
            if (typeHandle && objClass && objClass->IsSubClassOf(typeHandle.GetType().ManagedClass))
                value = obj;
        }
        break;
    }
        // Type Reference node
    case 23:
    {
        const StringView typeName(node->Values[0]);
        if (box->ID == 0)
            value.SetTypename(typeName);
        else
            value = typeName;
        break;
    }
        // Is
    case 24:
    {
        value = Value::False;
        const auto obj = (ScriptingObject*)tryGetValue(node->GetBox(1), Value::Null);
        if (obj)
        {
            const StringView typeName(node->Values[0]);
            const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
            const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
            const auto objClass = obj->GetClass();
            value.AsBool = typeHandle && objClass && objClass->IsSubClassOf(typeHandle.GetType().ManagedClass);
        }
        break;
    }
        // Cast
    case 25:
    {
        if (box->ID == 0)
        {
            // Get object and try to cast it
            auto obj = (ScriptingObject*)tryGetValue(node->GetBox(1), Value::Null);
            if (obj)
            {
                const StringView typeName(node->Values[0]);
                const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
                const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
                const auto objClass = obj->GetClass();
                if (!typeHandle || !objClass || !objClass->IsSubClassOf(typeHandle.GetType().ManagedClass))
                    obj = nullptr;
            }

            // Cache cast object value (only if it's valid)
            const bool isValid = obj != nullptr;
            if (isValid)
            {
                const auto scope = ThreadStacks.Get().Stack->Scope;
                int32 returnedIndex = 0;
                for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
                {
                    const auto& e = scope->ReturnedValues[returnedIndex];
                    if (e.NodeId == node->ID && e.BoxId == 4)
                        break;
                }
                if (returnedIndex == scope->ReturnedValues.Count())
                    scope->ReturnedValues.AddOne();
                auto& returnedValue = scope->ReturnedValues[returnedIndex];
                returnedValue.NodeId = node->ID;
                returnedValue.BoxId = 4;
                returnedValue.Value = obj;
            }

            // Call graph further
            const auto impulseBox = &node->Boxes[isValid ? 2 : 3];
            if (impulseBox && impulseBox->HasConnection())
                eatBox(node, impulseBox->FirstConnection());
        }
        else if (box->ID == 4)
        {
            // Find returned value inside the current scope
            const auto scope = ThreadStacks.Get().Stack->Scope;
            int32 returnedIndex = 0;
            for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
            {
                const auto& e = scope->ReturnedValues[returnedIndex];
                if (e.NodeId == node->ID && e.BoxId == 4)
                    break;
            }
            if (returnedIndex != scope->ReturnedValues.Count())
                value = scope->ReturnedValues[returnedIndex].Value;
        }
        break;
    }
        // Cast Value
    case 26:
    {
        if (box->ID == 0)
        {
            // Get object and try to cast it
            auto obj = tryGetValue(node->GetBox(1), Value::Null);
            if (obj)
            {
                const StringView typeName(node->Values[0]);
                const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
                if (StringUtils::Compare(typeNameAnsi.Get(), obj.Type.GetTypeName()) != 0)
                {
                    MonoClass* klass = Scripting::FindClassNative(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
                    MonoClass* objKlass = MUtils::GetClass(obj);
                    if (!klass || !objKlass || mono_class_is_subclass_of(objKlass, klass, false) == 0)
                        obj = Value::Null;
                }
            }

            // Cache cast object value (only if it's valid)
            const bool isValid = obj != Value::Null;
            if (isValid)
            {
                const auto scope = ThreadStacks.Get().Stack->Scope;
                int32 returnedIndex = 0;
                for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
                {
                    const auto& e = scope->ReturnedValues[returnedIndex];
                    if (e.NodeId == node->ID && e.BoxId == 4)
                        break;
                }
                if (returnedIndex == scope->ReturnedValues.Count())
                    scope->ReturnedValues.AddOne();
                auto& returnedValue = scope->ReturnedValues[returnedIndex];
                returnedValue.NodeId = node->ID;
                returnedValue.BoxId = 4;
                returnedValue.Value = MoveTemp(obj);
            }

            // Call graph further
            const auto impulseBox = &node->Boxes[isValid ? 2 : 3];
            if (impulseBox && impulseBox->HasConnection())
                eatBox(node, impulseBox->FirstConnection());
        }
        else if (box->ID == 4)
        {
            // Find returned value inside the current scope
            const auto scope = ThreadStacks.Get().Stack->Scope;
            int32 returnedIndex = 0;
            for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
            {
                const auto& e = scope->ReturnedValues[returnedIndex];
                if (e.NodeId == node->ID && e.BoxId == 4)
                    break;
            }
            if (returnedIndex != scope->ReturnedValues.Count())
                value = scope->ReturnedValues[returnedIndex].Value;
        }
        break;
    }
        // Is Null
    case 27:
        value = tryGetValue(node->GetBox(1), Value::Null) == Value::Null;
        break;
        // Is Valid
    case 28:
        value = tryGetValue(node->GetBox(1), Value::Null) != Value::Null;
        break;
    default:
        VisjectExecutor::ProcessGroupTools(box, node, value);
        break;
    }
}

void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& value)
{
    switch (node->TypeID)
    {
        // Method Override
    case 3:
    {
        if (boxBase->ID == 0)
        {
            // Call graph further
            if (boxBase->HasConnection())
                eatBox(node, boxBase->FirstConnection());
        }
        else
        {
            // Evaluate overriden method parameter value from the current scope
            auto& scope = ThreadStacks.Get().Stack->Scope;
            value = scope->Parameters[boxBase->ID - 1];
        }
        break;
    }
        // Invoke Method
    case 4:
    {
        // Call Impulse or Pure Method
        if (boxBase->ID == 0 || (bool)node->Values[3])
        {
            auto& cache = node->Data.InvokeMethod;
            if (!cache.Method)
            {
                // Load method signature
                const auto typeName = (StringView)node->Values[0];
                const auto methodName = (StringView)node->Values[1];
                const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
                const StringAsANSI<60> methodNameAnsi(methodName.Get(), methodName.Length());
                ScriptingTypeMethodSignature signature;
                signature.Name = StringAnsiView(methodNameAnsi.Get(), methodName.Length());
                auto& signatureCache = node->Values[4];
                if (signatureCache.Type.Type != VariantType::Blob)
                {
                    LOG(Error, "Missing method '{0}::{1}' signature data", typeName, methodName);
                    PrintStack(LogType::Error);
                    return;
                }
                MemoryReadStream stream((byte*)signatureCache.AsBlob.Data, signatureCache.AsBlob.Length);
                const byte version = stream.ReadByte();
                if (version == 4)
                {
                    signature.IsStatic = stream.ReadBool();
                    stream.ReadVariantType(&signature.ReturnType);
                    int32 signatureParamsCount;
                    stream.ReadInt32(&signatureParamsCount);
                    signature.Params.Resize(signatureParamsCount);
                    for (int32 i = 0; i < signatureParamsCount; i++)
                    {
                        auto& param = signature.Params[i];
                        int32 parameterNameLength;
                        stream.ReadInt32(&parameterNameLength);
                        stream.SetPosition(stream.GetPosition() + parameterNameLength * sizeof(Char));
                        stream.ReadVariantType(&param.Type);
                        param.IsOut = stream.ReadBool();
                    }
                }
                else
                {
                    LOG(Error, "Unsupported method '{0}::{1}' signature data", typeName, methodName);
                    PrintStack(LogType::Error);
                    return;
                }
                void* method;
                ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
                if (typeHandle)
                {
                    // Find method in the scripting type
                    method = typeHandle.Module->FindMethod(typeHandle, signature);
                    if (!method)
                    {
                        LOG(Error, "Missing method '{0}::{1}'", typeName, methodName);
                        PrintStack(LogType::Error);
                        return;
                    }
                }
                else
                {
                    // Fallback to C#-only types
                    const auto mclass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
                    if (mclass)
                    {
                        method = ManagedBinaryModule::FindMethod(mclass, signature);
                        if (!method)
                        {
                            LOG(Error, "Missing method '{0}::{1}'", typeName, methodName);
                            PrintStack(LogType::Error);
                            return;
                        }
                    }
                    else
                    {
                        if (typeName.HasChars())
                        {
                            LOG(Error, "Missing type '{0}'", typeName);
                            PrintStack(LogType::Error);
                        }
                        return;
                    }

                    // Mock the scripting type for C# method that doesn't exist in engine script types database
                    typeHandle = ScriptingTypeHandle(GetBinaryModuleFlaxEngine(), 0);
                }

                // Cache method data
                cache.Method = method;
                cache.Module = typeHandle.Module;
                cache.ParamsCount = signature.Params.Count();
                cache.IsStatic = signature.IsStatic;
                cache.OutParamsMask = 0;
                for (int32 paramIdx = 0; paramIdx < signature.Params.Count() && paramIdx < 32; paramIdx++)
                    cache.OutParamsMask |= signature.Params[paramIdx].IsOut ? (1 << static_cast<uint32>(paramIdx)) : 0;
            }

            // Evaluate object instance for non-static methods
            Variant instance;
            if (!cache.IsStatic)
            {
                // Evaluate object instance
                const auto box = node->GetBox(1);
                if (box->HasConnection())
                {
                    instance = eatBox(node, box->FirstConnection());
                }
                else
                {
                    // Unconnected instance box is treated as this script instance if type matches the member method class
                    auto& stack = ThreadStacks.Get();
                    instance.SetObject(stack.Stack->Instance);
                }
            }

            // Evaluate parameter values
            Variant* paramValues = (Variant*)alloca(cache.ParamsCount * sizeof(Variant));
            bool hasOutParams = false;
            for (int32 paramIdx = 0; paramIdx < cache.ParamsCount; paramIdx++)
            {
                auto& paramValue = paramValues[paramIdx];
                Memory::ConstructItem(&paramValue);
                const bool isOut = paramIdx < 32 && (cache.OutParamsMask & (1 << static_cast<uint32>(paramIdx))) != 0;
                hasOutParams |= isOut;
                const auto box = node->GetBox(paramIdx + 4);
                if (box->HasConnection() && !isOut)
                    paramValue = eatBox(node, box->FirstConnection());
                else if (node->Values.Count() > 5 + paramIdx)
                    paramValue = node->Values[5 + paramIdx];
            }

            // Invoke the method
            Variant result;
            if (cache.Module->InvokeMethod(cache.Method, instance, Span<Variant>(paramValues, cache.ParamsCount), result))
            {
                PrintStack(LogType::Error);
            }
            else
            {
                // Cache returned value inside the current scope
                const auto scope = ThreadStacks.Get().Stack->Scope;
                {
                    int32 returnedIndex = 0;
                    for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
                    {
                        const auto& e = scope->ReturnedValues[returnedIndex];
                        if (e.NodeId == node->ID && e.BoxId == 3)
                            break;
                    }
                    if (returnedIndex == scope->ReturnedValues.Count())
                        scope->ReturnedValues.AddOne();
                    auto& returnedValue = scope->ReturnedValues[returnedIndex];
                    returnedValue.NodeId = node->ID;
                    returnedValue.BoxId = 3;
                    returnedValue.Value = MoveTemp(result);
                }

                // Cache output parameters values inside the current scope
                if (hasOutParams)
                {
                    for (int32 paramIdx = 0; paramIdx < cache.ParamsCount; paramIdx++)
                    {
                        const bool isOut = paramIdx < 32 && (cache.OutParamsMask & (1 << static_cast<uint32>(paramIdx))) != 0;
                        if (isOut && node->GetBox(paramIdx + 4)->HasConnection())
                        {
                            int32 returnedIndex = 0;
                            for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
                            {
                                const auto& e = scope->ReturnedValues[returnedIndex];
                                if (e.NodeId == node->ID && e.BoxId == paramIdx + 4)
                                    break;
                            }
                            if (returnedIndex == scope->ReturnedValues.Count())
                                scope->ReturnedValues.AddOne();
                            auto& returnedValue = scope->ReturnedValues[returnedIndex];
                            returnedValue.NodeId = node->ID;
                            returnedValue.BoxId = paramIdx + 4;
                            returnedValue.Value = MoveTemp(paramValues[paramIdx]);
                        }
                    }
                }

                // Call graph further
                const auto returnedImpulse = &node->Boxes[2];
                if (returnedImpulse && returnedImpulse->HasConnection())
                    eatBox(node, returnedImpulse->FirstConnection());
            }

            // Free parameters data
            Memory::DestructItems(paramValues, cache.ParamsCount);
        }
        // Returned value or Output Parameter
        if (boxBase->ID == 3 || boxBase->ID >= 4)
        {
            // Find returned value inside the current scope (from the previous method call)
            const auto scope = ThreadStacks.Get().Stack->Scope;
            int32 returnedIndex = 0;
            for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
            {
                const auto& e = scope->ReturnedValues[returnedIndex];
                if (e.NodeId == node->ID && e.BoxId == boxBase->ID)
                    break;
            }
            if (returnedIndex != scope->ReturnedValues.Count())
                value = scope->ReturnedValues[returnedIndex].Value;
        }
        break;
    }
        // Return
    case 5:
    {
        auto& scope = ThreadStacks.Get().Stack->Scope;
        scope->FunctionReturn = tryGetValue(node->GetBox(1), Value::Zero);
        break;
    }
        // Function
    case 6:
    {
        if (boxBase->ID == 0)
        {
            // Call function
            if (boxBase->HasConnection())
                eatBox(node, boxBase->FirstConnection());
        }
        else
        {
            // Evaluate method parameter value from the current scope
            auto& scope = ThreadStacks.Get().Stack->Scope;
            value = scope->Parameters[boxBase->ID - 1];
        }
        break;
    }
        // Get Field
    case 7:
    {
        auto& cache = node->Data.GetSetField;
        if (!cache.Field)
        {
            const auto typeName = (StringView)node->Values[0];
            const auto fieldName = (StringView)node->Values[1];
            const auto fieldTypeName = (StringView)node->Values[2];
            const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
            const StringAsANSI<60> fieldNameAnsi(fieldName.Get(), fieldName.Length());
            const StringAsANSI<100> fieldTypeNameAnsi(fieldTypeName.Get(), fieldTypeName.Length());
            void* field;
            ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
            if (typeHandle)
            {
                // Find field in the scripting type
                field = typeHandle.Module->FindField(typeHandle, fieldNameAnsi.Get());
                if (!field)
                {
                    LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
                    PrintStack(LogType::Error);
                    return;
                }
            }
            else
            {
                // Fallback to C#-only types
                const auto mclass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
                if (mclass)
                {
                    field = mclass->GetField(fieldNameAnsi.Get());
                    if (!field)
                    {
                        LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
                        PrintStack(LogType::Error);
                        return;
                    }
                }
                else
                {
                    if (typeName.HasChars())
                    {
                        LOG(Error, "Missing type '{0}'", typeName);
                        PrintStack(LogType::Error);
                    }
                    return;
                }

                // Mock the scripting type for C# field that doesn't exist in engine script types database
                typeHandle = ScriptingTypeHandle(GetBinaryModuleFlaxEngine(), 0);
            }

            // Cache field data
            cache.Field = field;
            cache.Module = typeHandle.Module;
            ScriptingTypeFieldSignature signature;
            cache.Module->GetFieldSignature(field, signature);
            cache.IsStatic = signature.IsStatic;
        }

        // Evaluate object instance for non-static fields
        Variant instance;
        if (!cache.IsStatic)
        {
            // Evaluate object instance
            const auto box = node->GetBox(1);
            if (box->HasConnection())
            {
                instance = eatBox(node, box->FirstConnection());
            }
            else
            {
                // Unconnected instance box is treated as this script instance if type matches the member field class
                auto& stack = ThreadStacks.Get();
                instance.SetObject(stack.Stack->Instance);
            }
        }

        // Get field value
        if (cache.Module->GetFieldValue(cache.Field, instance, value))
        {
            PrintStack(LogType::Error);
        }
        break;
    }
        // Get Field
    case 8:
    {
        auto& cache = node->Data.GetSetField;
        if (!cache.Field)
        {
            const auto typeName = (StringView)node->Values[0];
            const auto fieldName = (StringView)node->Values[1];
            const auto fieldTypeName = (StringView)node->Values[2];
            const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
            const StringAsANSI<60> fieldNameAnsi(fieldName.Get(), fieldName.Length());
            const StringAsANSI<100> fieldTypeNameAnsi(fieldTypeName.Get(), fieldTypeName.Length());
            void* field;
            ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
            if (typeHandle)
            {
                // Find field in the scripting type
                field = typeHandle.Module->FindField(typeHandle, fieldNameAnsi.Get());
                if (!field)
                {
                    LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
                    PrintStack(LogType::Error);
                    return;
                }
            }
            else
            {
                // Fallback to C#-only types
                const auto mclass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
                if (mclass)
                {
                    field = mclass->GetField(fieldNameAnsi.Get());
                    if (!field)
                    {
                        LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
                        PrintStack(LogType::Error);
                        return;
                    }
                }
                else
                {
                    if (typeName.HasChars())
                    {
                        LOG(Error, "Missing type '{0}'", typeName);
                        PrintStack(LogType::Error);
                    }
                    return;
                }

                // Mock the scripting type for C# field that doesn't exist in engine script types database
                typeHandle = ScriptingTypeHandle(GetBinaryModuleFlaxEngine(), 0);
            }

            // Cache field data
            cache.Field = field;
            cache.Module = typeHandle.Module;
            ScriptingTypeFieldSignature signature;
            cache.Module->GetFieldSignature(field, signature);
            cache.IsStatic = signature.IsStatic;
        }

        // Evaluate object instance for non-static fields
        Variant instance;
        if (!cache.IsStatic)
        {
            // Evaluate object instance
            const auto box = node->GetBox(1);
            if (box->HasConnection())
            {
                instance = eatBox(node, box->FirstConnection());
            }
            else
            {
                // Unconnected instance box is treated as this script instance if type matches the member field class
                auto& stack = ThreadStacks.Get();
                instance.SetObject(stack.Stack->Instance);
            }
        }

        // Set field value
        value = tryGetValue(node->GetBox(0), 4, Value::Zero);
        if (cache.Module->SetFieldValue(cache.Field, instance, value))
        {
            PrintStack(LogType::Error);
            break;
        }

        // Call graph further
        const auto returnedImpulse = &node->Boxes[3];
        if (returnedImpulse && returnedImpulse->HasConnection())
            eatBox(node, returnedImpulse->FirstConnection());
        break;
    }
    default:
        break;
    }
}

void VisualScriptExecutor::ProcessGroupFlow(Box* boxBase, Node* node, Value& value)
{
    switch (node->TypeID)
    {
        // If
    case 1:
    {
        const bool condition = (bool)tryGetValue(node->GetBox(1), Value::Zero);
        boxBase = node->GetBox(condition ? 2 : 3);
        if (boxBase->HasConnection())
            eatBox(node, boxBase->FirstConnection());
        break;
    }
        // For Loop
    case 2:
    {
        const auto scope = ThreadStacks.Get().Stack->Scope;
        int32 iteratorIndex = 0;
        for (; iteratorIndex < scope->ReturnedValues.Count(); iteratorIndex++)
        {
            const auto& e = scope->ReturnedValues[iteratorIndex];
            if (e.NodeId == node->ID)
                break;
        }
        switch (boxBase->ID)
        {
            // Loop
        case 0:
        {
            if (iteratorIndex == scope->ReturnedValues.Count())
                scope->ReturnedValues.AddOne();
            auto& iteratorValue = scope->ReturnedValues[iteratorIndex];
            iteratorValue.NodeId = node->ID;
            iteratorValue.BoxId = 0;
            iteratorValue.Value = (int32)tryGetValue(node->GetBox(1), 0, Value::Zero);
            const int32 count = (int32)tryGetValue(node->GetBox(2), 1, Value::Zero);
            for (; iteratorValue.Value.AsInt < count; iteratorValue.Value.AsInt++)
            {
                boxBase = node->GetBox(4);
                if (boxBase->HasConnection())
                    eatBox(node, boxBase->FirstConnection());
            }
            boxBase = node->GetBox(6);
            if (boxBase->HasConnection())
                eatBox(node, boxBase->FirstConnection());
            break;
        }
            // Break
        case 3:
            // Reset loop iterator
            if (iteratorIndex != scope->ReturnedValues.Count())
                scope->ReturnedValues[iteratorIndex].Value.AsInt = MAX_int32 - 1;
            break;
            // Index
        case 5:
            if (iteratorIndex != scope->ReturnedValues.Count())
                value = scope->ReturnedValues[iteratorIndex].Value;
            break;
        }
        break;
    }
        // While Loop
    case 3:
    {
        const auto scope = ThreadStacks.Get().Stack->Scope;
        int32 iteratorIndex = 0;
        for (; iteratorIndex < scope->ReturnedValues.Count(); iteratorIndex++)
        {
            const auto& e = scope->ReturnedValues[iteratorIndex];
            if (e.NodeId == node->ID)
                break;
        }
        switch (boxBase->ID)
        {
            // Loop
        case 0:
        {
            if (iteratorIndex == scope->ReturnedValues.Count())
                scope->ReturnedValues.AddOne();
            auto& iteratorValue = scope->ReturnedValues[iteratorIndex];
            iteratorValue.NodeId = node->ID;
            iteratorValue.BoxId = 0;
            iteratorValue.Value = 0;
            for (; (bool)tryGetValue(node->GetBox(1), 1, Value::Zero) && iteratorValue.Value.AsInt != -1; iteratorValue.Value.AsInt++)
            {
                boxBase = node->GetBox(3);
                if (boxBase->HasConnection())
                    eatBox(node, boxBase->FirstConnection());
            }
            boxBase = node->GetBox(5);
            if (boxBase->HasConnection())
                eatBox(node, boxBase->FirstConnection());
            break;
        }
            // Break
        case 2:
            // Reset loop iterator
            if (iteratorIndex != scope->ReturnedValues.Count())
                scope->ReturnedValues[iteratorIndex].Value.AsInt = -1;
            break;
            // Index
        case 4:
            if (iteratorIndex != scope->ReturnedValues.Count())
                value = scope->ReturnedValues[iteratorIndex].Value;
            break;
        }
        break;
    }
    }
}

REGISTER_BINARY_ASSET(VisualScript, "FlaxEngine.VisualScript", nullptr, false);

VisualScript::VisualScript(const SpawnParams& params, const AssetInfo* info)
    : BinaryAsset(params, info)
{
}

Asset::LoadResult VisualScript::load()
{
    // Build Visual Script typename that is based on asset id
    String typeName = _id.ToString();
    StringUtils::ConvertUTF162ANSI(typeName.Get(), _typenameChars, 32);
    _typenameChars[32] = 0;
    _typename = StringAnsiView(_typenameChars, 32);

    // Load metadata
    const auto metadataChunk = GetChunk(1);
    if (metadataChunk == nullptr)
        return LoadResult::MissingDataChunk;
    MemoryReadStream metadataStream(metadataChunk->Get(), metadataChunk->Size());
    int32 version;
    metadataStream.ReadInt32(&version);
    switch (version)
    {
    case 1:
    {
        metadataStream.ReadString(&Meta.BaseTypename, 31);
        metadataStream.ReadInt32((int32*)&Meta.Flags);
        break;
    }
    default:
        LOG(Error, "Unknown Visual Script \'{1}\' metadata version {0}.", version, ToString());
        return LoadResult::InvalidData;
    }

    // Load graph
    const auto surfaceChunk = GetChunk(0);
    if (surfaceChunk == nullptr)
        return LoadResult::MissingDataChunk;
    MemoryReadStream surfaceStream(surfaceChunk->Get(), surfaceChunk->Size());
    if (Graph.Load(&surfaceStream, true))
    {
        LOG(Warning, "Failed to load graph \'{0}\'", ToString());
        return LoadResult::Failed;
    }

    // Find method nodes
    for (auto& node : Graph.Nodes)
    {
        switch (node.Type)
        {
        case GRAPH_NODE_MAKE_TYPE(16, 3):
        {
            auto& method = _methods.AddOne();
            method.Script = this;
            method.Node = &node;
            method.Name = StringAnsi((StringView)node.Values[0]);
            method.MethodFlags = (MethodFlags)((byte)MethodFlags::Virtual | (byte)MethodFlags::Override);
            method.Signature.Name = method.Name;
            method.Signature.IsStatic = false;
            method.Signature.Params.Resize(node.Values[1].AsInt);
            method.ParamNames.Resize(method.Signature.Params.Count());
            break;
        }
        case GRAPH_NODE_MAKE_TYPE(16, 6):
        {
            auto& method = _methods.AddOne();
            method.Script = this;
            method.Node = &node;
            method.Signature.IsStatic = false;
            auto& signatureData = node.Values[0];
            if (signatureData.Type.Type != VariantType::Blob || signatureData.AsBlob.Length == 0)
                break;
            MemoryReadStream signatureStream((byte*)signatureData.AsBlob.Data, signatureData.AsBlob.Length);
            switch (signatureStream.ReadByte())
            {
            case 1:
            {
                signatureStream.ReadStringAnsi(&method.Name, 71);
                method.MethodFlags = (MethodFlags)signatureStream.ReadByte();
                method.Signature.IsStatic = ((byte)method.MethodFlags & (byte)MethodFlags::Static) != 0;
                signatureStream.ReadVariantType(&method.Signature.ReturnType);
                int32 parametersCount;
                signatureStream.ReadInt32(&parametersCount);
                method.Signature.Params.Resize(parametersCount);
                method.ParamNames.Resize(parametersCount);
                for (int32 i = 0; i < parametersCount; i++)
                {
                    auto& param = method.Signature.Params[i];
                    signatureStream.ReadStringAnsi(&method.ParamNames[i], 13);
                    signatureStream.ReadVariantType(&param.Type);
                    param.IsOut = signatureStream.ReadByte() != 0;
                    bool hasDefaultValue = signatureStream.ReadByte() != 0;
                }
                break;
            }
            default:
                break;
            }
            method.Signature.Name = method.Name;
            break;
        }
        }
    }

    // Setup fields list
    _fields.Resize(Graph.Parameters.Count());
    for (int32 i = 0; i < Graph.Parameters.Count(); i++)
    {
        auto& parameter = Graph.Parameters[i];
        auto& field = _fields[i];
        field.Script = this;
        field.Parameter = &parameter;
        field.Index = i;
        field.Name.Set(parameter.Name.Get(), parameter.Name.Length());
    }

#if USE_EDITOR
    if (_instances.HasItems())
    {
        // Setup scripting type
        CacheScriptingType();

        // Create default instance object to with valid new vtable
        ScriptingObject* defaultInstance = _scriptingTypeHandle.GetType().GetDefaultInstance();

        // Reinitialize existing Visual Script instances in the Editor
        for (auto& e : _instances)
        {
            ScriptingObject* object = Scripting::TryFindObject<ScriptingObject>(e.Key);
            if (!object)
                continue;

            // Hack vtable similarly to VisualScriptObjectSpawn
            ScriptingType& visualScriptType = (ScriptingType&)object->GetType();
            if (visualScriptType.Class.ScriptVTable)
            {
                // Override object vtable with hacked one that has Visual Script functions calls
                ASSERT(visualScriptType.Class.VTable);
                *(void**)object = visualScriptType.Class.VTable;
            }
        }
        const int32 oldCount = _oldParamsLayout.Count();
        const int32 count = Graph.Parameters.Count();
        if (oldCount != 0 && count != 0)
        {
            // Update instanced data from previous format to the current graph parameters scheme
            for (auto& e : _instances)
            {
                auto& instanceParams = e.Value;
                Array<Variant> valuesCache(MoveTemp(instanceParams));
                instanceParams.Resize(count);
                for (int32 i = 0; i < count; i++)
                {
                    const int32 oldIndex = _oldParamsLayout.Find(Graph.Parameters[i].Identifier);
                    instanceParams[i] = oldIndex != -1 ? valuesCache[oldIndex] : Graph.Parameters[i].Value;
                }
            }
        }
        else
        {
            // Reset instances values to defaults
            for (auto& e : _instances)
            {
                auto& instanceParams = e.Value;
                instanceParams.Resize(count);
                for (int32 i = 0; i < count; i++)
                    instanceParams[i] = Graph.Parameters[i].Value;
            }
        }
    }
#endif

    return LoadResult::Ok;
}

void VisualScript::unload(bool isReloading)
{
#if USE_EDITOR
    if (isReloading)
    {
        // Cache existing instanced parameters IDs to restore values after asset reload (params order might be changed but the IDs are stable)
        _oldParamsLayout.Resize(Graph.Parameters.Count());
        for (int32 i = 0; i < Graph.Parameters.Count(); i++)
        {
            auto& param = Graph.Parameters[i];
            _oldParamsLayout[i] = param.Identifier;
        }
    }
    else
    {
        _oldParamsLayout.Clear();
    }
#else
    _instances.Clear();
#endif

    // Clear resources
    _methods.Clear();
    _fields.Clear();
    Graph.Clear();

    // Note: preserve the registered scripting type but invalidate the locally cached handle
    if (_scriptingTypeHandle)
    {
        auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex];
        if (type.Class.DefaultInstance)
        {
            Delete(type.Class.DefaultInstance);
            type.Class.DefaultInstance = nullptr;
        }
        VisualScriptingModule.TypeNameToTypeIndex.RemoveValue(_scriptingTypeHandle.TypeIndex);
        VisualScriptingModule.Scripts[_scriptingTypeHandle.TypeIndex] = nullptr;
        _scriptingTypeHandleCached = _scriptingTypeHandle;
        _scriptingTypeHandle = ScriptingTypeHandle();
    }
}

AssetChunksFlag VisualScript::getChunksToPreload() const
{
    return GET_CHUNK_FLAG(0) | GET_CHUNK_FLAG(1);
}

void VisualScript::CacheScriptingType()
{
    auto& binaryModule = VisualScriptingModule;

    // Find base type
    const StringAnsi baseTypename(Meta.BaseTypename);
    const ScriptingTypeHandle baseType = Scripting::FindScriptingType(baseTypename);
    if (baseType)
    {
        // Find first native base C++ class of this Visual Script class
        ScriptingTypeHandle nativeType = baseType;
        while (nativeType && nativeType.GetType().Class.ScriptVTable)
        {
            nativeType = nativeType.GetType().GetBaseType();
        }
        if (!nativeType)
        {
            LOG(Error, "Missing native base class for {0}", ToString());
            return;
        }

        // Create scripting type
        if (_scriptingTypeHandleCached)
        {
            // Reuse cached slot (already created objects with that type handle can use the new type info eg. after asset reload when editing script in editor)
            ASSERT(_scriptingTypeHandleCached.GetType().Fullname == _typename);
            _scriptingTypeHandle = _scriptingTypeHandleCached;
            _scriptingTypeHandleCached = ScriptingTypeHandle();
            auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex];
            type.~ScriptingType();
            new(&type)ScriptingType(_typename, &binaryModule, baseType.GetType().Size, ScriptingType::DefaultInitRuntime, VisualScriptingBinaryModule::VisualScriptObjectSpawn, baseType);
            binaryModule.Scripts[_scriptingTypeHandle.TypeIndex] = this;
        }
        else
        {
            // Allocate new slot
            const int32 typeIndex = binaryModule.Types.Count();
            binaryModule.Types.AddUninitialized();
            new(binaryModule.Types.Get() + binaryModule.Types.Count() - 1)ScriptingType(_typename, &binaryModule, baseType.GetType().Size, ScriptingType::DefaultInitRuntime, VisualScriptingBinaryModule::VisualScriptObjectSpawn, baseType);
            binaryModule.TypeNameToTypeIndex[_typename] = typeIndex;
            _scriptingTypeHandle = ScriptingTypeHandle(&binaryModule, typeIndex);
            binaryModule.Scripts.Add(this);

#if USE_EDITOR
            // When first Visual Script gets loaded register for other modules unload to clear runtime execution cache
            if (typeIndex == 0)
            {
                Scripting::ScriptsReloading.Bind<VisualScriptingBinaryModule, &VisualScriptingBinaryModule::OnScriptsReloading>(&binaryModule);
            }
#endif
        }
        auto& type = _scriptingTypeHandle.Module->Types[_scriptingTypeHandle.TypeIndex];
        type.ManagedClass = baseType.GetType().ManagedClass;

        // Create custom vtable for this class (build out of the wrapper C++ methods that call Visual Script graph)
        // Call setup for all class starting from the first native type (first that uses virtual calls will allocate table of a proper size, further base types will just add own methods)
        for (ScriptingTypeHandle e = nativeType; e;)
        {
            const ScriptingType& eType = e.GetType();
            if (eType.Class.SetupScriptVTable)
            {
                ASSERT(eType.ManagedClass);
                eType.Class.SetupScriptVTable(eType.ManagedClass, type.Class.ScriptVTable, type.Class.ScriptVTableBase);
            }
            e = eType.GetBaseType();
        }
        MMethod** scriptVTable = (MMethod**)type.Class.ScriptVTable;
        while (scriptVTable && *scriptVTable)
        {
            const MMethod* referenceMethod = *scriptVTable;

            // Find that method overriden in Visual Script (the current or one of the base classes in Visual Script)
            auto node = FindMethod(referenceMethod->GetName(), referenceMethod->GetParametersCount());
            if (node == nullptr)
            {
                // Check base classes that are Visual Script
                auto e = baseType;
                while (e.Module == &binaryModule && node == nullptr)
                {
                    auto& eType = e.GetType();
                    Guid id;
                    if (!Guid::Parse(eType.Fullname, id))
                    {
                        if (const auto visualScript = Content::LoadAsync<VisualScript>(id))
                        {
                            node = visualScript->FindMethod(referenceMethod->GetName(), referenceMethod->GetParametersCount());
                        }
                    }
                    e = e.GetType().GetBaseType();
                }
            }

            // Set the method to call (null entry marks unused entries that won't use Visual Script wrapper calls)
            *scriptVTable = (MMethod*)node;

            // Move to the next entry (table is null terminated)
            scriptVTable++;
        }
    }
    else if (Meta.BaseTypename.HasChars())
    {
        LOG(Error, "Failed to find a scripting type \'{0}\' that is a base type for {1}", Meta.BaseTypename, ToString());
    }
    else
    {
        LOG(Error, "Cannot use {0} as script because base typename is missing.", ToString());
    }
}

VisualScriptingBinaryModule::VisualScriptingBinaryModule()
    : _name("Visual Scripting")
{
}

ScriptingObject* VisualScriptingBinaryModule::VisualScriptObjectSpawn(const ScriptingObjectSpawnParams& params)
{
    // Create native object (base type can be C++ or C#)
    ScriptingType& visualScriptType = (ScriptingType&)params.Type.GetType();
    ScriptingTypeHandle baseTypeHandle = visualScriptType.GetBaseType();
    const ScriptingType* baseTypePtr = &baseTypeHandle.GetType();
    while (baseTypePtr->Class.Spawn == &VisualScriptObjectSpawn)
    {
        baseTypeHandle = baseTypePtr->GetBaseType();
        baseTypePtr = &baseTypeHandle.GetType();
    }
    ScriptingObject* object = baseTypePtr->Class.Spawn(params);
    if (!object)
    {
        return nullptr;
    }

    // Beware! Hacking vtables incoming! Undefined behaviors exploits! Low-level programming!
    // What's happening here?
    // We create a custom vtable for the Visual Script objects that use a native class object with virtual functions overrides.
    // To make it easy to use in C++ we inject custom wrapper methods into C++ object vtable to execute Visual Script graph from them.
    // Because virtual member functions calls are C++ ABI and impl-defined this is quite hard. But works.
    if (visualScriptType.Class.ScriptVTable)
    {
        if (!visualScriptType.Class.VTable)
        {
            // Duplicate vtable
            void** vtable = *(void***)object;
            const int32 prefixSize = GetVTablePrefix();
            int32 entriesCount = 0;
            while (vtable[entriesCount] && entriesCount < 200)
                entriesCount++;
            const int32 size = entriesCount * sizeof(void*);
            visualScriptType.Class.VTable = (void**)((byte*)Platform::Allocate(prefixSize + size, 16) + prefixSize);
            Platform::MemoryCopy((byte*)visualScriptType.Class.VTable - prefixSize, (byte*)vtable - prefixSize, prefixSize + size);

            // Override vtable entries by the class
            for (ScriptingTypeHandle e = baseTypeHandle; e;)
            {
                const ScriptingType& eType = e.GetType();
                if (eType.Class.SetupScriptObjectVTable)
                    eType.Class.SetupScriptObjectVTable(visualScriptType.Class.ScriptVTable, visualScriptType.Class.ScriptVTableBase, visualScriptType.Class.VTable, entriesCount, 1);
                e = eType.GetBaseType();
            }
        }

        // Override object vtable with hacked one that has Visual Script functions calls
        *(void**)object = visualScriptType.Class.VTable;
    }

    // Mark as custom scripting type
    object->Flags |= ObjectFlags::IsCustomScriptingType;

    // Get Visual Script asset
    ASSERT(&VisualScriptingModule == params.Type.Module);
    VisualScript* visualScript = VisualScriptingModule.Scripts[params.Type.TypeIndex];

    // Initialize instance data
    auto& instanceParams = visualScript->_instances[object->GetID()];
    instanceParams.Resize(visualScript->Graph.Parameters.Count());
    for (int32 i = 0; i < instanceParams.Count(); i++)
        instanceParams[i] = visualScript->Graph.Parameters[i].Value;

    return object;
}

#if USE_EDITOR

void VisualScriptingBinaryModule::OnScriptsReloading()
{
    // Clear any cached types from that module across all loaded Visual Scripts
    for (auto& script : Scripts)
    {
        if (!script || !script->IsLoaded())
            continue;
        ScopeLock lock(script->Locker);

        // Clear cached types (underlying base class could be in reloaded C# scripts)
        if (script->_scriptingTypeHandle)
        {
            auto& type = VisualScriptingModule.Types[script->_scriptingTypeHandle.TypeIndex];
            if (type.Class.DefaultInstance)
            {
                Delete(type.Class.DefaultInstance);
                type.Class.DefaultInstance = nullptr;
            }
            VisualScriptingModule.TypeNameToTypeIndex.RemoveValue(script->_scriptingTypeHandle.TypeIndex);
            script->_scriptingTypeHandleCached = script->_scriptingTypeHandle;
            script->_scriptingTypeHandle = ScriptingTypeHandle();
        }

        // Clear methods cache
        for (auto& node : script->Graph.Nodes)
        {
            switch (node.Type)
            {
                // Invoke Method
            case GRAPH_NODE_MAKE_TYPE(16, 4):
            {
                node.Data.InvokeMethod.Method = nullptr;
                break;
            }
                // Get/Set Field
            case GRAPH_NODE_MAKE_TYPE(16, 7):
            case GRAPH_NODE_MAKE_TYPE(16, 8):
            {
                node.Data.GetSetField.Field = nullptr;
                break;
            }
            }
        }
    }
}

#endif

const StringAnsi& VisualScriptingBinaryModule::GetName() const
{
    return _name;
}

bool VisualScriptingBinaryModule::IsLoaded() const
{
    return true;
}

bool VisualScriptingBinaryModule::FindScriptingType(const StringAnsiView& typeName, int32& typeIndex)
{
    // Type Name for Visual Scripts is 32 chars Guid representation of asset ID
    if (typeName.Length() == 32)
    {
        if (TypeNameToTypeIndex.TryGet(typeName, typeIndex))
            return true;
        Guid id;
        if (!Guid::Parse(typeName, id))
        {
            const auto visualScript = Content::LoadAsync<VisualScript>(id);
            if (visualScript)
            {
                const auto handle = visualScript->GetScriptingType();
                if (handle)
                {
                    typeIndex = handle.TypeIndex;
                    return true;
                }
            }
        }
    }
    return false;
}

void* VisualScriptingBinaryModule::FindMethod(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name, int32 numParams)
{
    return (void*)Scripts[typeHandle.TypeIndex]->FindMethod(name, numParams);
}

bool VisualScriptingBinaryModule::InvokeMethod(void* method, const Variant& instance, Span<Variant> paramValues, Variant& result)
{
    auto vsMethod = (VisualScript::Method*)method;
    ScriptingObject* instanceObject = nullptr;
    if (!vsMethod->Signature.IsStatic)
    {
        instanceObject = (ScriptingObject*)instance;
        if (!instanceObject || instanceObject->GetTypeHandle() != vsMethod->Script->GetScriptingType())
        {
            if (!instanceObject)
                LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) without object instance", String(vsMethod->Script->GetScriptTypeName()), String(vsMethod->Name), vsMethod->ParamNames.Count());
            else
                LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) with invalid object instance of type '{3}'", String(vsMethod->Script->GetScriptTypeName()), String(vsMethod->Name), vsMethod->ParamNames.Count(), String(instanceObject->GetType().Fullname));
            return true;
        }
    }
    result = VisualScripting::Invoke(vsMethod, instanceObject, paramValues);
    return false;
}

void VisualScriptingBinaryModule::GetMethodSignature(void* method, ScriptingTypeMethodSignature& methodSignature)
{
    const auto vsMethod = (const VisualScript::Method*)method;
    methodSignature = vsMethod->Signature;
}

void* VisualScriptingBinaryModule::FindField(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name)
{
    return (void*)Scripts[typeHandle.TypeIndex]->FindField(name);
}

void VisualScriptingBinaryModule::GetFieldSignature(void* field, ScriptingTypeFieldSignature& fieldSignature)
{
    const auto vsFiled = (VisualScript::Field*)field;
    fieldSignature.Name = vsFiled->Name;
    fieldSignature.ValueType = vsFiled->Parameter->Type;
    fieldSignature.IsStatic = false;
}

bool VisualScriptingBinaryModule::GetFieldValue(void* field, const Variant& instance, Variant& result)
{
    const auto vsFiled = (VisualScript::Field*)field;
    const auto instanceObject = (ScriptingObject*)instance;
    if (!instanceObject)
    {
        LOG(Error, "Failed to get field '{0}' without object instance", vsFiled->Parameter->Name);
        return true;
    }
    const auto instanceParams = vsFiled->Script->_instances.Find(instanceObject->GetID());
    if (!instanceParams)
    {
        LOG(Error, "Missing parameters for the object instance.");
        return true;
    }
    result = instanceParams->Value[vsFiled->Index];
    return false;
}

bool VisualScriptingBinaryModule::SetFieldValue(void* field, const Variant& instance, Variant& value)
{
    const auto vsFiled = (VisualScript::Field*)field;
    const auto instanceObject = (ScriptingObject*)instance;
    if (!instanceObject)
    {
        LOG(Error, "Failed to set field '{0}' without object instance", vsFiled->Parameter->Name);
        return true;
    }
    const auto instanceParams = vsFiled->Script->_instances.Find(instanceObject->GetID());
    if (!instanceParams)
    {
        LOG(Error, "Missing parameters for the object instance.");
        return true;
    }
    instanceParams->Value[vsFiled->Index] = value;
    return false;
}

void VisualScriptingBinaryModule::SerializeObject(JsonWriter& stream, ScriptingObject* object, const ScriptingObject* otherObj)
{
    char idName[33];
    stream.StartObject();
    const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
    if (asset)
    {
        const auto instanceParams = asset->_instances.Find(object->GetID());
        if (instanceParams)
        {
            auto& params = instanceParams->Value;
            if (otherObj)
            {
                // Serialize parameters diff
                const auto otherParams = asset->_instances.Find(otherObj->GetID());
                if (otherParams)
                {
                    for (int32 paramIndex = 0; paramIndex < params.Count(); paramIndex++)
                    {
                        auto& param = asset->Graph.Parameters[paramIndex];
                        auto& value = params[paramIndex];
                        auto& otherValue = otherParams->Value[paramIndex];
                        if (value != otherValue)
                        {
                            param.Identifier.ToString(idName, Guid::FormatType::N);
                            stream.Key(idName, 32);
                            Serialization::Serialize(stream, params[paramIndex], &otherValue);
                        }
                    }
                }
                else
                {
                    for (int32 paramIndex = 0; paramIndex < params.Count(); paramIndex++)
                    {
                        auto& param = asset->Graph.Parameters[paramIndex];
                        auto& value = params[paramIndex];
                        auto& otherValue = param.Value;
                        if (value != otherValue)
                        {
                            param.Identifier.ToString(idName, Guid::FormatType::N);
                            stream.Key(idName, 32);
                            Serialization::Serialize(stream, params[paramIndex], &otherValue);
                        }
                    }
                }
            }
            else
            {
                // Serialize all parameters
                for (int32 paramIndex = 0; paramIndex < params.Count(); paramIndex++)
                {
                    auto& param = asset->Graph.Parameters[paramIndex];
                    auto& value = params[paramIndex];
                    param.Identifier.ToString(idName, Guid::FormatType::N);
                    stream.Key(idName, 32);
                    Serialization::Serialize(stream, value, nullptr);
                }
            }
        }
    }
    stream.EndObject();
}

void VisualScriptingBinaryModule::DeserializeObject(ISerializable::DeserializeStream& stream, ScriptingObject* object, ISerializeModifier* modifier)
{
    ASSERT(stream.IsObject());
    const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
    if (asset)
    {
        const auto instanceParams = asset->_instances.Find(object->GetID());
        if (instanceParams)
        {
            // Deserialize all parameters
            auto& params = instanceParams->Value;
            for (auto i = stream.MemberBegin(); i != stream.MemberEnd(); ++i)
            {
                StringAnsiView idNameAnsi(i->name.GetString(), i->name.GetStringLength());
                Guid paramId;
                if (!Guid::Parse(idNameAnsi, paramId))
                {
                    int32 paramIndex;
                    if (asset->Graph.GetParameter(paramId, paramIndex))
                    {
                        Serialization::Deserialize(i->value, params[paramIndex], modifier);
                    }
                }
            }
        }
    }
}

void VisualScriptingBinaryModule::OnObjectIdChanged(ScriptingObject* object, const Guid& oldId)
{
    const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
    if (asset)
    {
        auto& instanceParams = asset->_instances[object->GetID()];
        auto oldParams = asset->_instances.Find(oldId);
        if (oldParams)
        {
            instanceParams = MoveTemp(oldParams->Value);
            asset->_instances.Remove(oldParams);
        }
    }
}

void VisualScriptingBinaryModule::OnObjectDeleted(ScriptingObject* object)
{
    const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
    if (asset)
    {
        // Cleanup object data
        asset->_instances.Remove(object->GetID());
    }
}

void VisualScriptingBinaryModule::Destroy(bool isReloading)
{
    // Skip module unregister during reloads (Visual Scripts are persistent)
    if (isReloading)
        return;

    BinaryModule::Destroy(isReloading);
}

ScriptingTypeHandle VisualScript::GetScriptingType()
{
    if (!_scriptingTypeHandle && !WaitForLoaded())
    {
        CacheScriptingType();
    }
    return _scriptingTypeHandle;
}

ScriptingObject* VisualScript::CreateInstance()
{
    const auto scriptingTypeHandle = GetScriptingType();
    return scriptingTypeHandle ? scriptingTypeHandle.GetType().Class.Spawn(ScriptingObjectSpawnParams(Guid::New(), scriptingTypeHandle)) : nullptr;
}

Variant VisualScript::GetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance) const
{
    CHECK_RETURN(instance, Variant());
    for (int32 paramIndex = 0; paramIndex < Graph.Parameters.Count(); paramIndex++)
    {
        if (Graph.Parameters[paramIndex].Name == name)
        {
            const auto instanceParams = _instances.Find(instance->GetID());
            if (instanceParams)
                return instanceParams->Value[paramIndex];
            LOG(Error, "Failed to access Visual Script parameter {1} for {0}.", instance->ToString(), name);
            return Graph.Parameters[paramIndex].Value;
        }
    }
    LOG(Warning, "Failed to get {0} parameter '{1}'", ToString(), name);
    return Variant();
}

void VisualScript::SetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance, const Variant& value) const
{
    CHECK(instance);
    for (int32 paramIndex = 0; paramIndex < Graph.Parameters.Count(); paramIndex++)
    {
        if (Graph.Parameters[paramIndex].Name == name)
        {
            const auto instanceParams = _instances.Find(instance->GetID());
            if (instanceParams)
            {
                instanceParams->Value[paramIndex] = value;
                return;
            }
            LOG(Error, "Failed to access Visual Script parameter {1} for {0}.", instance->ToString(), name);
            return;
        }
    }
    LOG(Warning, "Failed to set {0} parameter '{1}'", ToString(), name);
}

void VisualScript::SetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance, Variant&& value) const
{
    CHECK(instance);
    for (int32 paramIndex = 0; paramIndex < Graph.Parameters.Count(); paramIndex++)
    {
        if (Graph.Parameters[paramIndex].Name == name)
        {
            const auto instanceParams = _instances.Find(instance->GetID());
            if (instanceParams)
            {
                instanceParams->Value[paramIndex] = MoveTemp(value);
                return;
            }
        }
    }
    LOG(Warning, "Failed to set {0} parameter '{1}'", ToString(), name);
}

const VisualScript::Method* VisualScript::FindMethod(const StringAnsiView& name, int32 numParams) const
{
    for (const auto& e : _methods)
    {
        if (e.Signature.Params.Count() == numParams && e.Name == name)
            return &e;
    }
    return nullptr;
}

const VisualScript::Field* VisualScript::FindField(const StringAnsiView& name) const
{
    for (const auto& e : _fields)
    {
        if (e.Name == name)
            return &e;
    }
    return nullptr;
}

BytesContainer VisualScript::LoadSurface()
{
    ScopeLock lock(Locker);
    if (!LoadChunks(GET_CHUNK_FLAG(0)))
    {
        const auto data = GetChunk(0);
        BytesContainer result;
        result.Copy(data->Data);
        return result;
    }

    LOG(Warning, "\'{0}\' surface data is missing.", GetPath());
    return BytesContainer();
}

#if USE_EDITOR

bool VisualScript::SaveSurface(BytesContainer& data, const Metadata& meta)
{
    // Wait for asset to be loaded or don't if last load failed
    if (LastLoadFailed())
    {
        LOG(Warning, "Saving asset that failed to load.");
    }
    else if (WaitForLoaded())
    {
        LOG(Error, "Asset loading failed. Cannot save it.");
        return true;
    }

    ScopeLock lock(Locker);

    // Release all chunks
    for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
        ReleaseChunk(i);

    // Set Visject Surface data
    auto visjectSurfaceChunk = GetOrCreateChunk(0);
    visjectSurfaceChunk->Data.Copy(data);

    // Set metadata
    auto metadataChunk = GetOrCreateChunk(1);
    MemoryWriteStream metaStream(512);
    {
        metaStream.WriteInt32(1);
        metaStream.WriteString(meta.BaseTypename, 31);
        metaStream.WriteInt32((int32)meta.Flags);
    }
    metadataChunk->Data.Copy(metaStream.GetHandle(), metaStream.GetPosition());

    // Save
    AssetInitData assetData;
    assetData.SerializedVersion = 1;
    if (SaveAsset(assetData))
    {
        LOG(Error, "Cannot save \'{0}\'", ToString());
        return true;
    }

    return false;
}

void VisualScript::GetMethodSignature(int32 index, String& name, byte& flags, String& returnTypeName, Array<String>& paramNames, Array<String>& paramTypeNames, Array<bool>& paramOuts)
{
    auto& method = _methods[index];
    name = String(method.Name);
    flags = (byte)method.MethodFlags;
    returnTypeName = method.Signature.ReturnType.GetTypeName();
    paramNames.Resize(method.Signature.Params.Count());
    paramTypeNames.Resize(method.Signature.Params.Count());
    paramOuts.Resize(method.Signature.Params.Count());
    for (int32 i = 0; i < method.Signature.Params.Count(); i++)
    {
        auto& param = method.Signature.Params[i];
        paramNames[i] = String(method.ParamNames[i]);
        paramTypeNames[i] = param.Type.GetTypeName();
        paramOuts[i] = param.IsOut;
    }
}

Span<byte> VisualScript::GetMetaData(int32 typeID)
{
    auto meta = Graph.Meta.GetEntry(typeID);
    return meta ? ToSpan(meta->Data.Get(), meta->Data.Count()) : Span<byte>(nullptr, 0);
}

Span<byte> VisualScript::GetMethodMetaData(int32 index, int32 typeID)
{
    auto& method = _methods[index];
    auto meta = method.Node->Meta.GetEntry(typeID);
    return meta ? ToSpan(meta->Data.Get(), meta->Data.Count()) : Span<byte>(nullptr, 0);
}

#endif

VisualScripting::StackFrame* VisualScripting::GetThreadStackTop()
{
    return ThreadStacks.Get().Stack;
}

String VisualScripting::GetStackTrace()
{
    String result;
    auto frame = ThreadStacks.Get().Stack;
    while (frame)
    {
        String node;
        switch (frame->Node->Type)
        {
            // Get/Set Parameter
        case GRAPH_NODE_MAKE_TYPE(6, 3):
        case GRAPH_NODE_MAKE_TYPE(6, 4):
        {
            const auto param = frame->Script->Graph.GetParameter((Guid)frame->Node->Values[0]);
            node = frame->Node->TypeID == 3 ? TEXT("Get ") : TEXT("Set ");
            node += param ? param->Name : ((Guid)frame->Node->Values[0]).ToString();
            break;
        }
            // Method Override
        case GRAPH_NODE_MAKE_TYPE(16, 3):
            node = (StringView)frame->Node->Values[0];
            node += TEXT("()");
            break;
            // Invoke Method
        case GRAPH_NODE_MAKE_TYPE(16, 4):
            node = (StringView)frame->Node->Values[0];
            node += TEXT(".");
            node += (StringView)frame->Node->Values[1];
            node += TEXT("()");
            break;
            // Function
        case GRAPH_NODE_MAKE_TYPE(16, 6):
            node = String(frame->Script->GetScriptTypeName());
            for (int32 i = 0; i < frame->Script->_methods.Count(); i++)
            {
                auto& method = frame->Script->_methods[i];
                if (method.Node == frame->Node)
                {
                    node += TEXT(".");
                    node += String(method.Name);
                    node += TEXT("()");
                    break;
                }
            }
            break;
        default:
            node = StringUtils::ToString(frame->Node->Type);
            break;
        }
        result += String::Format(TEXT("    at {0}:{1} in node {2}\n"), StringUtils::GetFileNameWithoutExtension(frame->Script->GetPath()), frame->Script->GetID(), node);
        frame = frame->PreviousFrame;
    }
    return result;
}

VisualScriptingBinaryModule* VisualScripting::GetBinaryModule()
{
    return &VisualScriptingModule;
}

Variant VisualScripting::Invoke(VisualScript::Method* method, ScriptingObject* instance, Span<Variant> parameters)
{
    CHECK_RETURN(method && method->Script->IsLoaded(), Variant::Zero);

    // Add to the calling stack
    ScopeContext scope;
    scope.Parameters = parameters;
    auto& stack = ThreadStacks.Get();
    StackFrame frame;
    frame.Script = method->Script;
    frame.Node = method->Node;
    frame.Box = method->Node->GetBox(0);
    frame.Instance = instance;
    frame.PreviousFrame = stack.Stack;
    frame.Scope = &scope;
    stack.Stack = &frame;
    stack.StackFramesCount++;

    // Call per group custom processing event
    const auto func = VisualScriptingExecutor._perGroupProcessCall[method->Node->GroupID];
    (VisualScriptingExecutor.*func)(frame.Box, method->Node, scope.FunctionReturn);

    // Remove from the calling stack
    stack.StackFramesCount--;
    stack.Stack = frame.PreviousFrame;

    return scope.FunctionReturn;
}

#if VISUAL_SCRIPT_DEBUGGING

bool VisualScripting::Evaluate(VisualScript* script, ScriptingObject* instance, uint32 nodeId, uint32 boxId, Variant& result)
{
    if (!script)
        return false;
    const auto node = script->Graph.GetNode(nodeId);
    if (!node)
        return false;
    const auto box = node->GetBox(boxId);
    if (!box)
        return false;

    // Add to the calling stack
    ScopeContext scope;
    auto& stack = ThreadStacks.Get();
    StackFrame frame;
    frame.Script = script;
    frame.Node = node;
    frame.Box = box;
    frame.Instance = instance;
    frame.PreviousFrame = stack.Stack;
    frame.Scope = &scope;
    stack.Stack = &frame;
    stack.StackFramesCount++;

    // Call per group custom processing event
    const auto func = VisualScriptingExecutor._perGroupProcessCall[node->GroupID];
    (VisualScriptingExecutor.*func)(box, node, result);

    // Remove from the calling stack
    stack.StackFramesCount--;
    stack.Stack = frame.PreviousFrame;

    return true;
}

#endif
