module bubel.ecs.id_manager;

import bubel.ecs.entity;
import bubel.ecs.std;
import bubel.ecs.vector;

import bubel.ecs.atomic;
import core.stdc.string : memcpy;

/************************************************************************************************************************
IDManager is responsible for assignment and removing IDs. IDs are unique throughtout a whole duration of the program.
*/
struct IDManager
{
    /************************************************************************************************************************
    Get new ID.
    */
    pragma(inline, false) EntityID getNewID() nothrow @nogc
    {
        int current = m_stack_top.atomicOp!"-="(1) + 1;
        if(current < 0)
        {
            uint add_id = m_last_id.atomicOp!"+="(1) - 1;

            if (add_id >= m_ids_array.length)
            {
                uint local_id = add_id - cast(uint) m_ids_array.length;
                uint block_id = local_id >> 16;
                if (block_id >= m_blocks_count)
                {
                    add_mutex.lock();
                    if(block_id >= m_blocks_count)
                    {
                        m_blocks[m_blocks_count].alloc();
                        m_blocks_count++;
                    }
                    add_mutex.unlock();
                }
            }

            EntityID id;
            id.id = add_id;
            id.counter = 0;
            return id;
        }

        uint index = m_free_stack[current];
        EntityID id;
        id.id = index;
        id.counter = m_ids_array[index].counter;
        return id;
    }

    /************************************************************************************************************************
    Release ID.
    */
    void releaseID(EntityID id) nothrow @nogc
    {
        Data* data = &m_ids_array[id.id];
        if (data.counter != id.counter)
            return;
        data.counter++;
        data.entity = null;

        m_stack_top.atomicOp!"+="(1);
        m_free_stack[m_stack_top] = id.id;
    }

    /************************************************************************************************************************
    Update pointer to entity. The purpose of this function is to ensure that pointer to entity is always correct.
    */
    void update(ref Entity entity) nothrow @nogc
    {
        if (entity.id.id >= cast(uint) m_ids_array.length)
        {
            uint local_id = entity.id.id - cast(uint) m_ids_array.length;
            uint block_id = local_id >> 16;
            local_id -= block_id << 16;
            m_blocks[block_id].data[local_id].entity = &entity;
        }
        else //if (entity.id.counter == m_ids_array[entity.id.id].counter)
            m_ids_array[entity.id.id].entity = &entity;
    }

    /************************************************************************************************************************
    Returns pointer to entity.
    */
    export Entity* getEntityPointer(EntityID id) nothrow @nogc
    {
        if (id.id >= m_ids_array.length)
        {
            uint local_id = id.id - cast(uint) m_ids_array.length;
            uint block_id = local_id >> 16;
            assert(block_id < m_blocks_count);
            if (block_id >= m_blocks_count)
                return null;
            local_id -= block_id << 16;
            if (m_blocks[block_id].data[local_id].counter != id.counter)
                return null;
            return m_blocks[block_id].data[local_id].entity;
        }

        Data* data = &m_ids_array[id.id];
        if (data.counter != id.counter)
            return null;
        else
            return data.entity;
    }

    /************************************************************************************************************************
    Check if entity with specified ID exist.
    */
    export bool isExist(EntityID id) nothrow @nogc
    {
        if(id.id >= m_ids_array.length)return false;
        Data* data = &m_ids_array[id.id];
        if(data.entity is null)return false;
        return data.counter == id.counter;
    }

    /************************************************************************************************************************
    Initialize manager.
    */
    void initialize() nothrow @nogc
    {
        m_ids_array = Mallocator.makeArray!Data(65536);
        m_free_stack = Mallocator.makeArray!uint(65536);
        m_blocks = Mallocator.makeArray!Block(64);
        foreach(ref block;m_blocks)block = Block();
        m_blocks_count = 1;
        m_blocks[0].alloc();

        add_mutex = Mallocator.make!Mutex();
        add_mutex.initialize();

        getNewID();
        optimize();
    }

    /************************************************************************************************************************
    Free manager memory.
    */
    void deinitialize() @trusted @nogc nothrow
    {
        if(m_ids_array)Mallocator.dispose(m_ids_array);
        if(m_free_stack)Mallocator.dispose(m_free_stack);
        if(m_blocks)
        {
            foreach(ref block;m_blocks)
            {
                if(block.data)block.free();
            }
            Mallocator.dispose(m_blocks);
        }
        if(add_mutex)
        {
            add_mutex.destroy();
            Mallocator.dispose(add_mutex);//cast(void*)add_mutex); //workaround for compiler bug
            add_mutex = null;
        }
    }

    /************************************************************************************************************************
    Optimize memory. Must be called if any ID was added and some ID will be removed.
    */
    void optimize() nothrow @nogc
    {
        if(m_stack_top < -1)m_stack_top = -1;
        if(m_last_id > m_ids_array.length)
        {
            uint begin = cast(uint)m_ids_array.length;
            Data[] new_array = Mallocator.makeArray!Data(begin + (m_blocks_count << 16));
            memcpy(new_array.ptr, m_ids_array.ptr, m_ids_array.length * Data.sizeof);
            Mallocator.dispose(m_ids_array);
            m_ids_array = new_array;

            uint[] new_stack = Mallocator.makeArray!uint(m_ids_array.length);
            memcpy(new_stack.ptr,m_free_stack.ptr,m_free_stack.length * uint.sizeof);
            Mallocator.dispose(m_free_stack);
            m_free_stack = new_stack;

            foreach(block;m_blocks[0..m_blocks_count-1])
            {
                memcpy(cast(void*)m_ids_array.ptr + begin * Data.sizeof, block.data.ptr, 65536 * Data.sizeof);
                begin += 65536;
            }
            memcpy(cast(void*)m_ids_array.ptr + begin * Data.sizeof, m_blocks[m_blocks_count-1].data.ptr, (m_last_id - begin) * Data.sizeof);
            foreach(ref block;m_blocks[1..m_blocks_count])block.free();
            m_blocks_count = 1;
        }
    }

    private static struct Block
    {
        void alloc() nothrow @nogc
        {
            assert(data is null);
            data = Mallocator.makeArray!Data(65536);
        }

        void free() nothrow @nogc
        {
            assert(data !is null);
            Mallocator.dispose(data);
            data = null;
        }

        Data[] data = null; //65536
    }

    private static struct Data
    {
        uint counter = 0;
        //uint next_id = uint.max;
        Entity* entity = null;
    }

private:
    Mutex* add_mutex;
    
    Data[] m_ids_array = null;
    uint m_blocks_count = 0;
    Block[] m_blocks;
    uint[] m_free_stack = null;
    
    align(64) shared uint m_last_id = 0;
    align(64) shared int m_stack_top = -1;
}
