module bubel.ecs.events;

import bubel.ecs.block_allocator;
import bubel.ecs.entity;
import bubel.ecs.manager;
import bubel.ecs.std;

import std.algorithm.comparison : max;

package struct EventManager
{

    void initialize(EntityManager* m) nothrow @nogc
    {
        allocator = BlockAllocator(events_block_size, events_blocks_in_allocation);
        event_block_alloc_mutex = Mallocator.make!Mutex;
        event_block_alloc_mutex.initialize();
        manager = m;
    }

    void destroy() nothrow @nogc
    {
        if(event_block_alloc_mutex)
        {
            event_block_alloc_mutex.destroy();
            Mallocator.dispose(event_block_alloc_mutex);
            event_block_alloc_mutex = null;
        }
    }

    export void sendEvent(Ev)(EntityID id, Ev event, uint thread_id = 0) nothrow @nogc
    {
        uint block_id = current_index+thread_id;

        EventData* data = &events[Ev.event_id];
        EventBlock* block = data.blocks[block_id];
        //EntityManager.EventInfo* info = &manager.events[Ev.event_id];
        event.entity_id = id;

        if(block is null)
        {
            event_block_alloc_mutex.lock();
            scope (exit)
                event_block_alloc_mutex.unlock();

            block = cast(EventBlock*) allocator.getBlock();
            *block = EventBlock();
            data.first_blocks[block_id] = block;
            data.blocks[block_id] = block;
        }

        if(block.count >= data.max_events)
        {
            event_block_alloc_mutex.lock();
            scope (exit)
                event_block_alloc_mutex.unlock();

            EventBlock* new_block = cast(EventBlock*) allocator.getBlock();
            *new_block = EventBlock();
            block.next = new_block;
            block = new_block;
            data.blocks[block_id] = block;
        }

        Ev* event_array = cast(Ev*)(cast(void*)block + data.data_offset);
        event_array[block.count] = event;
        block.count++;
    }

    void swapCurrent() nothrow @nogc
    {
        uint threads_count = cast(uint)manager.threads.length;
        if(current_index == 0)current_index = threads_count;
        else current_index = 0;

        foreach(ref event;events)
        {
            foreach(ref first_block; event.first_blocks[current_index .. current_index + threads_count])
            {
                EventBlock* block = first_block;
                while(block)
                {
                    EventBlock* to_dispose = block;
                    block = block.next;
                    allocator.freeBlock(to_dispose);
                }
                first_block = null;
            }
            foreach(ref block; event.blocks[current_index .. current_index + threads_count])
            {
                block = null;
            }
        }
    }

    void clearEvents() nothrow @nogc
    {
        //uint threads_count = cast(uint)manager.threads.length;
        foreach(ref event;events)
        {
            foreach(ref first_block; event.first_blocks)
            {
                EventBlock* block = first_block;
                while(block)
                {
                    EventBlock* to_dispose = block;
                    block = block.next;
                    allocator.freeBlock(to_dispose);
                }
                first_block = null;
            }
            foreach(ref block; event.blocks)
            {
                block = null;
            }
        }
    }

    void allocateData(uint threads_count) nothrow @nogc
    {
        disposeData();
        events = Mallocator.makeArray!EventData(manager.events.length);
        foreach(i,ref event;events)
        {
            event.blocks = Mallocator.makeArray!(EventBlock*)(threads_count*2);
            event.first_blocks = Mallocator.makeArray!(EventBlock*)(threads_count*2);
            event.data_offset = EventBlock.sizeof;//manager.events[i].
            manager.alignNum(event.data_offset, manager.events[i].alignment);

            event.max_events = cast(ushort)((events_block_size - event.data_offset) / manager.events[i].size);
        }
    }

    private void disposeData() nothrow @nogc
    {
        clearEvents();
        if(events)
        {
            foreach(ref event;events)
            {
                Mallocator.dispose(event.blocks);
                Mallocator.dispose(event.first_blocks);
            }
            Mallocator.dispose(events);
        }
        allocator.freeMemory();
    }

    ~this() nothrow @nogc
    {
        disposeData();
    }

    ///Single page size. Must be power of two.
    enum events_block_size = 1 << 14;
    ///Number of pages in block.
    enum events_blocks_in_allocation = 128;

    struct EventBlock
    {
        EventBlock* next;
        ushort count = 0;
    }

    struct EventData
    {
        ushort data_offset;
        ushort max_events;
        EventBlock*[] blocks;
        EventBlock*[] first_blocks;
        
        //EventBlock*[] current_blocks;
    }

    uint current_index = 0;
    EventData[] events;
    Mutex* event_block_alloc_mutex;

    BlockAllocator allocator;
    EntityManager* manager;
}
