#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H
#define COMPONENTS_LUA_SCRIPTSCONTAINER_H

#include <map>
#include <set>
#include <string>
#include <variant>

#include <components/debug/debuglog.hpp>
#include <components/esm/luascripts.hpp>

#include "luastate.hpp"
#include "serialization.hpp"

namespace LuaUtil
{
    class ScriptTracker;
    class ScriptsContainer;
    using ScriptsContainerLifetime = std::shared_ptr<ScriptsContainer*>;

    // Immutable ScriptsContainerLifetime
    class ScriptsContainerWeakPtr
    {
        ScriptsContainerLifetime mWeakPtr;

    public:
        ScriptsContainerWeakPtr(const ScriptsContainerWeakPtr&) = default;
        ScriptsContainerWeakPtr(ScriptsContainerWeakPtr&&) = default;

        explicit ScriptsContainerWeakPtr(ScriptsContainerLifetime ptr)
            : mWeakPtr(std::move(ptr))
        {
        }

        ScriptsContainer* operator*() const noexcept
        {
            if (auto ptr = mWeakPtr.get())
                return *ptr;
            // this shouldn't happen unless you use it after a move or try to be funny by constructing from nullptr
            return nullptr;
        }
    };

    inline auto operator<=>(const ScriptsContainerWeakPtr& lhs, const ScriptsContainerWeakPtr& rhs)
    {
        return *lhs <=> *rhs;
    }

    inline auto operator<=>(const ScriptsContainerWeakPtr& lhs, ScriptsContainer* rhs)
    {
        return *lhs <=> rhs;
    }

    // ScriptsContainer is a base class for all scripts containers (LocalScripts,
    // GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox.
    // Scripts from different containers can interact to each other only via events.
    // Scripts within one container can interact via interfaces.
    // All scripts from one container have the same set of API packages available.
    //
    // Each script should return a table in a specific format that describes its
    // handlers and interfaces. Every section of the table is optional. Basic structure:
    //
    //     local function update(dt)
    //         print("Update")
    //     end
    //
    //     local function someEventHandler(eventData)
    //         print("'SomeEvent' received")
    //     end
    //
    //     return {
    //         -- Provides interface for other scripts in the same container
    //         interfaceName = "InterfaceName",
    //         interface = {
    //             someFunction = function() print("someFunction was called from another script") end,
    //         },
    //
    //         -- Script interface for the engine. Not available for other script.
    //         -- An error is printed if unknown handler is specified.
    //         engineHandlers = {
    //             onUpdate = update,
    //             onInit = function(initData) ... end,  -- used when the script is just created (not loaded)
    //             onSave = function() return ... end,
    //             onLoad = function(state, initData) ... end,  -- "state" is the data that was earlier returned by
    //             onSave
    //
    //             -- Works only if a child class has passed a EngineHandlerList
    //             -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers.
    //             onSomethingElse = function() print("something else") end
    //         },
    //
    //         -- Handlers for events, sent from other scripts. Engine itself never sent events. Any name can be used
    //         for an event. eventHandlers = {
    //             SomeEvent = someEventHandler
    //         }
    //     }

    class ScriptsContainer
    {
    public:
        // ScriptId of each script is stored with this key in Script::mHiddenData.
        // Removed from mHiddenData when the script if removed.
        constexpr static std::string_view sScriptIdKey = "_id";

        // Debug identifier of each script is stored with this key in Script::mHiddenData.
        // Present in mHiddenData even after removal of the script from ScriptsContainer.
        constexpr static std::string_view sScriptDebugNameKey = "_name";

        using TimerType = ESM::LuaTimer::Type;

        // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print`
        // output. `tracker` is a tracker for managing the container's state. `load` specifies whether the container
        // should be constructed in a loaded state.
        ScriptsContainer(
            LuaState* lua, std::string_view namePrefix, ScriptTracker* tracker = nullptr, bool load = true);

        ScriptsContainer(const ScriptsContainer&) = delete;
        ScriptsContainer(ScriptsContainer&&) = delete;
        virtual ~ScriptsContainer();

        // `conf` specifies the list of scripts that should be autostarted in this container; the script
        // names themselves are stored in ScriptsConfiguration.
        void setAutoStartConf(ScriptIdsWithInitializationData conf) { mAutoStartScripts = std::move(conf); }
        const ScriptIdsWithInitializationData& getAutoStartConf() const { return mAutoStartScripts; }

