/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com.                */
#include "ringbuffer.h"

CRingBufferBase::CItem *CRingBufferBase::NextBlock(CItem *pItem)
{
	if(pItem->m_pNext)
		return pItem->m_pNext;
	return m_pFirst;
}

CRingBufferBase::CItem *CRingBufferBase::PrevBlock(CItem *pItem)
{
	if(pItem->m_pPrev)
		return pItem->m_pPrev;
	return m_pLast;
}

CRingBufferBase::CItem *CRingBufferBase::MergeBack(CItem *pItem)
{
	// make sure that this block and previous block is free
	if(!pItem->m_Free || !pItem->m_pPrev || !pItem->m_pPrev->m_Free)
		return pItem;

	// merge the blocks
	pItem->m_pPrev->m_Size += pItem->m_Size;
	pItem->m_pPrev->m_pNext = pItem->m_pNext;

	// fixup pointers
	if(pItem->m_pNext)
		pItem->m_pNext->m_pPrev = pItem->m_pPrev;
	else
		m_pLast = pItem->m_pPrev;

	if(pItem == m_pProduce)
		m_pProduce = pItem->m_pPrev;

	if(pItem == m_pConsume)
		m_pConsume = pItem->m_pPrev;

	// return the current block
	return pItem->m_pPrev;
}

void CRingBufferBase::Init(void *pMemory, int Size, int Flags)
{
	m_Size = Size / sizeof(CItem) * sizeof(CItem);
	m_pBuffer = static_cast<CItem *>(pMemory);
	m_Flags = Flags;
	Clear();
}

void CRingBufferBase::Clear()
{
	m_pFirst = m_pBuffer;
	m_pFirst->m_pPrev = nullptr;
	m_pFirst->m_pNext = nullptr;
	m_pFirst->m_Free = 1;
	m_pFirst->m_Size = m_Size;
	m_pLast = m_pFirst;
	m_pProduce = m_pFirst;
	m_pConsume = m_pFirst;
}

void *CRingBufferBase::Allocate(int Size)
{
	int WantedSize = (Size + sizeof(CItem) + sizeof(CItem) - 1) / sizeof(CItem) * sizeof(CItem);
	CItem *pBlock = nullptr;

	// check if we even can fit this block
	if(WantedSize > m_Size)
		return nullptr;

	while(true)
	{
		// check for space
		if(m_pProduce->m_Free)
		{
			if(m_pProduce->m_Size >= WantedSize)
				pBlock = m_pProduce;
			else
			{
				// wrap around to try to find a block
				if(m_pFirst->m_Free && m_pFirst->m_Size >= WantedSize)
					pBlock = m_pFirst;
			}
		}

		if(pBlock)
			break;
		else
		{
			// we have no block, check our policy and see what todo
			if(m_Flags & FLAG_RECYCLE)
			{
				if(!PopFirst())
					return nullptr;
			}
			else
				return nullptr;
		}
	}

	// okey, we have our block

	// split the block if needed
	if(pBlock->m_Size > WantedSize + (int)sizeof(CItem))
	{
		CItem *pNewItem = (CItem *)((char *)pBlock + WantedSize);
		pNewItem->m_pPrev = pBlock;
		pNewItem->m_pNext = pBlock->m_pNext;
		if(pNewItem->m_pNext)
			pNewItem->m_pNext->m_pPrev = pNewItem;
		pBlock->m_pNext = pNewItem;

		pNewItem->m_Free = 1;
		pNewItem->m_Size = pBlock->m_Size - WantedSize;
		pBlock->m_Size = WantedSize;

		if(!pNewItem->m_pNext)
			m_pLast = pNewItem;
	}

	// set next block
	m_pProduce = NextBlock(pBlock);

	// set as used and return the item pointer
	pBlock->m_Free = 0;
	return (void *)(pBlock + 1);
}

void CRingBufferBase::SetPopCallback(std::function<void(void *pCurrent)> PopCallback)
{
	m_PopCallback = std::move(PopCallback);
}

int CRingBufferBase::PopFirst()
{
	if(m_pConsume->m_Free)
		return 0;

	if(m_PopCallback)
	{
		m_PopCallback(m_pConsume + 1);
	}

	// set the free flag
	m_pConsume->m_Free = 1;

	// previous block is also free, merge them
	m_pConsume = MergeBack(m_pConsume);

	// advance the consume pointer
	m_pConsume = NextBlock(m_pConsume);
	while(m_pConsume->m_Free && m_pConsume != m_pProduce)
	{
		m_pConsume = MergeBack(m_pConsume);
		m_pConsume = NextBlock(m_pConsume);
	}

	// in the case that we have caught up with the produce pointer
	// we might stand on a free block so merge em
	MergeBack(m_pConsume);
	return 1;
}

void *CRingBufferBase::Prev(void *pCurrent)
{
	CItem *pItem = ((CItem *)pCurrent) - 1;

	while(true)
	{
		pItem = PrevBlock(pItem);
		if(pItem == m_pProduce)
			return nullptr;
		if(!pItem->m_Free)
			return pItem + 1;
	}
}

void *CRingBufferBase::Next(void *pCurrent)
{
	CItem *pItem = ((CItem *)pCurrent) - 1;

	while(true)
	{
		pItem = NextBlock(pItem);
		if(pItem == m_pProduce)
			return nullptr;
		if(!pItem->m_Free)
			return pItem + 1;
	}
}

void *CRingBufferBase::First()
{
	if(m_pConsume->m_Free)
		return nullptr;
	return (void *)(m_pConsume + 1);
}

void *CRingBufferBase::Last()
{
	return Prev(m_pProduce + 1);
}
