#include "Globals.h"
#include "ChunkGeneratorThread.h"
#include "Generating/ChunkGenerator.h"
#include "Generating/ChunkDesc.h"





/** If the generation queue size exceeds this number, a warning will be output */
const size_t QUEUE_WARNING_LIMIT = 1000;

/** If the generation queue size exceeds this number, chunks with no clients will be skipped */
const size_t QUEUE_SKIP_LIMIT = 500;





cChunkGeneratorThread::cChunkGeneratorThread(void) :
	Super("Chunk Generator"),
	m_Generator(nullptr),
	m_PluginInterface(nullptr),
	m_ChunkSink(nullptr)
{
}





cChunkGeneratorThread::~cChunkGeneratorThread()
{
	Stop();
}





bool cChunkGeneratorThread::Initialize(cPluginInterface & a_PluginInterface, cChunkSink & a_ChunkSink, cIniFile & a_IniFile)
{
	m_PluginInterface = &a_PluginInterface;
	m_ChunkSink = &a_ChunkSink;

	m_Generator = cChunkGenerator::CreateFromIniFile(a_IniFile);
	if (m_Generator == nullptr)
	{
		LOGERROR("Generator could not start, aborting the server");
		return false;
	}
	return true;
}





void cChunkGeneratorThread::Stop(void)
{
	m_ShouldTerminate = true;
	m_Event.Set();
	m_evtRemoved.Set();  // Wake up anybody waiting for empty queue
	Super::Stop();
	m_Generator.reset();
}





void cChunkGeneratorThread::QueueGenerateChunk(
	cChunkCoords a_Coords,
	bool a_ForceRegeneration,
	cChunkCoordCallback * a_Callback
)
{
	ASSERT(m_ChunkSink->IsChunkQueued(a_Coords));

	{
		cCSLock Lock(m_CS);

		// Add to queue, issue a warning if too many:
		if (m_Queue.size() >= QUEUE_WARNING_LIMIT)
		{
			LOGWARN("WARNING: Adding chunk %s to generation queue; Queue is too big! (%zu)", a_Coords.ToString().c_str(), m_Queue.size());
		}
		m_Queue.emplace_back(a_Coords, a_ForceRegeneration, a_Callback);
	}

	m_Event.Set();
}





void cChunkGeneratorThread::GenerateBiomes(cChunkCoords a_Coords, cChunkDef::BiomeMap & a_BiomeMap)
{
	if (m_Generator != nullptr)
	{
		m_Generator->GenerateBiomes(a_Coords, a_BiomeMap);
	}
}





void cChunkGeneratorThread::WaitForQueueEmpty(void)
{
	cCSLock Lock(m_CS);
	while (!m_ShouldTerminate && !m_Queue.empty())
	{
		cCSUnlock Unlock(Lock);
		m_evtRemoved.Wait();
	}
}





size_t cChunkGeneratorThread::GetQueueLength(void) const
{
	cCSLock Lock(m_CS);
	return m_Queue.size();
}





int cChunkGeneratorThread::GetSeed() const
{
	return m_Generator->GetSeed();
}





EMCSBiome cChunkGeneratorThread::GetBiomeAt(int a_BlockX, int a_BlockZ)
{
	ASSERT(m_Generator != nullptr);
	return m_Generator->GetBiomeAt(a_BlockX, a_BlockZ);
}





void cChunkGeneratorThread::Execute(void)
{
	// To be able to display performance information, the generator counts the chunks generated.
	// When the queue gets empty, the count is reset, so that waiting for the queue is not counted into the total time.
	int NumChunksGenerated = 0;  // Number of chunks generated since the queue was last empty
	clock_t GenerationStart = clock();  // Clock tick when the queue started to fill
	clock_t LastReportTick = clock();  // Clock tick of the last report made (so that performance isn't reported too often)

	while (!m_ShouldTerminate)
	{
		cCSLock Lock(m_CS);
		while (m_Queue.empty())
		{
			if ((NumChunksGenerated > 16) && (clock() - LastReportTick > CLOCKS_PER_SEC))
			{
				/* LOG("Chunk generator performance: %.2f ch / sec (%d ch total)",
					static_cast<double>(NumChunksGenerated) * CLOCKS_PER_SEC/ (clock() - GenerationStart),
					NumChunksGenerated
				); */
			}
			cCSUnlock Unlock(Lock);
			m_Event.Wait();
			if (m_ShouldTerminate)
			{
				return;
			}
			NumChunksGenerated = 0;
			GenerationStart = clock();
			LastReportTick = clock();
		}

		if (m_Queue.empty())
		{
			// Sometimes the queue remains empty
			// If so, we can't do any front() operations on it!
			continue;
		}

		auto item = m_Queue.front();  // Get next chunk from the queue
		bool SkipEnabled = (m_Queue.size() > QUEUE_SKIP_LIMIT);
		m_Queue.erase(m_Queue.begin());  // Remove the item from the queue
		Lock.Unlock();  // Unlock ASAP
		m_evtRemoved.Set();

		// Display perf info once in a while:
		if ((NumChunksGenerated > 512) && (clock() - LastReportTick > 2 * CLOCKS_PER_SEC))
		{
			LOG("Chunk generator performance: %.2f ch / sec (%d ch total)",
				static_cast<double>(NumChunksGenerated) * CLOCKS_PER_SEC / (clock() - GenerationStart),
				NumChunksGenerated
			);
			LastReportTick = clock();
		}

		// Skip the chunk if it's already generated and regeneration is not forced. Report as success:
		if (!item.m_ForceRegeneration && m_ChunkSink->IsChunkValid(item.m_Coords))
		{
			LOGD("Chunk %s already generated, skipping generation", item.m_Coords.ToString().c_str());
			if (item.m_Callback != nullptr)
			{
				item.m_Callback->Call(item.m_Coords, true);
			}
			continue;
		}

		// Skip the chunk if the generator is overloaded:
		if (SkipEnabled && !m_ChunkSink->HasChunkAnyClients(item.m_Coords))
		{
			LOGWARNING("Chunk generator overloaded, skipping chunk %s", item.m_Coords.ToString().c_str());
			if (item.m_Callback != nullptr)
			{
				item.m_Callback->Call(item.m_Coords, false);
			}
			continue;
		}

		// Generate the chunk:
		DoGenerate(item.m_Coords);
		if (item.m_Callback != nullptr)
		{
			item.m_Callback->Call(item.m_Coords, true);
		}
		NumChunksGenerated++;
	}  // while (!bStop)
}





void cChunkGeneratorThread::DoGenerate(cChunkCoords a_Coords)
{
	ASSERT(m_PluginInterface != nullptr);
	ASSERT(m_ChunkSink != nullptr);

	cChunkDesc ChunkDesc(a_Coords);
	m_PluginInterface->CallHookChunkGenerating(ChunkDesc);
	m_Generator->Generate(ChunkDesc);
	m_PluginInterface->CallHookChunkGenerated(ChunkDesc);

	#ifndef NDEBUG
		// Verify that the generator has produced valid data:
		ChunkDesc.VerifyHeightmap();
	#endif

	m_ChunkSink->OnChunkGenerated(ChunkDesc);
}
