/*****************************************************************************
 * Copyright (c) 2014-2025 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#ifdef ENABLE_SCRIPTING

    #include "ScriptEngine.h"

    #include "../PlatformEnvironment.h"
    #include "../actions/BannerPlaceAction.h"
    #include "../actions/CustomAction.h"
    #include "../actions/GameAction.h"
    #include "../actions/LargeSceneryPlaceAction.h"
    #include "../actions/RideCreateAction.h"
    #include "../actions/StaffHireNewAction.h"
    #include "../actions/WallPlaceAction.h"
    #include "../config/Config.h"
    #include "../core/EnumMap.hpp"
    #include "../core/File.h"
    #include "../core/FileScanner.h"
    #include "../core/Path.hpp"
    #include "../interface/InteractiveConsole.h"
    #include "../platform/Platform.h"
    #include "Duktape.hpp"
    #include "bindings/entity/ScEntity.hpp"
    #include "bindings/entity/ScGuest.hpp"
    #include "bindings/entity/ScLitter.hpp"
    #include "bindings/entity/ScParticle.hpp"
    #include "bindings/entity/ScPeep.hpp"
    #include "bindings/entity/ScStaff.hpp"
    #include "bindings/entity/ScVehicle.hpp"
    #include "bindings/game/ScCheats.hpp"
    #include "bindings/game/ScConsole.hpp"
    #include "bindings/game/ScContext.hpp"
    #include "bindings/game/ScDisposable.hpp"
    #include "bindings/game/ScPlugin.hpp"
    #include "bindings/game/ScProfiler.hpp"
    #include "bindings/network/ScNetwork.hpp"
    #include "bindings/network/ScPlayer.hpp"
    #include "bindings/network/ScPlayerGroup.hpp"
    #include "bindings/network/ScSocket.hpp"
    #include "bindings/object/ScInstalledObject.hpp"
    #include "bindings/object/ScObject.hpp"
    #include "bindings/object/ScObjectManager.h"
    #include "bindings/ride/ScRide.hpp"
    #include "bindings/ride/ScRideStation.hpp"
    #include "bindings/world/ScClimate.hpp"
    #include "bindings/world/ScDate.hpp"
    #include "bindings/world/ScMap.hpp"
    #include "bindings/world/ScPark.hpp"
    #include "bindings/world/ScParkMessage.hpp"
    #include "bindings/world/ScResearch.hpp"
    #include "bindings/world/ScScenario.hpp"
    #include "bindings/world/ScTile.hpp"
    #include "bindings/world/ScTileElement.hpp"

    #include <cassert>
    #include <iostream>
    #include <memory>
    #include <stdexcept>
    #include <string>

using namespace OpenRCT2;
using namespace OpenRCT2::Scripting;

struct ExpressionStringifier final
{
private:
    std::stringstream _ss;
    duk_context* _context{};
    int32_t _indent{};

    ExpressionStringifier(duk_context* ctx)
        : _context(ctx)
    {
    }

    void PushIndent(int32_t c = 1)
    {
        _indent += c;
    }

    void PopIndent(int32_t c = 1)
    {
        _indent -= c;
    }

    void LineFeed()
    {
        _ss << "\n" << std::string(_indent, ' ');
    }

    void Stringify(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel)
    {
        if (nestLevel >= 8)
        {
            _ss << "[...]";
            return;
        }

        switch (val.type())
        {
            case DukValue::Type::UNDEFINED:
                _ss << "undefined";
                break;
            case DukValue::Type::NULLREF:
                _ss << "null";
                break;
            case DukValue::Type::BOOLEAN:
                StringifyBoolean(val);
                break;
            case DukValue::Type::NUMBER:
                StringifyNumber(val);
                break;
            case DukValue::Type::STRING:
                _ss << "'" << val.as_string() << "'";
                break;
            case DukValue::Type::OBJECT:
                if (val.is_function())
                {
                    StringifyFunction(val);
                }
                else if (val.is_array())
                {
                    StringifyArray(val, canStartWithNewLine, nestLevel);
                }
                else
                {
                    StringifyObject(val, canStartWithNewLine, nestLevel);
                }
                break;
            case DukValue::Type::BUFFER:
                _ss << "[Buffer]";
                break;
            case DukValue::Type::POINTER:
                _ss << "[Pointer]";
                break;
            case DukValue::Type::LIGHTFUNC:
                _ss << "[LightFunc]";
                break;
        }
    }

    void StringifyArray(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel)
    {
        constexpr auto maxItemsToShow = 4;

        val.push();
        auto arrayLen = duk_get_length(_context, -1);
        if (arrayLen == 0)
        {
            _ss << "[]";
        }
        else if (arrayLen == 1)
        {
            _ss << "[ ";
            for (duk_uarridx_t i = 0; i < arrayLen; i++)
            {
                if (duk_get_prop_index(_context, -1, i))
                {
                    if (i != 0)
                    {
                        _ss << ", ";
                    }
                    Stringify(DukValue::take_from_stack(_context), false, nestLevel + 1);
                }
            }
            _ss << " ]";
        }
        else
        {
            if (canStartWithNewLine)
            {
                PushIndent();
                LineFeed();
            }
            _ss << "[ ";
            PushIndent(2);
            for (duk_uarridx_t i = 0; i < arrayLen; i++)
            {
                if (i != 0)
                {
                    _ss << ",";
                    LineFeed();
                }
                if (i >= maxItemsToShow)
                {
                    auto remainingItemsNotShown = arrayLen - maxItemsToShow;
                    if (remainingItemsNotShown == 1)
                    {
                        _ss << "... 1 more item";
                    }
                    else
                    {
                        _ss << "... " << std::to_string(remainingItemsNotShown) << " more items";
                    }
                    break;
                }

                if (duk_get_prop_index(_context, -1, i))
                {
                    Stringify(DukValue::take_from_stack(_context), false, nestLevel + 1);
                }
            }
            _ss << " ]";
            PopIndent(2);
            if (canStartWithNewLine)
            {
                PopIndent();
            }
        }
        duk_pop(_context);
    }

    void StringifyObject(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel)
    {
        auto numEnumerables = GetNumEnumerablesOnObject(val);
        if (numEnumerables == 0)
        {
            _ss << "{}";
        }
        else if (numEnumerables == 1)
        {
            _ss << "{ ";

            val.push();
            duk_enum(_context, -1, 0);
            auto index = 0;
            while (duk_next(_context, -1, 1))
            {
                if (index != 0)
                {
                    _ss << ", ";
                }
                auto value = DukValue::take_from_stack(_context, -1);
                auto key = DukValue::take_from_stack(_context, -1);
                if (key.type() == DukValue::Type::STRING)
                {
                    _ss << key.as_string() << ": ";
                }
                else
                {
                    // For some reason the key was not a string
                    _ss << "?: ";
                }
                Stringify(value, true, nestLevel + 1);
                index++;
            }
            duk_pop_2(_context);

            _ss << " }";
        }
        else
        {
            if (canStartWithNewLine)
            {
                PushIndent();
                LineFeed();
            }

            _ss << "{ ";
            PushIndent(2);

            val.push();
            duk_enum(_context, -1, 0);
            auto index = 0;
            while (duk_next(_context, -1, 1))
            {
                if (index != 0)
                {
                    _ss << ",";
                    LineFeed();
                }
                auto value = DukValue::take_from_stack(_context, -1);
                auto key = DukValue::take_from_stack(_context, -1);
                if (key.type() == DukValue::Type::STRING)
                {
                    _ss << key.as_string() << ": ";
                }
                else
                {
                    // For some reason the key was not a string
                    _ss << "?: ";
                }
                Stringify(value, true, nestLevel + 1);
                index++;
            }
            duk_pop_2(_context);

            PopIndent(2);
            _ss << " }";

            if (canStartWithNewLine)
            {
                PopIndent();
            }
        }
    }

    void StringifyFunction(const DukValue& val)
    {
        val.push();
        if (duk_is_c_function(_context, -1))
        {
            _ss << "[Native Function]";
        }
        else if (duk_is_ecmascript_function(_context, -1))
        {
            _ss << "[ECMAScript Function]";
        }
        else
        {
            _ss << "[Function]";
        }
        duk_pop(_context);
    }

    void StringifyBoolean(const DukValue& val)
    {
        _ss << (val.as_bool() ? "true" : "false");
    }

    void StringifyNumber(const DukValue& val)
    {
        const auto d = val.as_double();
        const duk_int_t i = val.as_int();
        if (AlmostEqual<double>(d, i))
        {
            _ss << std::to_string(i);
        }
        else
        {
            _ss << std::to_string(d);
        }
    }

    size_t GetNumEnumerablesOnObject(const DukValue& val)
    {
        size_t count = 0;
        val.push();
        duk_enum(_context, -1, 0);
        while (duk_next(_context, -1, 0))
        {
            count++;
            duk_pop(_context);
        }
        duk_pop_2(_context);
        return count;
    }

    // Taken from http://en.cppreference.com/w/cpp/types/numeric_limits/epsilon
    template<class T>
    static typename std::enable_if<!std::numeric_limits<T>::is_integer, bool>::type AlmostEqual(T x, T y, int32_t ulp = 20)
    {
        // the machine epsilon has to be scaled to the magnitude of the values used
        // and multiplied by the desired precision in ULPs (units in the last place)
        return std::abs(x - y) <= std::numeric_limits<T>::epsilon() * std::abs(x + y) * ulp
            // unless the result is subnormal
            || std::abs(x - y)
            < (std::numeric_limits<T>::min)(); // TODO: Remove parentheses around min once the macro is removed
    }

public:
    static std::string StringifyExpression(const DukValue& val)
    {
        ExpressionStringifier instance(val.context());
        instance.Stringify(val, false, 0);
        return instance._ss.str();
    }
};

DukContext::DukContext()
{
    _context = duk_create_heap_default();
    if (_context == nullptr)
    {
        throw std::runtime_error("Unable to initialise duktape context.");
    }
}

DukContext::~DukContext()
{
    duk_destroy_heap(_context);
}

ScriptEngine::ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env)
    : _console(console)
    , _env(env)
    , _hookEngine(*this)
{
}

void ScriptEngine::Initialise()
{
    if (_initialised)
        throw std::runtime_error("Script engine already initialised.");

    auto ctx = static_cast<duk_context*>(_context);
    ScCheats::Register(ctx);
    ScClimate::Register(ctx);
    ScWeatherState::Register(ctx);
    ScConfiguration::Register(ctx);
    ScConsole::Register(ctx);
    ScContext::Register(ctx);
    ScDate::Register(ctx);
    ScDisposable::Register(ctx);
    ScMap::Register(ctx);
    ScNetwork::Register(ctx);
    ScObjectManager::Register(ctx);
    ScInstalledObject::Register(ctx);
    ScObject::Register(ctx);
    ScSceneryObject::Register(ctx);
    ScSmallSceneryObject::Register(ctx);
    ScLargeSceneryObject::Register(ctx);
    ScLargeSceneryObjectTile::Register(ctx);
    ScWallObject::Register(ctx);
    ScFootpathAdditionObject::Register(ctx);
    ScBannerObject::Register(ctx);
    ScSceneryGroupObject::Register(ctx);
    ScPark::Register(ctx);
    ScParkMessage::Register(ctx);
    ScPlayer::Register(ctx);
    ScPlayerGroup::Register(ctx);
    ScProfiler::Register(ctx);
    ScResearch::Register(ctx);
    ScRide::Register(ctx);
    ScRideStation::Register(ctx);
    ScRideObject::Register(ctx);
    ScRideObjectVehicle::Register(ctx);
    ScTile::Register(ctx);
    ScTileElement::Register(ctx);
    ScTrackIterator::Register(ctx);
    ScTrackSegment::Register(ctx);
    ScEntity::Register(ctx);
    ScLitter::Register(ctx);
    ScVehicle::Register(ctx);
    ScCrashedVehicleParticle::Register(ctx);
    ScPeep::Register(ctx);
    ScGuest::Register(ctx);
    ScThought::Register(ctx);
    #ifndef DISABLE_NETWORK
    ScSocket::Register(ctx);
    ScListener::Register(ctx);
    #endif
    ScScenario::Register(ctx);
    ScScenarioObjective::Register(ctx);
    ScPatrolArea::Register(ctx);
    ScStaff::Register(ctx);
    ScHandyman::Register(ctx);
    ScMechanic::Register(ctx);
    ScSecurity::Register(ctx);
    ScPlugin::Register(ctx);

    dukglue_register_global(ctx, std::make_shared<ScCheats>(), "cheats");
    dukglue_register_global(ctx, std::make_shared<ScClimate>(), "climate");
    dukglue_register_global(ctx, std::make_shared<ScConsole>(_console), "console");
    dukglue_register_global(ctx, std::make_shared<ScContext>(_execInfo, _hookEngine), "context");
    dukglue_register_global(ctx, std::make_shared<ScDate>(), "date");
    dukglue_register_global(ctx, std::make_shared<ScMap>(ctx), "map");
    dukglue_register_global(ctx, std::make_shared<ScNetwork>(ctx), "network");
    dukglue_register_global(ctx, std::make_shared<ScPark>(ctx), "park");
    dukglue_register_global(ctx, std::make_shared<ScPlugin>(), "pluginManager");
    dukglue_register_global(ctx, std::make_shared<ScProfiler>(ctx), "profiler");
    dukglue_register_global(ctx, std::make_shared<ScScenario>(), "scenario");
    dukglue_register_global(ctx, std::make_shared<ScObjectManager>(), "objectManager");

    RegisterConstants();

    _initialised = true;
    _transientPluginsEnabled = false;
    _transientPluginsStarted = false;

    LoadSharedStorage();
    ClearParkStorage();
}

class ConstantBuilder
{
private:
    duk_context* _ctx;
    DukValue _obj;

public:
    ConstantBuilder(duk_context* ctx)
        : _ctx(ctx)
    {
        duk_push_global_object(_ctx);
        _obj = DukValue::take_from_stack(_ctx);
    }

    ConstantBuilder& Namespace(std::string_view ns)
    {
        auto flags = DUK_DEFPROP_ENUMERABLE | DUK_DEFPROP_HAVE_WRITABLE | DUK_DEFPROP_HAVE_ENUMERABLE
            | DUK_DEFPROP_HAVE_CONFIGURABLE | DUK_DEFPROP_HAVE_VALUE;

        // Create a new object for namespace
        duk_push_global_object(_ctx);
        duk_push_lstring(_ctx, ns.data(), ns.size());
        duk_push_object(_ctx);

        // Keep a reference to the namespace object
        duk_dup_top(_ctx);
        _obj = DukValue::take_from_stack(_ctx);

        // Place the namespace object into the global context
        duk_def_prop(_ctx, -3, flags);
        duk_pop(_ctx);

        return *this;
    }

    ConstantBuilder& Constant(std::string_view name, int32_t value)
    {
        auto flags = DUK_DEFPROP_ENUMERABLE | DUK_DEFPROP_HAVE_WRITABLE | DUK_DEFPROP_HAVE_ENUMERABLE
            | DUK_DEFPROP_HAVE_CONFIGURABLE | DUK_DEFPROP_HAVE_VALUE;
        _obj.push();
        duk_push_lstring(_ctx, name.data(), name.size());
        duk_push_int(_ctx, value);
        duk_def_prop(_ctx, -3, flags);
        duk_pop(_ctx);
        return *this;
    }
};

void ScriptEngine::RegisterConstants()
{
    ConstantBuilder builder(_context);
    builder.Namespace("TrackSlope")
        .Constant("None", EnumValue(TrackPitch::None))
        .Constant("Up25", EnumValue(TrackPitch::Up25))
        .Constant("Up60", EnumValue(TrackPitch::Up60))
        .Constant("Down25", EnumValue(TrackPitch::Down25))
        .Constant("Down60", EnumValue(TrackPitch::Down60))
        .Constant("Up90", EnumValue(TrackPitch::Up90))
        .Constant("Down90", EnumValue(TrackPitch::Down90));
    builder.Namespace("TrackBanking")
        .Constant("None", EnumValue(TrackRoll::None))
        .Constant("BankLeft", EnumValue(TrackRoll::Left))
        .Constant("BankRight", EnumValue(TrackRoll::Right))
        .Constant("UpsideDown", EnumValue(TrackRoll::UpsideDown));
}

void ScriptEngine::RefreshPlugins()
{
    // Get a list of removed and added plugin files
    auto pluginFiles = GetPluginFiles();
    std::vector<std::string> plugins;
    std::vector<std::string> removedPlugins;
    std::vector<std::string> addedPlugins;
    for (const auto& plugin : _plugins)
    {
        if (plugin->HasPath())
        {
            plugins.emplace_back(plugin->GetPath());
        }
    }

    // The lists need to be sorted for std::set_difference to work properly
    std::sort(pluginFiles.begin(), pluginFiles.end());
    std::sort(plugins.begin(), plugins.end());

    std::set_difference(
        plugins.begin(), plugins.end(), pluginFiles.begin(), pluginFiles.end(), std::back_inserter(removedPlugins));
    std::set_difference(
        pluginFiles.begin(), pluginFiles.end(), plugins.begin(), plugins.end(), std::back_inserter(addedPlugins));

    // Unregister plugin files that were removed
    for (const auto& plugin : removedPlugins)
    {
        UnregisterPlugin(plugin);
    }

    // Register plugin files that were added
    for (const auto& plugin : addedPlugins)
    {
        RegisterPlugin(plugin);
    }

    // Turn on hot reload if not already enabled
    if (!_hotReloadingInitialised && Config::Get().plugin.EnableHotReloading && NetworkGetMode() == NETWORK_MODE_NONE)
    {
        SetupHotReloading();
    }
}

std::vector<std::string> ScriptEngine::GetPluginFiles() const
{
    // Scan for .js files in plugin directory
    std::vector<std::string> pluginFiles;
    auto base = _env.GetDirectoryPath(DirBase::user, DirId::plugins);
    if (Path::DirectoryExists(base))
    {
        auto pattern = Path::Combine(base, u8"*.js");
        auto scanner = Path::ScanDirectory(pattern, true);
        while (scanner->Next())
        {
            auto path = std::string(scanner->GetPath());
            if (ShouldLoadScript(path))
            {
                pluginFiles.push_back(path);
            }
        }
    }
    return pluginFiles;
}

bool ScriptEngine::ShouldLoadScript(std::string_view path)
{
    // A lot of JavaScript is often found in a node_modules directory tree and is most likely unwanted, so ignore it
    return path.find("/node_modules/") == std::string_view::npos && path.find("\\node_modules\\") == std::string_view::npos;
}

void ScriptEngine::UnregisterPlugin(std::string_view path)
{
    try
    {
        auto pluginIt = std::find_if(_plugins.begin(), _plugins.end(), [path](const std::shared_ptr<Plugin>& plugin) {
            return plugin->GetPath() == path;
        });
        auto& plugin = *pluginIt;

        StopPlugin(plugin);
        UnloadPlugin(plugin);
        LogPluginInfo(plugin, "Unregistered");

        _plugins.erase(pluginIt);
    }
    catch (const std::exception& e)
    {
        _console.WriteLineError(e.what());
    }
}

void ScriptEngine::RegisterPlugin(std::string_view path)
{
    try
    {
        auto plugin = std::make_shared<Plugin>(_context, path);

        // We must load the plugin to get the metadata for it
        ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
        plugin->Load();

        // Unload the plugin now, metadata is kept
        plugin->Unload();

        LogPluginInfo(plugin, "Registered");
        _plugins.push_back(std::move(plugin));
    }
    catch (const std::exception& e)
    {
        _console.WriteLineError(e.what());
    }
}

void ScriptEngine::StartIntransientPlugins()
{
    LoadSharedStorage();

    for (auto& plugin : _plugins)
    {
        if (!plugin->HasStarted() && !plugin->IsTransient())
        {
            LoadPlugin(plugin);
            StartPlugin(plugin);
        }
    }

    _intransientPluginsStarted = true;
}

void ScriptEngine::StopUnloadRegisterAllPlugins()
{
    std::vector<std::string> pluginPaths;
    for (auto& plugin : _plugins)
    {
        pluginPaths.emplace_back(plugin->GetPath());
        StopPlugin(plugin);
    }
    for (auto& plugin : _plugins)
    {
        UnloadPlugin(plugin);
    }
    for (auto& pluginPath : pluginPaths)
    {
        UnregisterPlugin(pluginPath);
    }
}

void ScriptEngine::LoadTransientPlugins()
{
    if (!_initialised)
    {
        Initialise();
        RefreshPlugins();
    }
    _transientPluginsEnabled = true;
}

void ScriptEngine::LoadPlugin(const std::string& path)
{
    auto plugin = std::make_shared<Plugin>(_context, path);
    LoadPlugin(plugin);
}

void ScriptEngine::LoadPlugin(std::shared_ptr<Plugin>& plugin)
{
    try
    {
        if (!plugin->IsLoaded())
        {
            const auto& metadata = plugin->GetMetadata();
            if (metadata.MinApiVersion <= kPluginApiVersion)
            {
                ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
                plugin->Load();
                LogPluginInfo(plugin, "Loaded");
            }
            else
            {
                LogPluginInfo(plugin, "Requires newer API version: v" + std::to_string(metadata.MinApiVersion));
            }
        }
    }
    catch (const std::exception& e)
    {
        _console.WriteLineError(e.what());
    }
}

void ScriptEngine::UnloadPlugin(std::shared_ptr<Plugin>& plugin)
{
    if (plugin->IsLoaded())
    {
        plugin->Unload();
        LogPluginInfo(plugin, "Unloaded");
    }
}

void ScriptEngine::StartPlugin(std::shared_ptr<Plugin> plugin)
{
    if (!plugin->HasStarted() && ShouldStartPlugin(plugin))
    {
        ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
        try
        {
            LogPluginInfo(plugin, "Started");
            plugin->Start();
        }
        catch (const std::exception& e)
        {
            _console.WriteLineError(e.what());
        }
    }
}

void ScriptEngine::StopPlugin(std::shared_ptr<Plugin> plugin)
{
    if (plugin->HasStarted())
    {
        plugin->StopBegin();

        for (const auto& callback : _pluginStoppedSubscriptions)
        {
            callback(plugin);
        }
        RemoveCustomGameActions(plugin);
        RemoveIntervals(plugin);
        RemoveSockets(plugin);
        _hookEngine.UnsubscribeAll(plugin);

        plugin->StopEnd();
        LogPluginInfo(plugin, "Stopped");
    }
}

void ScriptEngine::ReloadPlugin(std::shared_ptr<Plugin> plugin)
{
    StopPlugin(plugin);
    {
        ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
        plugin->Load();
        LogPluginInfo(plugin, "Reloaded");
    }
    StartPlugin(plugin);
}

void ScriptEngine::SetupHotReloading()
{
    try
    {
        auto base = _env.GetDirectoryPath(DirBase::user, DirId::plugins);
        if (Path::DirectoryExists(base))
        {
            _pluginFileWatcher = std::make_unique<FileWatcher>(base);
            _pluginFileWatcher->OnFileChanged = [this](u8string_view path) {
                std::lock_guard guard(_changedPluginFilesMutex);
                _changedPluginFiles.emplace(path);
            };
            _hotReloadingInitialised = true;
        }
    }
    catch (const std::exception& e)
    {
        Console::Error::WriteLine("Unable to enable hot reloading of plugins: %s", e.what());
    }
}

void ScriptEngine::DoAutoReloadPluginCheck()
{
    if (_hotReloadingInitialised)
    {
        auto tick = Platform::GetTicks();
        if (tick - _lastHotReloadCheckTick > 1000)
        {
            AutoReloadPlugins();
            _lastHotReloadCheckTick = tick;
        }
    }
}

void ScriptEngine::AutoReloadPlugins()
{
    if (!_changedPluginFiles.empty())
    {
        std::lock_guard guard(_changedPluginFilesMutex);
        for (const auto& path : _changedPluginFiles)
        {
            auto findResult = std::find_if(_plugins.begin(), _plugins.end(), [&path](const std::shared_ptr<Plugin>& plugin) {
                return Path::Equals(path, plugin->GetPath());
            });
            if (findResult != _plugins.end())
            {
                auto& plugin = *findResult;
                try
                {
                    ReloadPlugin(plugin);
                }
                catch (const std::exception& e)
                {
                    _console.WriteLineError(e.what());
                }
            }
        }
        _changedPluginFiles.clear();
    }
}

void ScriptEngine::UnloadTransientPlugins()
{
    // Stop them all first
    for (auto& plugin : _plugins)
    {
        if (plugin->IsTransient())
        {
            StopPlugin(plugin);
        }
    }

    // Now unload them
    for (auto& plugin : _plugins)
    {
        if (plugin->IsTransient())
        {
            UnloadPlugin(plugin);
        }
    }

    _transientPluginsEnabled = false;
    _transientPluginsStarted = false;
}

void ScriptEngine::StartTransientPlugins()
{
    LoadSharedStorage();

    // Load transient plugins
    for (auto& plugin : _plugins)
    {
        if (plugin->IsTransient() && !plugin->IsLoaded() && ShouldStartPlugin(plugin))
        {
            LoadPlugin(plugin);
        }
    }

    // Start transient plugins
    for (auto& plugin : _plugins)
    {
        if (plugin->IsTransient() && plugin->IsLoaded() && !plugin->HasStarted())
        {
            StartPlugin(plugin);
        }
    }

    _transientPluginsStarted = true;
}

bool ScriptEngine::ShouldStartPlugin(const std::shared_ptr<Plugin>& plugin)
{
    auto networkMode = NetworkGetMode();
    if (networkMode == NETWORK_MODE_CLIENT)
    {
        // Only client plugins and plugins downloaded from server should be started
        const auto& metadata = plugin->GetMetadata();
        if (metadata.Type == PluginType::Remote && plugin->HasPath())
        {
            LogPluginInfo(plugin, "Remote plugin not started");
            return false;
        }
    }
    return true;
}

void ScriptEngine::Tick()
{
    if (!_initialised)
    {
        return;
    }

    PROFILED_FUNCTION();

    CheckAndStartPlugins();
    UpdateIntervals();
    UpdateSockets();
    ProcessREPL();
    DoAutoReloadPluginCheck();
}

void ScriptEngine::CheckAndStartPlugins()
{
    auto startIntransient = !_intransientPluginsStarted;
    auto startTransient = !_transientPluginsStarted && _transientPluginsEnabled;

    if (startIntransient || startTransient)
    {
        RefreshPlugins();
    }
    if (startIntransient)
    {
        StartIntransientPlugins();
    }
    if (startTransient)
    {
        StartTransientPlugins();
    }
}

void ScriptEngine::ProcessREPL()
{
    while (_evalQueue.size() > 0)
    {
        auto item = std::move(_evalQueue.front());
        _evalQueue.pop();
        auto promise = std::move(std::get<0>(item));
        auto command = std::move(std::get<1>(item));
        if (duk_peval_string(_context, command.c_str()) != 0)
        {
            std::string result = std::string(duk_safe_to_string(_context, -1));
            _console.WriteLineError(result);
        }
        else if (duk_get_type(_context, -1) != DUK_TYPE_UNDEFINED)
        {
            auto result = Stringify(DukValue::copy_from_stack(_context, -1));
            _console.WriteLine(result);
        }
        duk_pop(_context);
        // Signal the promise so caller can continue
        promise.set_value();
    }
}

std::future<void> ScriptEngine::Eval(const std::string& s)
{
    std::promise<void> barrier;
    auto future = barrier.get_future();
    _evalQueue.emplace(std::move(barrier), s);
    return future;
}

DukValue ScriptEngine::ExecutePluginCall(
    const std::shared_ptr<Plugin>& plugin, const DukValue& func, const std::vector<DukValue>& args, bool isGameStateMutable)
{
    duk_push_undefined(_context);
    auto dukUndefined = DukValue::take_from_stack(_context);
    return ExecutePluginCall(plugin, func, dukUndefined, args, isGameStateMutable);
}

// Must pass plugin by-value, a JS function could destroy the original reference
DukValue ScriptEngine::ExecutePluginCall(
    std::shared_ptr<Plugin> plugin, const DukValue& func, const DukValue& thisValue, const std::vector<DukValue>& args,
    bool isGameStateMutable)
{
    DukStackFrame frame(_context);
    if (func.is_function() && plugin->HasStarted())
    {
        ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, isGameStateMutable);
        func.push();
        thisValue.push();
        for (const auto& arg : args)
        {
            arg.push();
        }
        auto result = duk_pcall_method(_context, static_cast<duk_idx_t>(args.size()));
        if (result == DUK_EXEC_SUCCESS)
        {
            return DukValue::take_from_stack(_context);
        }

        auto message = duk_safe_to_string(_context, -1);
        LogPluginInfo(plugin, message);
        duk_pop(_context);
    }
    return DukValue();
}

void ScriptEngine::LogPluginInfo(std::string_view message)
{
    auto plugin = _execInfo.GetCurrentPlugin();
    LogPluginInfo(plugin, message);
}

void ScriptEngine::LogPluginInfo(const std::shared_ptr<Plugin>& plugin, std::string_view message)
{
    if (plugin == nullptr)
    {
        _console.WriteLine(std::string(message));
    }
    else
    {
        const auto& pluginName = plugin->GetMetadata().Name;
        _console.WriteLine("[" + pluginName + "] " + std::string(message));
    }
}

void ScriptEngine::AddNetworkPlugin(std::string_view code)
{
    auto plugin = std::make_shared<Plugin>(_context, std::string());
    plugin->SetCode(code);
    _plugins.push_back(plugin);
}

void ScriptEngine::RemoveNetworkPlugins()
{
    auto it = _plugins.begin();
    while (it != _plugins.end())
    {
        auto plugin = (*it);
        if (!plugin->HasPath())
        {
            StopPlugin(plugin);
            UnloadPlugin(plugin);
            LogPluginInfo(plugin, "Unregistered network plugin");

            it = _plugins.erase(it);
        }
        else
        {
            it++;
        }
    }
}

GameActions::Result ScriptEngine::QueryOrExecuteCustomGameAction(const CustomAction& customAction, bool isExecute)
{
    std::string actionz = customAction.GetId();
    auto kvp = _customActions.find(actionz);
    if (kvp != _customActions.end())
    {
        const auto& customActionInfo = kvp->second;

        // Deserialise the JSON args
        std::string argsz = customAction.GetJson();

        auto dukArgs = DuktapeTryParseJson(_context, argsz);
        if (!dukArgs)
        {
            auto action = GameActions::Result();
            action.Error = GameActions::Status::InvalidParameters;
            action.ErrorTitle = "Invalid JSON";
            return action;
        }

        std::vector<DukValue> pluginCallArgs;
        if (customActionInfo.Owner->GetTargetAPIVersion() <= kApiVersionCustomActionArgs)
        {
            pluginCallArgs = { *dukArgs };
        }
        else
        {
            DukObject obj(_context);
            obj.Set("action", actionz);
            obj.Set("args", *dukArgs);
            obj.Set("player", customAction.GetPlayer());
            obj.Set("type", EnumValue(customAction.GetType()));

            auto flags = customAction.GetActionFlags();
            obj.Set("isClientOnly", (flags & GameActions::Flags::ClientOnly) != 0);
            pluginCallArgs = { obj.Take() };
        }

        // Ready to call plugin handler
        DukValue dukResult;
        if (!isExecute)
        {
            dukResult = ExecutePluginCall(customActionInfo.Owner, customActionInfo.Query, pluginCallArgs, false);
        }
        else
        {
            dukResult = ExecutePluginCall(customActionInfo.Owner, customActionInfo.Execute, pluginCallArgs, true);
        }
        return DukToGameActionResult(dukResult);
    }

    auto action = GameActions::Result();
    action.Error = GameActions::Status::Unknown;
    action.ErrorTitle = "Unknown custom action";
    action.ErrorMessage = customAction.GetPluginName() + ": " + actionz;
    return action;
}

GameActions::Result ScriptEngine::DukToGameActionResult(const DukValue& d)
{
    auto result = GameActions::Result();
    if (d.type() == DUK_TYPE_OBJECT)
    {
        result.Error = static_cast<GameActions::Status>(AsOrDefault<int32_t>(d["error"]));
        result.ErrorTitle = AsOrDefault<std::string>(d["errorTitle"]);
        result.ErrorMessage = AsOrDefault<std::string>(d["errorMessage"]);
        result.Cost = AsOrDefault<int32_t>(d["cost"]);
        auto expenditureType = AsOrDefault<std::string>(d["expenditureType"]);
        if (!expenditureType.empty())
        {
            auto expenditure = StringToExpenditureType(expenditureType);
            if (expenditure != ExpenditureType::Count)
            {
                result.Expenditure = expenditure;
            }
        }
    }
    else
    {
        result.Error = GameActions::Status::Unknown;
        result.ErrorTitle = "Unknown";
        result.ErrorMessage = "Unknown";
    }
    return result;
}

constexpr static const char* ExpenditureTypes[] = {
    "ride_construction",
    "ride_runningcosts",
    "land_purchase",
    "landscaping",
    "park_entrance_tickets",
    "park_ride_tickets",
    "shop_sales",
    "shop_stock",
    "food_drink_sales",
    "food_drink_stock",
    "wages",
    "marketing",
    "research",
    "interest",
};

std::string_view ScriptEngine::ExpenditureTypeToString(ExpenditureType expenditureType)
{
    auto index = static_cast<size_t>(expenditureType);
    if (index < std::size(ExpenditureTypes))
    {
        return ExpenditureTypes[index];
    }
    return {};
}

ExpenditureType ScriptEngine::StringToExpenditureType(std::string_view expenditureType)
{
    auto it = std::find(std::begin(ExpenditureTypes), std::end(ExpenditureTypes), expenditureType);
    if (it != std::end(ExpenditureTypes))
    {
        return static_cast<ExpenditureType>(std::distance(std::begin(ExpenditureTypes), it));
    }
    return ExpenditureType::Count;
}

DukValue ScriptEngine::GameActionResultToDuk(const GameAction& action, const GameActions::Result& result)
{
    DukStackFrame frame(_context);
    DukObject obj(_context);

    obj.Set("error", static_cast<duk_int_t>(result.Error));
    if (result.Error != GameActions::Status::Ok)
    {
        obj.Set("errorTitle", result.GetErrorTitle());
        obj.Set("errorMessage", result.GetErrorMessage());
    }

    if (result.Cost != kMoney64Undefined)
    {
        obj.Set("cost", result.Cost);
    }
    if (!result.Position.IsNull())
    {
        obj.Set("position", ToDuk(_context, result.Position));
    }
    if (result.Expenditure != ExpenditureType::Count)
    {
        obj.Set("expenditureType", ExpenditureTypeToString(result.Expenditure));
    }

    // RideCreateAction only
    if (action.GetType() == GameCommand::CreateRide)
    {
        if (result.Error == GameActions::Status::Ok)
        {
            const auto rideIndex = result.GetData<RideId>();
            obj.Set("ride", rideIndex.ToUnderlying());
        }
    }
    // StaffHireNewAction only
    else if (action.GetType() == GameCommand::HireNewStaffMember)
    {
        if (result.Error == GameActions::Status::Ok)
        {
            const auto actionResult = result.GetData<StaffHireNewActionResult>();
            if (!actionResult.StaffEntityId.IsNull())
            {
                obj.Set("peep", actionResult.StaffEntityId.ToUnderlying());
            }
        }
    }
    // BannerPlaceAction, LargeSceneryPlaceAction, WallPlaceAction
    auto bannerId = BannerIndex::GetNull();
    switch (action.GetType())
    {
        case GameCommand::PlaceBanner:
            bannerId = result.GetData<BannerPlaceActionResult>().bannerId;
            break;
        case GameCommand::PlaceLargeScenery:
            bannerId = result.GetData<LargeSceneryPlaceActionResult>().bannerId;
            break;
        case GameCommand::PlaceWall:
            bannerId = result.GetData<WallPlaceActionResult>().BannerId;
            break;
        default:
            break;
    }
    if (!bannerId.IsNull())
    {
        obj.Set("bannerIndex", bannerId.ToUnderlying());
    }

    return obj.Take();
}

bool ScriptEngine::RegisterCustomAction(
    const std::shared_ptr<Plugin>& plugin, std::string_view action, const DukValue& query, const DukValue& execute)
{
    std::string actionz = std::string(action);
    if (_customActions.find(actionz) != _customActions.end())
    {
        return false;
    }

    CustomActionInfo customAction;
    customAction.Owner = plugin;
    customAction.Name = std::move(actionz);
    customAction.Query = query;
    customAction.Execute = execute;
    _customActions[customAction.Name] = std::move(customAction);
    return true;
}

void ScriptEngine::RemoveCustomGameActions(const std::shared_ptr<Plugin>& plugin)
{
    for (auto it = _customActions.begin(); it != _customActions.end();)
    {
        if (it->second.Owner == plugin)
        {
            it = _customActions.erase(it);
        }
        else
        {
            it++;
        }
    }
}

class DukToGameActionParameterVisitor : public GameActionParameterVisitor
{
private:
    DukValue _dukValue;

public:
    DukToGameActionParameterVisitor(DukValue&& dukValue)
        : _dukValue(std::move(dukValue))
    {
    }

    void Visit(std::string_view name, bool& param) override
    {
        param = _dukValue[name].as_bool();
    }

    void Visit(std::string_view name, int32_t& param) override
    {
        param = _dukValue[name].as_int();
    }

    void Visit(std::string_view name, std::string& param) override
    {
        param = _dukValue[name].as_string();
    }
};

class DukFromGameActionParameterVisitor : public GameActionParameterVisitor
{
private:
    DukObject& _dukObject;

public:
    DukFromGameActionParameterVisitor(DukObject& dukObject)
        : _dukObject(dukObject)
    {
    }

    void Visit(std::string_view name, bool& param) override
    {
        std::string szName(name);
        _dukObject.Set(szName.c_str(), param);
    }

    void Visit(std::string_view name, int32_t& param) override
    {
        std::string szName(name);
        _dukObject.Set(szName.c_str(), param);
    }

    void Visit(std::string_view name, std::string& param) override
    {
        std::string szName(name);
        _dukObject.Set(szName.c_str(), param);
    }
};

// clang-format off
const static EnumMap<GameCommand> ActionNameToType = {
    { "balloonpress", GameCommand::BalloonPress },
    { "bannerplace", GameCommand::PlaceBanner },
    { "bannerremove", GameCommand::RemoveBanner },
    { "bannersetcolour", GameCommand::SetBannerColour },
    { "bannersetname", GameCommand::SetBannerName },
    { "bannersetstyle", GameCommand::SetBannerStyle },
    { "clearscenery", GameCommand::ClearScenery },
    { "climateset", GameCommand::SetClimate },
    { "footpathplace", GameCommand::PlacePath },
    { "footpathlayoutplace", GameCommand::PlacePathLayout },
    { "footpathremove", GameCommand::RemovePath },
    { "footpathadditionplace", GameCommand::PlaceFootpathAddition },
    { "footpathadditionremove", GameCommand::RemoveFootpathAddition },
    { "gamesetspeed", GameCommand::SetGameSpeed },
    { "guestsetflags", GameCommand::GuestSetFlags },
    { "guestsetname", GameCommand::SetGuestName },
    { "landbuyrights", GameCommand::BuyLandRights },
    { "landlower", GameCommand::LowerLand },
    { "landraise", GameCommand::RaiseLand },
    { "landsetheight", GameCommand::SetLandHeight },
    { "landsetrights", GameCommand::SetLandOwnership },
    { "landsmooth", GameCommand::EditLandSmooth },
    { "largesceneryplace", GameCommand::PlaceLargeScenery },
    { "largesceneryremove", GameCommand::RemoveLargeScenery },
    { "largescenerysetcolour", GameCommand::SetLargeSceneryColour },
    { "loadorquit", GameCommand::LoadOrQuit },
    { "mapchangesize", GameCommand::ChangeMapSize },
    { "mazeplacetrack", GameCommand::PlaceMazeDesign },
    { "mazesettrack", GameCommand::SetMazeTrack },
    { "networkmodifygroup", GameCommand::ModifyGroups },
    { "parkentranceplace", GameCommand::PlaceParkEntrance },
    { "parkentranceremove", GameCommand::RemoveParkEntrance },
    { "parkmarketing", GameCommand::StartMarketingCampaign },
    { "parksetdate", GameCommand::SetDate },
    { "parksetentrancefee", GameCommand::SetParkEntranceFee },
    { "parksetloan", GameCommand::SetCurrentLoan },
    { "parksetname", GameCommand::SetParkName },
    { "parksetparameter", GameCommand::SetParkOpen },
    { "parksetresearchfunding", GameCommand::SetResearchFunding },
    { "pausetoggle", GameCommand::TogglePause },
    { "peeppickup", GameCommand::PickupGuest },
    { "peepspawnplace", GameCommand::PlacePeepSpawn },
    { "playerkick", GameCommand::KickPlayer },
    { "playersetgroup", GameCommand::SetPlayerGroup },
    { "ridecreate", GameCommand::CreateRide },
    { "ridedemolish", GameCommand::DemolishRide },
    { "rideentranceexitplace", GameCommand::PlaceRideEntranceOrExit },
    { "rideentranceexitremove", GameCommand::RemoveRideEntranceOrExit },
    { "ridefreezerating", GameCommand::FreezeRideRating },
    { "ridesetappearance", GameCommand::SetRideAppearance },
    { "ridesetcolourscheme", GameCommand::SetColourScheme },
    { "ridesetname", GameCommand::SetRideName },
    { "ridesetprice", GameCommand::SetRidePrice },
    { "ridesetsetting", GameCommand::SetRideSetting },
    { "ridesetstatus", GameCommand::SetRideStatus },
    { "ridesetvehicle", GameCommand::SetRideVehicles },
    { "scenariosetsetting", GameCommand::EditScenarioOptions },
    { "cheatset", GameCommand::Cheat },
    { "signsetname", GameCommand::SetSignName },
    { "signsetstyle", GameCommand::SetSignStyle },
    { "smallsceneryplace", GameCommand::PlaceScenery },
    { "smallsceneryremove", GameCommand::RemoveScenery },
    { "smallscenerysetcolour", GameCommand::SetSceneryColour},
    { "stafffire", GameCommand::FireStaffMember },
    { "staffhire", GameCommand::HireNewStaffMember },
    { "staffsetcolour", GameCommand::SetStaffColour },
    { "staffsetcostume", GameCommand::SetStaffCostume },
    { "staffsetname", GameCommand::SetStaffName },
    { "staffsetorders", GameCommand::SetStaffOrders },
    { "staffsetpatrolarea", GameCommand::SetStaffPatrol },
    { "surfacesetstyle", GameCommand::ChangeSurfaceStyle },
    { "tilemodify", GameCommand::ModifyTile },
    { "trackdesign", GameCommand::PlaceTrackDesign },
    { "trackplace", GameCommand::PlaceTrack },
    { "trackremove", GameCommand::RemoveTrack },
    { "tracksetbrakespeed", GameCommand::SetBrakesSpeed },
    { "wallplace", GameCommand::PlaceWall },
    { "wallremove", GameCommand::RemoveWall },
    { "wallsetcolour", GameCommand::SetWallColour },
    { "waterlower", GameCommand::LowerWater },
    { "waterraise", GameCommand::RaiseWater },
    { "watersetheight", GameCommand::SetWaterHeight }
};
// clang-format on

static std::string GetActionName(GameCommand commandId)
{
    auto it = ActionNameToType.find(commandId);
    if (it != ActionNameToType.end())
    {
        return std::string{ it->first };
    }
    return {};
}

static std::unique_ptr<GameAction> CreateGameActionFromActionId(const std::string& name)
{
    auto result = ActionNameToType.find(name);
    if (result != ActionNameToType.end())
    {
        return GameActions::Create(result->second);
    }
    return nullptr;
}

void ScriptEngine::RunGameActionHooks(const GameAction& action, GameActions::Result& result, bool isExecute)
{
    DukStackFrame frame(_context);

    auto hookType = isExecute ? HookType::actionExecute : HookType::actionQuery;
    if (_hookEngine.HasSubscriptions(hookType))
    {
        DukObject obj(_context);

        auto actionId = action.GetType();
        if (action.GetType() == GameCommand::Custom)
        {
            auto customAction = static_cast<const CustomAction&>(action);
            obj.Set("action", customAction.GetId());

            auto dukArgs = DuktapeTryParseJson(_context, customAction.GetJson());
            if (dukArgs)
            {
                obj.Set("args", *dukArgs);
            }
            else
            {
                DukObject args(_context);
                obj.Set("args", args.Take());
            }
        }
        else
        {
            auto actionName = GetActionName(actionId);
            if (!actionName.empty())
            {
                obj.Set("action", actionName);
            }

            DukObject args(_context);
            DukFromGameActionParameterVisitor visitor(args);
            const_cast<GameAction&>(action).AcceptParameters(visitor);
            const_cast<GameAction&>(action).AcceptFlags(visitor);
            obj.Set("args", args.Take());
        }

        obj.Set("player", action.GetPlayer());
        obj.Set("type", EnumValue(actionId));

        auto flags = action.GetActionFlags();
        obj.Set("isClientOnly", (flags & GameActions::Flags::ClientOnly) != 0);

        obj.Set("result", GameActionResultToDuk(action, result));
        auto dukEventArgs = obj.Take();

        _hookEngine.Call(hookType, dukEventArgs, false);

        if (!isExecute)
        {
            auto dukResult = dukEventArgs["result"];
            if (dukResult.type() == DukValue::Type::OBJECT)
            {
                auto error = AsOrDefault<int32_t>(dukResult["error"]);
                if (error != 0)
                {
                    result.Error = static_cast<GameActions::Status>(error);
                    result.ErrorTitle = AsOrDefault<std::string>(dukResult["errorTitle"]);
                    result.ErrorMessage = AsOrDefault<std::string>(dukResult["errorMessage"]);
                }
            }
        }
    }
}

std::unique_ptr<GameAction> ScriptEngine::CreateGameAction(
    const std::string& actionid, const DukValue& args, const std::string& pluginName)
{
    auto action = CreateGameActionFromActionId(actionid);
    if (action != nullptr)
    {
        DukValue argsCopy = args;
        DukToGameActionParameterVisitor visitor(std::move(argsCopy));
        action->AcceptParameters(visitor);
        if (args["flags"].type() == DukValue::Type::NUMBER)
        {
            action->AcceptFlags(visitor);
        }
        return action;
    }

    // Serialise args to json so that it can be sent
    auto ctx = args.context();
    if (args.type() == DukValue::Type::OBJECT)
    {
        args.push();
    }
    else
    {
        duk_push_object(ctx);
    }
    auto jsonz = duk_json_encode(ctx, -1);
    auto json = std::string(jsonz);
    duk_pop(ctx);
    auto customAction = std::make_unique<CustomAction>(actionid, json, pluginName);

    if (customAction->GetPlayer() == -1 && NetworkGetMode() != NETWORK_MODE_NONE)
    {
        customAction->SetPlayer(NetworkGetCurrentPlayerId());
    }
    return customAction;
}

void ScriptEngine::InitSharedStorage()
{
    duk_push_object(_context);
    _sharedStorage = std::move(DukValue::take_from_stack(_context));
}

void ScriptEngine::LoadSharedStorage()
{
    InitSharedStorage();

    auto path = _env.GetFilePath(PathId::pluginStore);
    try
    {
        if (File::Exists(path))
        {
            auto data = File::ReadAllBytes(path);
            auto result = DuktapeTryParseJson(
                _context, std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
            if (result)
            {
                _sharedStorage = std::move(*result);
            }
        }
    }
    catch (const std::exception&)
    {
        Console::Error::WriteLine("Unable to read '%s'", path.c_str());
    }
}

void ScriptEngine::SaveSharedStorage()
{
    auto path = _env.GetFilePath(PathId::pluginStore);
    try
    {
        _sharedStorage.push();
        auto json = std::string(duk_json_encode(_context, -1));
        duk_pop(_context);

        File::WriteAllBytes(path, json.c_str(), json.size());
    }
    catch (const std::exception&)
    {
        Console::Error::WriteLine("Unable to write to '%s'", path.c_str());
    }
}

void ScriptEngine::ClearParkStorage()
{
    duk_push_object(_context);
    _parkStorage = std::move(DukValue::take_from_stack(_context));
}

std::string ScriptEngine::GetParkStorageAsJSON()
{
    _parkStorage.push();
    auto json = std::string(duk_json_encode(_context, -1));
    duk_pop(_context);
    return json;
}

void ScriptEngine::SetParkStorageFromJSON(std::string_view value)
{
    auto result = DuktapeTryParseJson(_context, value);
    if (result)
    {
        _parkStorage = std::move(*result);
    }
}

IntervalHandle ScriptEngine::AllocateHandle()
{
    const auto nextHandle = _nextIntervalHandle;

    // In case of overflow start from 1 again
    _nextIntervalHandle = std::max(_nextIntervalHandle + 1u, 1u);

    return nextHandle;
}

IntervalHandle ScriptEngine::AddInterval(const std::shared_ptr<Plugin>& plugin, int32_t delay, bool repeat, DukValue&& callback)
{
    auto handle = AllocateHandle();
    assert(handle != 0);

    auto& interval = _intervals[handle];
    interval.Owner = plugin;
    interval.Delay = delay;
    interval.LastTimestamp = _lastIntervalTimestamp;
    interval.Callback = std::move(callback);
    interval.Repeat = repeat;

    return handle;
}

void ScriptEngine::RemoveInterval(const std::shared_ptr<Plugin>& plugin, IntervalHandle handle)
{
    if (handle == 0)
        return;

    auto it = _intervals.find(handle);
    if (it == _intervals.end())
        return;

    auto& interval = it->second;

    // Only allow owner or REPL (nullptr) to remove intervals
    if (plugin == nullptr || interval.Owner == plugin)
    {
        interval.Deleted = true;
    }
}

void ScriptEngine::UpdateIntervals()
{
    uint32_t timestamp = Platform::GetTicks();
    if (timestamp < _lastIntervalTimestamp)
    {
        // timestamp has wrapped, subtract all intervals by the remaining amount before wrap
        auto delta = static_cast<int64_t>(std::numeric_limits<uint32_t>::max() - _lastIntervalTimestamp);
        for (auto& entry : _intervals)
        {
            auto& interval = entry.second;
            interval.LastTimestamp = -delta;
        }
    }
    _lastIntervalTimestamp = timestamp;

    // Erase all intervals marked as deleted.
    for (auto it = _intervals.begin(); it != _intervals.end();)
    {
        auto& interval = it->second;

        if (interval.Deleted)
        {
            it = _intervals.erase(it);
        }
        else
        {
            it++;
        }
    }

    // Execute all intervals that are due.
    for (auto it = _intervals.begin(); it != _intervals.end(); it++)
    {
        auto& interval = it->second;

        if (timestamp < interval.LastTimestamp + interval.Delay)
        {
            continue;
        }

        if (interval.Deleted)
        {
            // There is a chance that in one of the callbacks it deletes another interval.
            continue;
        }

        ExecutePluginCall(interval.Owner, interval.Callback, {}, false);

        interval.LastTimestamp = timestamp;
        if (!interval.Repeat)
        {
            interval.Deleted = true;
        }
    }
}

void ScriptEngine::RemoveIntervals(const std::shared_ptr<Plugin>& plugin)
{
    for (auto it = _intervals.begin(); it != _intervals.end();)
    {
        auto& interval = it->second;

        if (interval.Owner == plugin)
        {
            it = _intervals.erase(it);
        }
        else
        {
            it++;
        }
    }
}

    #ifndef DISABLE_NETWORK
void ScriptEngine::AddSocket(const std::shared_ptr<ScSocketBase>& socket)
{
    _sockets.push_back(socket);
}
    #endif

void ScriptEngine::UpdateSockets()
{
    #ifndef DISABLE_NETWORK
    // Use simple for i loop as Update calls can modify the list
    auto it = _sockets.begin();
    while (it != _sockets.end())
    {
        auto& socket = *it;
        socket->Update();
        if (socket->IsDisposed())
        {
            it = _sockets.erase(it);
        }
        else
        {
            it++;
        }
    }
    #endif
}

void ScriptEngine::RemoveSockets(const std::shared_ptr<Plugin>& plugin)
{
    #ifndef DISABLE_NETWORK
    auto it = _sockets.begin();
    while (it != _sockets.end())
    {
        auto socket = it->get();
        if (socket->GetPlugin() == plugin)
        {
            socket->Dispose();
            it = _sockets.erase(it);
        }
        else
        {
            it++;
        }
    }
    #endif
}

std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
{
    return ExpressionStringifier::StringifyExpression(val);
}

std::string OpenRCT2::Scripting::ProcessString(const DukValue& value)
{
    if (value.type() == DukValue::Type::STRING)
        return value.as_string();
    return {};
}

bool OpenRCT2::Scripting::IsGameStateMutable()
{
    // Allow single player to alter game state anywhere
    if (NetworkGetMode() == NETWORK_MODE_NONE)
    {
        return true;
    }

    auto& scriptEngine = GetContext()->GetScriptEngine();
    auto& execInfo = scriptEngine.GetExecInfo();
    return execInfo.IsGameStateMutable();
}

void OpenRCT2::Scripting::ThrowIfGameStateNotMutable()
{
    // Allow single player to alter game state anywhere
    if (NetworkGetMode() != NETWORK_MODE_NONE)
    {
        auto& scriptEngine = GetContext()->GetScriptEngine();
        auto& execInfo = scriptEngine.GetExecInfo();
        if (!execInfo.IsGameStateMutable())
        {
            auto ctx = scriptEngine.GetContext();
            duk_error(ctx, DUK_ERR_ERROR, "Game state is not mutable in this context.");
        }
    }
}

int32_t OpenRCT2::Scripting::GetTargetAPIVersion()
{
    auto& scriptEngine = GetContext()->GetScriptEngine();
    auto& execInfo = scriptEngine.GetExecInfo();

    // Commands from the in-game console do not have a plug-in set
    auto plugin = execInfo.GetCurrentPlugin();
    if (plugin == nullptr)
    {
        // For in-game console, default to the current API version
        return kPluginApiVersion;
    }

    return plugin->GetTargetAPIVersion();
}

duk_bool_t duk_exec_timeout_check(void*)
{
    return false;
}

#endif
