package speiger.src.coreengine.utils.collections.managers.fixed;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import java.util.concurrent.Callable;

import speiger.src.collections.ints.collections.IntCollection;
import speiger.src.collections.ints.collections.IntIterator;
import speiger.src.collections.ints.functions.IntComparator;
import speiger.src.collections.ints.lists.IntArrayList;
import speiger.src.collections.ints.lists.IntList;
import speiger.src.collections.ints.maps.impl.hash.Int2IntOpenHashMap;
import speiger.src.collections.ints.maps.interfaces.Int2IntMap;
import speiger.src.collections.ints.misc.pairs.IntObjectPair;
import speiger.src.collections.ints.sets.IntAVLTreeSet;
import speiger.src.collections.ints.sets.IntLinkedOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.collections.ints.sets.IntSortedSet;
import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.utils.ObjectArrays;
import speiger.src.collections.objects.utils.ObjectLists;
import speiger.src.coreengine.utils.collections.pools.ThreadPool;

public class FixedDataManager
{
	static final ThreadPool<FixedSlot> SLOTS = new ThreadPool<>(1000, FixedSlot::new);
	
	final int bytesPerEntry;
	final IFixedDataHandler manager;
	Int2IntMap slots = new Int2IntOpenHashMap();
	IntArrayList slotData = new IntArrayList();
	IntSortedSet freeSlots = new IntAVLTreeSet();
	IntSet changes = new IntLinkedOpenHashSet();
	
	public FixedDataManager(int bytesPerEntry, IFixedDataHandler manager)
	{
		this.bytesPerEntry = bytesPerEntry;
		this.manager = manager;
		slots.setDefaultReturnValue(-1);
	}
		
	public boolean add(int dataIndex)
	{
		if(dataIndex == -1)
		{
			throw new IllegalStateException("-1 isnt allowed to be a dataIndex");
		}
		if(slots.containsKey(dataIndex))
		{
			return false;
		}
		if(freeSlots.isEmpty())
		{
			slots.put(dataIndex, slotData.size());
			slotData.add(dataIndex);
			changes.add(dataIndex);
			return true;
		}
		int slot = freeSlots.pollFirstInt();
		slots.put(dataIndex, slot);
		slotData.set(slot, dataIndex);
		changes.add(dataIndex);
		return true;
	}
	
	public void addAll(IntCollection indexes)
	{
		for(int index : indexes)
		{
			add(index);
		}
	}
	
	public boolean contains(int dataIndex)
	{
		return slots.containsKey(dataIndex);
	}
	
	public boolean update(int dataIndex)
	{
		if(slots.containsKey(dataIndex))
		{
			changes.add(dataIndex);
			return true;
		}
		return false;
	}
	
	public void updateAll(IntCollection indexes)
	{
		for(int index : indexes)
		{
			if(slots.containsKey(index))
			{
				changes.add(index);
			}
		}
	}
	
	public boolean remove(int dataIndex)
	{
		int slot = slots.remove(dataIndex);
		if(slot >= 0 && slot < slotData.size())
		{
			slotData.set(slot, -1);
			freeSlots.add(slot);
			changes.remove(dataIndex);
			return true;
		}
		return false;
	}
	
	public void removeAll(IntCollection indexes)
	{
		for(int index : indexes)
		{
			remove(index);
		}
	}
	
	public void clear()
	{
		changes.clear();
		slots.clear();
		freeSlots.clear();
		slotData.clear();
	}
	
	public void reload(boolean now)
	{
		if(slots.isEmpty())
		{
			return;
		}
		changes.addAll(slots.keySet());
		if(now)
		{
			updateNow();
		}
	}
	
	public int size()
	{
		return slots.size();
	}
	
	public IntIterator valueIterator()
	{
		return slotData.iterator();
	}
	
	public void copy(FixedDataManager manager)
	{
		IntList list = manager.slotData;
		for(int i = 0,m=list.size();i<m;i++)
		{
			int data = list.getInt(i);
			if(data == -1)
			{
				continue;
			}
			add(data);
		}
	}
	
	public boolean sort(IntComparator comparator)
	{
		clearEmptySlots();
		if(slotData.isEmpty())
		{
			return false;
		}
		int[] oldData = slotData.toIntArray();
		slotData.sort(comparator);
		int[] newData = slotData.elements();
		boolean changed = false;
		for(int i = 0,m=slotData.size();i<m;i++)
		{
			if(oldData[i] != newData[i])
			{
				changes.add(newData[i]);
				slots.put(newData[i], i);
				changed = true;
			}
		}
		return changed;
	}
	
