#ifndef ENTT_ENTITY_PROTOTYPE_HPP
#define ENTT_ENTITY_PROTOTYPE_HPP


#include <tuple>
#include <utility>
#include <cstddef>
#include <type_traits>
#include <unordered_map>
#include "../config/config.h"
#include "registry.hpp"
#include "entity.hpp"


namespace entt {


/**
 * @brief Prototype container for _concepts_.
 *
 * A prototype is used to define a _concept_ in terms of components.<br/>
 * Prototypes act as templates for those specific types of an application which
 * users would otherwise define through a series of component assignments to
 * entities. In other words, prototypes can be used to assign components to
 * entities of a registry at once.
 *
 * @note
 * Components used along with prototypes must be copy constructible. Prototypes
 * wrap component types with custom types, so they do not interfere with other
 * users of the registry they were built with.
 *
 * @warning
 * Prototypes directly use their underlying registries to store entities and
 * components for their purposes. Users must ensure that the lifetime of a
 * registry and its contents exceed that of the prototypes that use it.
 *
 * @tparam Entity A valid entity type (see entt_traits for more details).
 */
template<typename Entity>
class prototype final {
    using basic_fn_type = void(const prototype &, registry<Entity> &, const Entity);
    using component_type = typename registry<Entity>::component_type;

    template<typename Component>
    struct component_wrapper { Component component; };

    struct component_handler {
        basic_fn_type *assign_or_replace;
        basic_fn_type *assign;
    };

    void release() {
        if(reg->valid(entity)) {
            reg->destroy(entity);
        }
    }

public:
    /*! @brief Registry type. */
    using registry_type = registry<Entity>;
    /*! @brief Underlying entity identifier. */
    using entity_type = Entity;
    /*! @brief Unsigned integer type. */
    using size_type = std::size_t;

    /**
     * @brief Constructs a prototype that is bound to a given registry.
     * @param reg A valid reference to a registry.
     */
    prototype(registry<Entity> &reg)
        : reg{&reg},
          entity{reg.create()}
    {}

    /**
     * @brief Releases all its resources.
     */
    ~prototype() {
        release();
    }

    /*! @brief Copying a prototype isn't allowed. */
    prototype(const prototype &) = delete;

    /**
     * @brief Move constructor.
     *
     * After prototype move construction, instances that have been moved from
     * are placed in a valid but unspecified state. It's highly discouraged to
     * continue using them.
     *
     * @param other The instance to move from.
     */
    prototype(prototype &&other)
        : handlers{std::move(other.handlers)},
          reg{other.reg},
          entity{other.entity}
    {
        other.entity = null;
    }

    /*! @brief Copying a prototype isn't allowed. @return This prototype. */
    prototype & operator=(const prototype &) = delete;

    /**
     * @brief Move assignment operator.
     *
     * After prototype move assignment, instances that have been moved from are
     * placed in a valid but unspecified state. It's highly discouraged to
     * continue using them.
     *
     * @param other The instance to move from.
     * @return This prototype.
     */
    prototype & operator=(prototype &&other) {
        if(this != &other) {
            auto tmp{std::move(other)};
            handlers.swap(tmp.handlers);
            std::swap(reg, tmp.reg);
            std::swap(entity, tmp.entity);
        }

        return *this;
    }

    /**
     * @brief Assigns to or replaces the given component of a prototype.
     * @tparam Component Type of component to assign or replace.
     * @tparam Args Types of arguments to use to construct the component.
     * @param args Parameters to use to initialize the component.
     * @return A reference to the newly created component.
     */
    template<typename Component, typename... Args>
    Component & set(Args &&... args) {
        basic_fn_type *assign_or_replace = [](const prototype &prototype, registry<Entity> &other, const Entity dst) {
            const auto &wrapper = prototype.reg->template get<component_wrapper<Component>>(prototype.entity);
            other.template assign_or_replace<Component>(dst, wrapper.component);
        };

        basic_fn_type *assign = [](const prototype &prototype, registry<Entity> &other, const Entity dst) {
            if(!other.template has<Component>(dst)) {
                const auto &wrapper = prototype.reg->template get<component_wrapper<Component>>(prototype.entity);
                other.template assign<Component>(dst, wrapper.component);
            }
        };

        handlers[reg->template type<Component>()] = component_handler{assign_or_replace, assign};
        auto &wrapper = reg->template assign_or_replace<component_wrapper<Component>>(entity, Component{std::forward<Args>(args)...});
        return wrapper.component;
    }

    /**
     * @brief Removes the given component from a prototype.
     * @tparam Component Type of component to remove.
     */
    template<typename Component>
    void unset() ENTT_NOEXCEPT {
        reg->template reset<component_wrapper<Component>>(entity);
        handlers.erase(reg->template type<Component>());
    }