        // Adds package that will be available (via `require`) for all scripts in the container.
        // Automatically applies LuaUtil::makeReadOnly to the package.
        void addPackage(std::string packageName, sol::main_object package);

        // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a
        // new script, adds it to the container, and calls onInit for this script. Returns `true` if the script was
        // successfully added. The script should have CUSTOM flag. If the flag is not set, or file not found, or has
        // syntax errors, returns false. If such script already exists in the container, then also returns false.
        bool addCustomScript(int scriptId, std::string_view initData = {});

        bool hasScript(int scriptId) const;
        void removeScript(int scriptId);

        void processTimers(double simulationTime, double gameTime);

        // Calls `onUpdate` (if present) for every script in the container.
        // Handlers are called in the same order as scripts were added.
        void update(float dt) { callEngineHandlers(mUpdateHandlers, dt); }

        // Calls event handlers `eventName` (if present) for every script.
        // If several scripts register handlers for `eventName`, they are called in reverse order.
        // If some handler returns `false`, all remaining handlers are ignored. Any other return value
        // (including `nil`) has no effect.
        void receiveEvent(std::string_view eventName, std::string_view eventData);

        // Serializer defines how to serialize/deserialize userdata. If serializer is not provided,
        // only built-in types and types from util package can be serialized.
        void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; }

        // Special deserializer to use when load data from saves. Can be used to remap content files in Refnums.
        void setSavedDataDeserializer(const UserdataSerializer* serializer) { mSavedDataDeserializer = serializer; }

        // Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used.
        void addAutoStartedScripts();

        // Removes all scripts including the auto started.
        void removeAllScripts();

        // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to
        // ESM::LuaScripts.
        void save(ESM::LuaScripts&);

        // Removes all scripts; starts scripts according to `autoStartMode` and
        // loads the savedScripts. Runs "onLoad" for each script.
        void load(const ESM::LuaScripts& savedScripts);

        // Callbacks for serializable timers should be registered in advance.
        // The script with the given path should already present in the container.
        void registerTimerCallback(int scriptId, std::string_view callbackName, sol::main_protected_function callback);

        // Sets up a timer, that can be automatically saved and loaded.
        //   type - the type of timer, either SIMULATION_TIME or GAME_TIME.
        //   time - the absolute game time (in seconds or in hours) when the timer should be executed.
        //   scriptPath - script path in VFS is used as script id. The script with the given path should already present
        //   in the container. callbackName - callback (should be registered in advance) for this timer. callbackArg -
        //   parameter for the callback (should be serializable).
        void setupSerializableTimer(
            TimerType type, double time, int scriptId, std::string_view callbackName, sol::main_object callbackArg);

