/***************************************************************************
*                                                                          *
*  Copyright (C) Golaem S.A.  All Rights Reserved.                         *
*                                                                          *
***************************************************************************/

#pragma once

#include "glmCommon.h"
#include "glmMutex.h"
#include "glmScopedLock.h"
#include "glmThreading.h"
#include "glmArray.h"
#include <glmLog.h>

#define GLM_CACHEPERTHREAD_INTERLOCK_CHECK 0

namespace glm
{
	template<class T>
	class CachePerThread;

	//-------------------------------------------------------------------------
	// Any instance of this class will keep the _data locked untill destruction
	template<class T>
	class LockedThreadData
	{
	public:
		friend class CachePerThread<T>;

		GLM_CLASS_STAT_ID( glm::MemoryStatIds::GolaemShared);

		LockedThreadData();
		LockedThreadData(T& data, atomic32_t& lock, uint64_t& threadId);
		LockedThreadData(const LockedThreadData& source);
		~LockedThreadData();

		T& getData() { return *_data; }

	private:
		T* _data;
		atomic32_t* _lock;
		uint64_t* _threadId;	//only used for safe check purposes

		void setDataNoLock(T& data, atomic32_t& lock, uint64_t& threadId);	//only the friend CachePerThread should set data
	};

	//-------------------------------------------------------------------------
	template<class T>
	class CachePerThread
	{
	public:

		GLM_CLASS_STAT_ID( glm::MemoryStatIds::GolaemShared);

		CachePerThread(MemAllocStatId statId);
		~CachePerThread();

		void reset();

		void getCacheForCurrentThread(LockedThreadData<T>& retLockedThreadData) const;

		bool isValid() const;

		void allocateEntries(const int threadCount);
		void deallocateEntries();

		mutable glm::Array< T > _entries;

	private:

		void resizeEntry(size_t atLeastSize) const;
		mutable glm::SpinLock _resizeLock;

		size_t _capacity;
		atomic32_t* _entriesLock;
		uint64_t* _entriesThreadIds;

		CachePerThread(const CachePerThread&);
        CachePerThread& operator=(const CachePerThread&);
	};

	//-------------------------------------------------------------------------
	template<class T>
	LockedThreadData<T>::LockedThreadData()
		: _data(NULL)
		, _lock(NULL)
		, _threadId(NULL)
	{}

	//-------------------------------------------------------------------------
	template<class T>
	LockedThreadData<T>::LockedThreadData(T& data, atomic32_t& lock, uint64_t& threadId)
		: _data(&data)
		, _lock(&lock)
		, _threadId(&threadId)
	{}

	//-------------------------------------------------------------------------
	template<class T>
	LockedThreadData<T>::LockedThreadData(const LockedThreadData& source)
		: _data(source._data)
		, _lock(source._lock)
		, _threadId(source._threadId)
	{
		if(_lock!=NULL)
			atomicFetchAddRelaxed<atomic32_t>(*_lock, 1);
	}

	//-------------------------------------------------------------------------
	template<class T>
	LockedThreadData<T>::~LockedThreadData()
	{
		if (_lock != NULL)
		{
			atomic32_t::value_t previousValue = atomicFetchAddRelaxed<atomic32_t>(*_lock, -1);	//unlock the referenced data
			if (previousValue == 1)	//reset _threadId
				(*_threadId) = 0;
		}
	}

	//-------------------------------------------------------------------------
	template<class T>
	void LockedThreadData<T>::setDataNoLock(T& data, atomic32_t& lock, uint64_t& threadId)
	{
		//unlock self if a previous valid value exists
		if (_lock != NULL)
		{
			atomic32_t::value_t previousValue = atomicFetchAddRelaxed<atomic32_t>(*_lock, -1);	//unlock the current referenced data
			if (previousValue == 1)	//reset _threadId
				(*_threadId) = 0;
		}

		_data = &data;
		_lock = &lock;
		_threadId = &threadId;
	}

	//-------------------------------------------------------------------------
	template<class T>
	CachePerThread<T>::CachePerThread(MemAllocStatId statId)
		: _entries(statId)
		, _capacity(0)
		, _entriesLock(NULL)
	{
		GLM_DEBUG_ASSERT(_entries.capacity() == 0);
		//HACK: simply allocating enough caches for all possible threads
		allocateEntries( glm::getSupportedConcurrentThreadCount()+1 );
	}