    /**
     * @brief Checks if a prototype owns all the given components.
     * @tparam Component Components for which to perform the check.
     * @return True if the prototype owns all the components, false otherwise.
     */
    template<typename... Component>
    bool has() const ENTT_NOEXCEPT {
        return reg->template has<component_wrapper<Component>...>(entity);
    }

    /**
     * @brief Returns references to the given components.
     *
     * @warning
     * Attempting to get a component from a prototype that doesn't own it
     * results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode if the
     * prototype doesn't own an instance of the given component.
     *
     * @tparam Component Types of components to get.
     * @return References to the components owned by the prototype.
     */
    template<typename... Component>
    decltype(auto) get() const ENTT_NOEXCEPT {
        if constexpr(sizeof...(Component) == 1) {
            return (std::as_const(*reg).template get<component_wrapper<Component...>>(entity).component);
        } else {
            return std::tuple<std::add_const_t<Component> &...>{get<Component>()...};
        }
    }

    /*! @copydoc get */
    template<typename... Component>
    inline decltype(auto) get() ENTT_NOEXCEPT {
        if constexpr(sizeof...(Component) == 1) {
            return (const_cast<Component &>(std::as_const(*this).template get<Component>()), ...);
        } else {
            return std::tuple<Component &...>{get<Component>()...};
        }
    }

    /**
     * @brief Returns pointers to the given components.
     * @tparam Component Types of components to get.
     * @return Pointers to the components owned by the prototype.
     */
    template<typename... Component>
    auto try_get() const ENTT_NOEXCEPT {
        if constexpr(sizeof...(Component) == 1) {
            const auto *wrapper = reg->template try_get<component_wrapper<Component...>>(entity);
            return wrapper ? &wrapper->component : nullptr;
        } else {
            return std::tuple<std::add_const_t<Component> *...>{try_get<Component>()...};
        }
    }

    /*! @copydoc try_get */
    template<typename... Component>
    inline auto try_get() ENTT_NOEXCEPT {
        if constexpr(sizeof...(Component) == 1) {
            return (const_cast<Component *>(std::as_const(*this).template try_get<Component>()), ...);
        } else {
            return std::tuple<Component *...>{try_get<Component>()...};
        }
    }

    /**
     * @brief Creates a new entity using a given prototype.
     *
     * Utility shortcut, equivalent to the following snippet:
     *
     * @code{.cpp}
     * const auto entity = registry.create();
     * prototype(registry, entity);
     * @endcode
     *
     * @note
     * The registry may or may not be different from the one already used by
     * the prototype. There is also an overload that directly uses the
     * underlying registry.
     *
     * @param other A valid reference to a registry.
     * @return A valid entity identifier.
     */
    entity_type create(registry_type &other) const {
        const auto entity = other.create();
        assign(other, entity);
        return entity;
    }

    /**
     * @brief Creates a new entity using a given prototype.
     *
     * Utility shortcut, equivalent to the following snippet:
     *
     * @code{.cpp}
     * const auto entity = registry.create();
     * prototype(entity);
     * @endcode
     *
     * @note
     * This overload directly uses the underlying registry as a working space.
     * Therefore, the components of the prototype and of the entity will share
     * the same registry.
     *
     * @return A valid entity identifier.
     */
    inline entity_type create() const {
        return create(*reg);
    }

    /**
     * @brief Assigns the components of a prototype to a given entity.
     *
     * Assigning a prototype to an entity won't overwrite existing components
     * under any circumstances.<br/>
     * In other words, only those components that the entity doesn't own yet are
     * copied over. All the other components remain unchanged.
     *
     * @note
     * The registry may or may not be different from the one already used by
     * the prototype. There is also an overload that directly uses the
     * underlying registry.
     *
     * @warning
     * Attempting to use an invalid entity results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode in case of
     * invalid entity.
     *
     * @param other A valid reference to a registry.
     * @param dst A valid entity identifier.
     */
    void assign(registry_type &other, const entity_type dst) const {
        for(auto &handler: handlers) {
            handler.second.assign(*this, other, dst);
        }
    }

    /**
     * @brief Assigns the components of a prototype to a given entity.
     *
     * Assigning a prototype to an entity won't overwrite existing components
     * under any circumstances.<br/>
     * In other words, only those components that the entity doesn't own yet are
     * copied over. All the other components remain unchanged.
     *
     * @note
     * This overload directly uses the underlying registry as a working space.
     * Therefore, the components of the prototype and of the entity will share
     * the same registry.
     *
     * @warning
     * Attempting to use an invalid entity results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode in case of
     * invalid entity.
     *
     * @param dst A valid entity identifier.
     */
    inline void assign(const entity_type dst) const {
        assign(*reg, dst);
    }