	public void updateNow()
	{
		try
		{
			Callable<Runnable> call = update();
			if(call != null)
			{
				call.call().run();
			}
		}
		catch(Exception e){}
	}
	
	public Callable<Runnable> update()
	{
		int lastSize = slotData.size();
		clearEmptySlots();
		if(changes.isEmpty())
		{
			if(slotData.size() != lastSize)
			{
				return new ShrinkTask(new FinishingTask(manager, ObjectLists.empty(), size()));
			}
			return null;
		}
		FixedSlot[] slots = SLOTS.get(new FixedSlot[changes.size()]);
		int index = 0;
		for(int dataIndex : changes)
		{
			slots[index++].set(dataIndex, this.slots.get(dataIndex));
		}
		changes.clear();
		return new ProcessTask(slots, size(), bytesPerEntry, manager);
	}
	
	protected void clearEmptySlots()
	{
		if(freeSlots.isEmpty())
		{
			return;
		}
		if(slots.isEmpty())
		{
			freeSlots.clear();
			slotData.clear();
			changes.clear();
			return;
		}
		int size = freeSlots.size();
		while(size > 0)
		{
			size -= ensureData();
			if(slotData.isEmpty() || freeSlots.isEmpty())
			{
				break;
			}
			int index = freeSlots.pollFirstInt();
			int dataIndex = slotData.pop();
			slotData.set(index, dataIndex);
			slots.put(dataIndex, index);
			changes.add(dataIndex);
		}
	}
	
	protected int ensureData()
	{
		int cleared = 0;
		while(slotData.top() == -1)
		{
			freeSlots.remove(slotData.size() - 1);
			slotData.pop();
			cleared++;
		}
		return cleared;
	}
	
	static class ProcessTask implements Callable<Runnable>
	{
		FixedSlot[] slots;
		int size;
		int bytes;
		IFixedDataHandler handler;
		
		public ProcessTask(FixedSlot[] slots, int size, int bytes, IFixedDataHandler handler)
		{
			this.slots = slots;
			this.size = size;
			this.bytes = bytes;
			this.handler = handler;
		}
		
		@Override
		public Runnable call()
		{
			ObjectArrays.quickSort(slots);
			List<FixedSlot> fixed = new ObjectArrayList<FixedSlot>();
			List<IntObjectPair<byte[]>> data = new ObjectArrayList<>();
			FixedSlot first = null;
			FixedSlot last = null;
			for(int i = 0,m=slots.length;i<m;i++)
			{
				if(first == null)
				{
					first = last = slots[i];
					fixed.add(first);
					continue;
				}
				if(last != null && last.isNext(slots[i]))
				{
					last = slots[i];
					fixed.add(last);
					continue;
				}
				ByteBuffer buffer = ByteBuffer.allocate(fixed.size() * bytes).order(ByteOrder.nativeOrder());
				handler.createData(buffer, fixed.size(), fixed);
				fixed.clear();
				data.add(IntObjectPair.of(first.index * bytes, buffer.array()));
				i--;
				first = last = null;
			}
			if(first != null)
			{
				ByteBuffer buffer = ByteBuffer.allocate(fixed.size() * bytes).order(ByteOrder.nativeOrder());
				handler.createData(buffer, fixed.size(), fixed);
				fixed.clear();
				data.add(IntObjectPair.of(first.index * bytes, buffer.array()));
			}
			SLOTS.accept(slots);
			return new FinishingTask(handler, data, size);
		}
	}
	
	static class ShrinkTask implements Callable<Runnable>
	{
		Runnable run;
		
		public ShrinkTask(Runnable run)
		{
			this.run = run;
		}
		
		@Override
		public Runnable call() throws Exception
		{
			return run;
		}
	}
	
	static class FinishingTask implements Runnable
	{
		IFixedDataHandler handler;
		List<IntObjectPair<byte[]>> data;
		int size;
		
		public FinishingTask(IFixedDataHandler handler, List<IntObjectPair<byte[]>> data, int size)
		{
			this.handler = handler;
			this.data = data;
			this.size = size;
		}
		
		@Override
		public void run()
		{
			handler.updateData(data, size);
		}
	}
}
