#pragma once
#include <cstdint>
#include <format>
#include <queue>
#include <stdexcept>
#include <typeindex>
#include <unordered_map>
#include <unordered_set>
#include <set>

namespace zecsy 
{
    using entity_id = uint64_t;

    /*
     * unordered_set is also an option. But probably sorting ids may be 
     * beneficial for queries, because it might increase the chance of reusing 
     * cpu cache (aka "required components were close to each other"). Definitely
     * should play around with it 
     */
    using entities_set = std::set<entity_id>; 

    class component_storage final
    {
    public:
        template<typename T>
        bool has(entity_id e) const;

        template<typename First, typename Second, typename... Rest>
        bool has(entity_id e) const;

        template<typename T>
        T& get(entity_id e);

        template<typename T>
        void set(entity_id e);
        
        template<typename T>
        void set(entity_id e, const T& comp);
 
        template<typename First, typename Second, typename... Rest>
        void set(entity_id e);

        template<typename First, typename Second, typename... Rest>
        void set(entity_id e, const First& comp0, const Second& comp1, 
                const Rest&... rest_comps);
        
        template<typename T>
        void remove(entity_id e);
    
        template<typename First, typename Second, typename... Rest>
        void remove(entity_id e);
    private:
        using comp_index = std::type_index;
        using comp_to_entities_set = std::unordered_map<comp_index, entities_set>;
        using entity_to_index = std::unordered_map<entity_id, size_t>;

        using comp_to_entity_dict = std::unordered_map<comp_index, 
              entity_to_index>;
        
        using comp_to_storage = std::unordered_map<comp_index, 
              std::vector<uint8_t>>;

        using comp_to_reusable_ids = std::unordered_map<comp_index, 
              std::queue<size_t>>;

        comp_to_entities_set entities_dict;
        comp_to_entity_dict indices_dict; 
        comp_to_storage storage_dict;
        comp_to_reusable_ids reusable_id_queues;
    };

    class world final
    {
    public:
        entity_id make_entity();
        void destroy_entity(entity_id e);
        bool is_alive(entity_id e) const; 

        template<typename... T>
        bool has(entity_id e) const;

        template<typename T>
        T& get(entity_id e);

        template<typename... T>
        void set(entity_id e);
        
        template<typename... T>
        void set(entity_id e, const T&... comps);

        template<typename... T>
        void remove(entity_id e);

        template<typename... T, typename Callable>
        void query(Callable&& system);
    private:
        entities_set alive_entities;
        entity_id entity_counter = 0;
        component_storage storage;
    };

    inline entity_id world::make_entity()
    {
        auto id = ++entity_counter;
        alive_entities.emplace(id);

        return id;
    }

    inline void world::destroy_entity(entity_id e)
    {
        alive_entities.erase(e);
    }

    inline bool world::is_alive(entity_id e) const
    {
        return alive_entities.contains(e);
    }

    template<typename T>
    inline bool component_storage::has(entity_id e) const
    {
        if(entities_dict.contains(typeid(T)))
        {
            return entities_dict.at(typeid(T)).contains(e);
        }
        return false;
    }

    template<typename T>
    inline T& component_storage::get(entity_id e)
    {
        if(!has<T>(e))
        {
            throw std::runtime_error(std::format("Entity #{} doesn't have {}",
                        e, typeid(T).name()));
        }

        auto index = indices_dict[typeid(T)].at(e);
        
        auto* ptr = reinterpret_cast<T*>(&storage_dict[typeid(T)][0]);
        ptr += index;

        return *ptr;
    }

    template<typename T>
    inline T& world::get(entity_id e)
    {
        return storage.get<T>(e);
    }

    template<typename T>
    inline void component_storage::set(entity_id e, const T& comp)
    {
        entities_dict[typeid(T)].emplace(e);

        auto& storage = storage_dict[typeid(T)];

        auto& reusable_ids = reusable_id_queues[typeid(T)];

        if(reusable_ids.empty())
        {
            size_t T_size = sizeof(T);
            size_t old_size = storage.size();

            storage.resize(old_size + T_size);
            void* ptr = &storage[0] + old_size;
            new(ptr) T(comp);

            indices_dict[typeid(T)][e] = old_size / T_size;
            return;
        }

        auto index = reusable_ids.front();
        reusable_ids.pop();

        auto ptr = reinterpret_cast<T*>(&storage[0]);
        new(ptr + index) T(comp);

        indices_dict[typeid(T)][e] = index;
    }
    
    template<typename T>
    inline void component_storage::set(entity_id e)
    {
        set(e, T{});
    }

    template<typename... T>
    inline void world::set(entity_id e)
    {
        storage.set<T...>(e);
    }

    template<typename... T>
    inline void world::set(entity_id e, const T&... comps)
    {
        storage.set(e, comps...);
    }

    template<typename T>
    inline void component_storage::remove(entity_id e)
    {
        if(!has<T>(e))
        {
            return;
        }

        entities_dict[typeid(T)].erase(e);
        reusable_id_queues[typeid(T)].push(indices_dict[typeid(T)][e]);
        indices_dict[typeid(T)].erase(e);
    }

    template<typename... T>
    inline void world::remove(entity_id e)
    {
        storage.remove<T...>(e);
    }

    template<typename... T>
    inline bool world::has(entity_id e) const
    {
        return storage.has<T...>(e);
    }

    template<typename First, typename Second, typename... Rest>
    inline bool component_storage::has(entity_id e) const
    {
        return has<First>(e) && has<Second>(e) && (has<Rest>(e) && ...);
    }
    
    template<typename First, typename Second, typename... Rest>
    inline void component_storage::set(entity_id e)
    {
        set(e, First{});
        set(e, Second{});
        (set(e, Rest{}), ...);
    }

    template<typename First, typename Second, typename... Rest>
    inline void component_storage::set(entity_id e, const First& comp0, const Second& comp1, 
            const Rest&... rest_comps)
    {
        set(e, comp0);
        set(e, comp1);
        (set(e, rest_comps), ...);
    }

    template<typename First, typename Second, typename... Rest>
    inline void component_storage::remove(entity_id e)
    {
        remove<First>(e);
        remove<Second>(e);
        (remove<Rest>(e), ...);
    }

    template<typename... T, typename Callable>
    inline void world::query(Callable&& system)
    {
        for(auto e: alive_entities)
        {
            if((has<T...>(e)))
            {
                system(get<T>(e)...);
            }
        }
    }
};