    /**
     * @brief Assigns or replaces the components of a prototype for an entity.
     *
     * Existing components are overwritten, if any. All the other components
     * will be copied over to the target entity.
     *
     * @note
     * The registry may or may not be different from the one already used by
     * the prototype. There is also an overload that directly uses the
     * underlying registry.
     *
     * @warning
     * Attempting to use an invalid entity results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode in case of
     * invalid entity.
     *
     * @param other A valid reference to a registry.
     * @param dst A valid entity identifier.
     */
    void assign_or_replace(registry_type &other, const entity_type dst) const {
        for(auto &handler: handlers) {
            handler.second.assign_or_replace(*this, other, dst);
        }
    }

    /**
     * @brief Assigns or replaces the components of a prototype for an entity.
     *
     * Existing components are overwritten, if any. All the other components
     * will be copied over to the target entity.
     *
     * @note
     * This overload directly uses the underlying registry as a working space.
     * Therefore, the components of the prototype and of the entity will share
     * the same registry.
     *
     * @warning
     * Attempting to use an invalid entity results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode in case of
     * invalid entity.
     *
     * @param dst A valid entity identifier.
     */
    inline void assign_or_replace(const entity_type dst) const {
        assign_or_replace(*reg, dst);
    }

    /**
     * @brief Assigns the components of a prototype to an entity.
     *
     * Assigning a prototype to an entity won't overwrite existing components
     * under any circumstances.<br/>
     * In other words, only the components that the entity doesn't own yet are
     * copied over. All the other components remain unchanged.
     *
     * @note
     * The registry may or may not be different from the one already used by
     * the prototype. There is also an overload that directly uses the
     * underlying registry.
     *
     * @warning
     * Attempting to use an invalid entity results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode in case of
     * invalid entity.
     *
     * @param other A valid reference to a registry.
     * @param dst A valid entity identifier.
     */
    inline void operator()(registry_type &other, const entity_type dst) const ENTT_NOEXCEPT {
        assign(other, dst);
    }

    /**
     * @brief Assigns the components of a prototype to an entity.
     *
     * Assigning a prototype to an entity won't overwrite existing components
     * under any circumstances.<br/>
     * In other words, only the components that the entity doesn't own yet are
     * copied over. All the other components remain unchanged.
     *
     * @note
     * This overload directly uses the underlying registry as a working space.
     * Therefore, the components of the prototype and of the entity will share
     * the same registry.
     *
     * @warning
     * Attempting to use an invalid entity results in undefined behavior.<br/>
     * An assertion will abort the execution at runtime in debug mode in case of
     * invalid entity.
     *
     * @param dst A valid entity identifier.
     */
    inline void operator()(const entity_type dst) const ENTT_NOEXCEPT {
        assign(*reg, dst);
    }

    /**
     * @brief Creates a new entity using a given prototype.
     *
     * Utility shortcut, equivalent to the following snippet:
     *
     * @code{.cpp}
     * const auto entity = registry.create();
     * prototype(registry, entity);
     * @endcode
     *
     * @note
     * The registry may or may not be different from the one already used by
     * the prototype. There is also an overload that directly uses the
     * underlying registry.
     *
     * @param other A valid reference to a registry.
     * @return A valid entity identifier.
     */
    inline entity_type operator()(registry_type &other) const ENTT_NOEXCEPT {
        return create(other);
    }

    /**
     * @brief Creates a new entity using a given prototype.
     *
     * Utility shortcut, equivalent to the following snippet:
     *
     * @code{.cpp}
     * const auto entity = registry.create();
     * prototype(entity);
     * @endcode
     *
     * @note
     * This overload directly uses the underlying registry as a working space.
     * Therefore, the components of the prototype and of the entity will share
     * the same registry.
     *
     * @return A valid entity identifier.
     */
    inline entity_type operator()() const ENTT_NOEXCEPT {
        return create(*reg);
    }

    /**
     * @brief Returns a reference to the underlying registry.
     * @return A reference to the underlying registry.
     */
    inline const registry_type & backend() const ENTT_NOEXCEPT {
        return *reg;
    }

    /*! @copydoc backend */
    inline registry_type & backend() ENTT_NOEXCEPT {
        return const_cast<registry_type &>(std::as_const(*this).backend());
    }

private:
    std::unordered_map<component_type, component_handler> handlers;
    registry<Entity> *reg;
    entity_type entity;
};


}


#endif // ENTT_ENTITY_PROTOTYPE_HPP
