#ifndef CONTROLLER_HPP_
#define CONTROLLER_HPP_

#include <ControllerChannel/ControllerChannel.hpp>
#include <Log/Log.hpp>
#include <Serialize/Nested.hpp>
#include <memory>
#include <string>
#include <functional>
#include <unordered_map>
#include <Widget/Widget.hpp>
#include <Serialize/Serialize.hpp>
#include <Machine/Machine.hpp>

class Player;
class World;

/**
 * @brief Represents a controller through which a player interacts.
 */
class Controller
{
public:
    /**
     * @brief Specifies which side of a multiplayer connection a specific RPC call can run on.
     */
    enum class RunsOn
    {
        Client,
        Server,
    };

    /**
     * @brief Represents the promise of a result.
     * 
     * @tparam TResult The result type.
     */
    template<typename TResult>
    class Promise
    {
    private:
        TResult result;
        bool ready;
        std::function<void(TResult)> callback;

    public:
        typedef std::shared_ptr<Promise<TResult>> Handle;

        /**
         * @brief Construct the promise as not ready.
         */
        Promise() : ready(false), callback([](TResult){}) {}

        /**
         * @brief Resolve with the result.
         * 
         * @param result The result.
         */
        void resolve(TResult result)
        {
            this->result = result;
            ready = true;

            callback(result);
        }

        /**
         * @brief Set a function to call when the result is received.
         * 
         * @param callback The callback which receives the result.
         */
        void then(std::function<void(TResult)> callback)
        {
            this->callback = callback;

            if (ready)
            {
                callback(result);
            }
        }

        /**
         * @brief Create a promise.
         * 
         * @return The promise handle.
         */
        static Handle create()
        {
            return std::make_shared<Promise<TResult>>();
        }
    };

private:
    std::unordered_map<uint64_t, std::function<void(const uint8_t *, size_t)>> promises;
    uint64_t nextPromiseId;
    std::vector<std::function<bool(uint64_t, const uint8_t *, size_t)>> commands;

public:
    /**
     * @brief Represents a command (an RPC).
     * 
     * @tparam TReturn The return type of this command.
     * @tparam TArgs The argument types for this command.
     */
    template<typename TReturn, typename... TArgs>
    class Command
    {
    protected:
        typedef std::function<TReturn(TArgs...)> Callback;

    private:
        Controller *ctrl;
        uint8_t opcode;
        RunsOn runsOn;
        Callback callback;

        /**
         * @brief Call this command based on the serialized arguments.
         * 
         * @param promiseId The promise ID.
         * @param payload The serialized argument payload.
         * @param size The size of the payload.
         */
        bool dispatch(uint64_t promiseId, const uint8_t *payload, size_t size)
        {
            if (runsOn != ctrl->getRunsOnPolicy())
            {
                Log::error(formatString("RunsOn policy violation!"));
                return false;
            }

            std::tuple<TArgs...> argTuple{};
            if (std::apply([payload, size](TArgs&... args){ return deserialize(payload, size, args...); }, argTuple) != size)
            {
                return false;
            }

            TReturn result = std::apply(callback, argTuple);

            NestedPayload returnPayload;
            serializeInto(returnPayload.buffer, result);

            ctrl->channel.remoteReturn(promiseId, returnPayload);
            return true;
        }
    
    public:
        /**
         * @brief Construct the command.
         * 
         * @param ctrl The controller this command belongs to.
         * @param runsOn The side of the connection this command is allowed to run on.
         * @param callback The callback.
         */
        Command(Controller *ctrl, RunsOn runsOn, Callback callback) :
            ctrl(ctrl),
            runsOn(runsOn),
            callback(callback)
        {
            if (ctrl->commands.size() == 256)
            {
                crash("Too many commands registered against controller!");
            }

            opcode = ctrl->commands.size();
            ctrl->commands.push_back([this](uint64_t promiseId, const uint8_t *payload, size_t size) -> bool
            {
                return this->dispatch(promiseId, payload, size);
            });
        }

        /**
         * @brief Perform the remote procedure call.
         * 
         * @param args Arguments.
         * @return A promise which will resolve with the return value from this call.
         */
        typename Promise<TReturn>::Handle operator()(TArgs... args)
        {
            typename Promise<TReturn>::Handle promise = Promise<TReturn>::create();
            if (!ctrl->channel.hasPeer())
            {
                promise->resolve(callback(args...));
                return promise;
            }

            uint64_t promiseId = ctrl->nextPromiseId++;

            NestedPayload payload;
            serializeInto(payload.buffer, args...);

            ctrl->promises[promiseId] = [promise](const uint8_t *data, size_t size)
            {
                TReturn result{};
                if (deserialize(data, size, result) == size)
                {
                    promise->resolve(result);
                }
            };

            ctrl->channel.remoteCall(opcode, promiseId, payload);

            return promise;
        }

        friend class Controller;
    };

protected:
    ControllerChannel &channel;
    Player* player;
    World &world;
    std::shared_ptr<Machine> boundMachine;
    Command<uint32_t, uint32_t> requestClose;

public:
    /**
     * @brief Construct this controller.
     * 
     * @param channel The controller channel.
     */
    Controller(ControllerChannel &channel);

    virtual ~Controller() = default;

    /**
     * @brief Returns the saved machine config.
     *
     * TODO: Remove the dummy argument when possible.
     */
    Controller::Command<MachineConfig, uint32_t> saveConfig;

    /**
     * @brief Load the given config into the machine (without consuming items).
     * 
     * @param config Config to load.
     */
    Controller::Command<uint32_t, MachineConfig> loadConfig;

    /**
     * @brief Returns the player bound to this controller.
     */
    Player* getPlayer();

    /**
     * @brief Create the widget which displays this controller.
     * 
     * @return The widget.
     */
    virtual Widget* getWidget() = 0;

    /**
     * @brief Update the controller.
     */
    virtual void update();

    /**
     * @brief Dispatch a remote call.
     * 
     * @param promiseId The promise Id.
     * @param opcode The command opcode.
     * @param payload The payload.
     * @param size The payload size.
     */
    void dispatch(uint64_t promiseId, uint8_t opcode, const uint8_t *payload, size_t size);

    /**
     * @brief Resolve a promise.
     * 
     * @param promiseId The promise ID.
     * @param payload Result payload.
     * @param size Result payload size.
     */
    void resolve(uint64_t promiseId, const uint8_t *payload, size_t size);

    /**
     * @brief Returns true if an operation is currently in progress.
     */
    virtual bool isBusy() = 0;

    /**
     * @brief Returns the `RunsOn` value that a command needs to be allowed to be dispatched.
     *
     * Only applies if there is a peer.
     */
    RunsOn getRunsOnPolicy();
};

#endif