	template<class T>
	CachePerThread<T>::~CachePerThread()
	{
		//HACK: caches should have been deallocated before destruction
		//		GLM_DEBUG_ASSERT( _entries.capacity() == 0 );
		deallocateEntries();
	}

	//-------------------------------------------------------------------------
	template<class T>
	bool CachePerThread<T>::isValid() const
	{
		return (_entries.capacity() > 0);
	}

	//-------------------------------------------------------------------------
	template<class T>
	void CachePerThread<T>::reset()
	{
		if (_entries.empty() == false)
		{
			_entries.clear();
		}
	}

	//-------------------------------------------------------------------------
	template<class T>
	void CachePerThread<T>::allocateEntries(const int threadCount)
	{
		GLM_DEBUG_ASSERT(_entries.capacity() == 0);
		_entries.reserve(threadCount);
		_entriesLock = GLM_ALLOC_CAST(atomic32_t*, sizeof(atomic32_t)*threadCount, _entriesLock, _entries.getMemoryStatId())
		_entriesThreadIds = GLM_ALLOC_CAST(uint64_t*, sizeof(uint64_t)*threadCount, _entriesThreadIds, _entries.getMemoryStatId())
		_capacity = threadCount;
		const atomic32_t::value_t freeValue = 0;
		for (size_t i = 0; i < _capacity; ++i)
		{
			atomicStoreRelease(_entriesLock[i], freeValue);
			_entriesThreadIds[i] = 0;
		}
	}

	//-------------------------------------------------------------------------
	template<class T>
	void CachePerThread<T>::deallocateEntries()
	{
		GLM_DEBUG_ASSERT(_entries.capacity() > 0);

		//HACK: entries should have been reset before deallocation
		//GLM_DEBUG_ASSERT( _entries.empty() );
		_capacity = 0;
		_entries.clear();
		_entries.set_capacity(0);

		GLM_FREE( _entriesLock,  _entries.getMemoryStatId());
		GLM_FREE( _entriesThreadIds,  _entries.getMemoryStatId());
	}

	//-------------------------------------------------------------------------
	template<class T>
	void CachePerThread<T>::resizeEntry(size_t atLeastSize) const
	{
		// insure other thread will not get same entry for a new :
		ScopedLock<glm::SpinLock> lock(_resizeLock);

		if (_entries.size()<atLeastSize)
			_entries.resize(atLeastSize);

		GLM_ASSERT(_entries.size()<=_capacity);
	}

	//-------------------------------------------------------------------------
	template<class T>
	void CachePerThread<T>::getCacheForCurrentThread(LockedThreadData<T>& retLockedThreadData) const
	{
		GLM_DEBUG_ASSERT(isValid());

		//find the next available cache
		const atomic32_t::value_t freeValue = 0;
		const atomic32_t::value_t lockedValue = 1;
		int freeCacheIndex(-1);
		do
		{
			for (size_t i = 0; i < _capacity; ++i)
			{
				if (atomicCompareExchangeRelaxed(_entriesLock[i], lockedValue, freeValue) == freeValue)
				{
					freeCacheIndex = (int)i;
					_entriesThreadIds[i] = glm::convertThreadId(glm::getCurrentThreadId());
					break;
				}
#if GLM_CACHEPERTHREAD_INTERLOCK_CHECK
				else if (i<_entries.size())	//check if the entry is already reserved for the SAME thread...
				{
					if (_entriesThreadIds[i] == glm::convertThreadId(glm::getCurrentThreadId()))
						GLM_SDK_TRACE_ERROR("CachePerThread called again from the same thread ID. May lead to a multithread lock. Please contact support@golaem.com");
				}
#endif
			}
			if (freeCacheIndex<0)
				GLM_SDK_TRACE_WARNING("All CachePerThread are already in use while asking for a new one. This should not happen and might be the result of wrongly used CachePerThread. Please contact support@golaem.com");
		} while (freeCacheIndex<0);

		if (freeCacheIndex >= (int)_entries.size())
			resizeEntry(freeCacheIndex+1);
		GLM_DEBUG_ASSERT(freeCacheIndex < (int)_entries.size());

		retLockedThreadData.setDataNoLock(_entries[freeCacheIndex], _entriesLock[freeCacheIndex], _entriesThreadIds[freeCacheIndex]);
	}
}