        // Creates a timer. `callback` is an arbitrary Lua function. These timers are called "unsavable"
        // because they can not be stored in saves. I.e. loading a saved game will not fully restore the state.
        void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::main_protected_function callback);

        // Informs that new frame is started. Needed to track Lua instruction count per frame.
        void statsNextFrame();

        struct ScriptStats
        {
            float mAvgInstructionCount = 0; // averaged number of Lua instructions per frame
            int64_t mMemoryUsage = 0; // bytes
        };
        void collectStats(std::vector<ScriptStats>& stats) const;
        static int64_t getInstanceCount() { return sInstanceCount; }

        virtual bool isActive() const { return false; }

        ScriptsContainerWeakPtr getWeakPointer() const;

    protected:
        // Call a function on an interface.
        template <typename T, typename... Args>
        std::optional<T> callInterface(std::string_view interfaceName, std::string_view identifier, const Args&... args)
        {
            std::optional<T> res = std::nullopt;
            mLua.protectedCall([&](LuaUtil::LuaView& view) {
                LoadedData& data = ensureLoaded();
                auto interface = data.mPublicInterfaces.get<sol::optional<sol::table>>(interfaceName);
                if (interface)
                {
                    auto o = interface->get_or<sol::object>(identifier, sol::nil);
                    if (o.is<sol::function>())
                    {
                        sol::object luaRes = o.as<sol::function>().call(args...);
                        if (luaRes.is<T>())
                            res = luaRes.as<T>();
                    }
                }
            });

            return res;
        }

        struct Handler
        {
            int mScriptId;
            sol::main_function mFn;
        };

        struct EngineHandlerList
        {
            std::string_view mName;
            std::vector<Handler> mList;

            // "name" must be string literal
            explicit EngineHandlerList(std::string_view name)
                : mName(name)
            {
            }
        };

        // Calls given handlers in direct order.
        template <typename... Args>
        void callEngineHandlers(EngineHandlerList& handlers, const Args&... args)
        {
            ensureLoaded();
            for (Handler& handler : handlers.mList)
            {
                try
                {
                    LuaUtil::call({ this, handler.mScriptId }, handler.mFn, args...);
                }
                catch (std::exception& e)
                {
                    Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " << handlers.mName
                                      << " failed. " << e.what();
                }
            }
        }

        // To add a new engine handler a derived class should register the corresponding EngineHandlerList and define
        // a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`.
        void registerEngineHandlers(std::initializer_list<EngineHandlerList*> handlers);

        const std::string mNamePrefix;
        LuaUtil::LuaState& mLua;

    private:
        struct Script
        {
            std::optional<sol::main_function> mOnSave;
            std::optional<sol::main_function> mOnOverride;
            std::optional<sol::main_table> mInterface;
            std::string mInterfaceName;
            sol::main_table mHiddenData;
            std::map<std::string, sol::main_protected_function> mRegisteredCallbacks;
            std::map<int64_t, sol::main_protected_function> mTemporaryCallbacks;
            VFS::Path::Normalized mPath;
            ScriptStats mStats;

            ~Script();
        };
        struct Timer
        {
            double mTime;
            bool mSerializable;
            int mScriptId;
            std::variant<std::string, int64_t> mCallback; // string if serializable, integer otherwise
            sol::main_object mArg;
            std::string mSerializedArg;

            bool operator<(const Timer& t) const { return mTime > t.mTime; }
        };
        using EventHandlerList = std::vector<Handler>;

        friend class LuaState;
        void addInstructionCount(int scriptId, int64_t instructionCount);
        void addMemoryUsage(int scriptId, int64_t memoryDelta);

        // Add to container without calling onInit/onLoad.
        bool addScript(
            LuaView& view, int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& onLoad);

        // Returns script by id (throws an exception if doesn't exist)
        Script& getScript(int scriptId);

        void printError(int scriptId, std::string_view msg, const std::exception& e) const;

        const VFS::Path::Normalized& scriptPath(int scriptId) const
        {
            return mLua.getConfiguration()[scriptId].mScriptPath;
        }

        void callOnInit(LuaView& view, int scriptId, const sol::function& onInit, std::string_view data);
        void callTimer(const Timer& t);
        void updateTimerQueue(std::vector<Timer>& timerQueue, double time);
        static void insertTimer(std::vector<Timer>& timerQueue, Timer&& t);
        static void insertHandler(std::vector<Handler>& list, int scriptId, sol::function fn);
        static void removeHandler(std::vector<Handler>& list, int scriptId);
        void insertInterface(int scriptId, const Script& script);
        void removeInterface(int scriptId, const Script& script);
        void save(LuaView&, ESM::LuaScripts&);

        ScriptIdsWithInitializationData mAutoStartScripts;
        const UserdataSerializer* mSerializer = nullptr;
        const UserdataSerializer* mSavedDataDeserializer = nullptr;

        std::map<std::string, sol::main_object> mAPI;
        struct LoadedData
        {
            std::map<int, Script> mScripts;
            sol::main_table mPublicInterfaces;

            std::map<std::string, EventHandlerList, std::less<>> mEventHandlers;

            std::vector<Timer> mSimulationTimersQueue;
            std::vector<Timer> mGameTimersQueue;
        };
        using UnloadedData = ESM::LuaScripts;

        // Unloads the container to free resources held by the shared Lua state. This method serializes the container's
        // state. The serialized data is automatically restored to the Lua state as required. Unloading and reloading
        // the container is functionally equivalent to saving and loading the game, meaning the appropriate engine
        // handlers are invoked.
        UnloadedData& ensureUnloaded(LuaView& lua);
        LoadedData& ensureLoaded();

        EngineHandlerList mUpdateHandlers{ "onUpdate" };
        std::map<std::string_view, EngineHandlerList*> mEngineHandlers;
        std::variant<UnloadedData, LoadedData> mData;
        int64_t mTemporaryCallbackCounter = 0;

        std::map<int, int64_t> mRemovedScriptsMemoryUsage;
        ScriptsContainerLifetime mThis; // used by LuaState to track ownership of memory allocations

        ScriptTracker* mTracker;
        bool mRequiredLoading = false;
        friend class ScriptTracker;

        static int64_t sInstanceCount; // debug information, shown in Lua profiler
    };
}

#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H
