
#include "Globals.h"  // NOTE: MSVC stupidness requires this to be the same across all modules

#include "ChunkMap.h"
#include "BlockInfo.h"
#include "World.h"
#include "Root.h"
#include "Entities/Player.h"
#include "Item.h"
#include "Chunk.h"
#include "Generating/Trees.h"  // used in cChunkMap::ReplaceTreeBlocks() for tree block discrimination
#include "BlockArea.h"
#include "Bindings/PluginManager.h"
#include "Blocks/BlockHandler.h"
#include "MobCensus.h"
#include "MobSpawner.h"
#include "BoundingBox.h"
#include "SetChunkData.h"
#include "Blocks/ChunkInterface.h"
#include "Entities/Pickup.h"
#include "DeadlockDetect.h"





////////////////////////////////////////////////////////////////////////////////
// cChunkMap:

cChunkMap::cChunkMap(cWorld * a_World) :
	m_World(a_World)
{
}





cChunk & cChunkMap::ConstructChunk(int a_ChunkX, int a_ChunkZ)
{
	// If not exists insert. Then, return the chunk at these coordinates:
	return m_Chunks.try_emplace(
		{ a_ChunkX, a_ChunkZ },
		a_ChunkX, a_ChunkZ, this, m_World
	).first->second;
}





cChunk & cChunkMap::GetChunk(int a_ChunkX, int a_ChunkZ)
{
	ASSERT(m_CSChunks.IsLockedByCurrentThread());  // m_CSChunks should already be locked by the operation that called us

	auto & Chunk = ConstructChunk(a_ChunkX, a_ChunkZ);
	if (!Chunk.IsValid() && !Chunk.IsQueued())
	{
		Chunk.SetPresence(cChunk::cpQueued);
		m_World->GetStorage().QueueLoadChunk(a_ChunkX, a_ChunkZ);
	}
	return Chunk;
}





cChunk * cChunkMap::FindChunk(int a_ChunkX, int a_ChunkZ)
{
	ASSERT(m_CSChunks.IsLockedByCurrentThread());

	const auto Chunk = m_Chunks.find({ a_ChunkX, a_ChunkZ });
	return (Chunk == m_Chunks.end()) ? nullptr : &Chunk->second;
}





const cChunk * cChunkMap::FindChunk(int a_ChunkX, int a_ChunkZ) const
{
	ASSERT(m_CSChunks.IsLockedByCurrentThread());

	const auto Chunk = m_Chunks.find({ a_ChunkX, a_ChunkZ });
	return (Chunk == m_Chunks.end()) ? nullptr : &Chunk->second;
}





void cChunkMap::SendBlockEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cClientHandle & a_Client)
{
	cCSLock Lock(m_CSChunks);
	int ChunkX, ChunkZ;
	cChunkDef::BlockToChunk(a_BlockX, a_BlockZ, ChunkX, ChunkZ);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return;
	}
	Chunk->SendBlockEntity(a_BlockX, a_BlockY, a_BlockZ, a_Client);
}





bool cChunkMap::UseBlockEntity(cPlayer * a_Player, int a_BlockX, int a_BlockY, int a_BlockZ)
{
	// a_Player rclked block entity at the coords specified, handle it
	cCSLock Lock(m_CSChunks);
	int ChunkX, ChunkZ;
	cChunkDef::BlockToChunk(a_BlockX, a_BlockZ, ChunkX, ChunkZ);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return false;
	}
	return Chunk->UseBlockEntity(a_Player, a_BlockX, a_BlockY, a_BlockZ);
}





bool cChunkMap::DoWithChunk(int a_ChunkX, int a_ChunkZ, cChunkCallback a_Callback)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return false;
	}
	return a_Callback(*Chunk);
}





bool cChunkMap::DoWithChunkAt(Vector3i a_BlockPos, cChunkCallback a_Callback)
{
	const auto Position = cChunkDef::BlockToChunk(a_BlockPos);
	return DoWithChunk(Position.m_ChunkX, Position.m_ChunkZ, a_Callback);
}





void cChunkMap::WakeUpSimulators(Vector3i a_Block)
{
	cCSLock Lock(m_CSChunks);
	const auto Position = cChunkDef::BlockToChunk(a_Block);
	const auto Chunk = FindChunk(Position.m_ChunkX, Position.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return;
	}

	m_World->GetSimulatorManager()->WakeUp(*Chunk, cChunkDef::AbsoluteToRelative(a_Block, Position));
}





void cChunkMap::MarkChunkDirty(int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return;
	}
	Chunk->MarkDirty();
}





void cChunkMap::MarkChunkSaving(int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return;
	}
	Chunk->MarkSaving();
}





void cChunkMap::MarkChunkSaved (int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return;
	}
	Chunk->MarkSaved();
}





void cChunkMap::SetChunkData(struct SetChunkData && a_SetChunkData)
{
	const int ChunkX = a_SetChunkData.Chunk.m_ChunkX;
	const int ChunkZ = a_SetChunkData.Chunk.m_ChunkZ;
	{
		cCSLock Lock(m_CSChunks);
		const auto Chunk = FindChunk(ChunkX, ChunkZ);
		ASSERT(Chunk != nullptr);  // Chunk cannot have unloaded since it is marked as queued
		Chunk->SetAllData(std::move(a_SetChunkData));

		// Notify relevant ChunkStays:
		cChunkStays ToBeDisabled;
		for (cChunkStays::iterator itr = m_ChunkStays.begin(), end = m_ChunkStays.end(); itr != end; ++itr)
		{
			if ((*itr)->ChunkAvailable(ChunkX, ChunkZ))
			{
				// The chunkstay wants to be disabled, add it to a list of to-be-disabled chunkstays for later processing:
				ToBeDisabled.push_back(*itr);
			}
		}  // for itr - m_ChunkStays[]

		// Disable (and possibly remove) the chunkstays that chose to get disabled:
		for (cChunkStays::iterator itr = ToBeDisabled.begin(), end = ToBeDisabled.end(); itr != end; ++itr)
		{
			(*itr)->Disable();
		}
	}

	// Notify plugins of the chunk becoming available
	cPluginManager::Get()->CallHookChunkAvailable(*m_World, ChunkX, ChunkZ);
}





void cChunkMap::ChunkLighted(
	int a_ChunkX, int a_ChunkZ,
	const cChunkDef::BlockNibbles & a_BlockLight,
	const cChunkDef::BlockNibbles & a_SkyLight
)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if (Chunk == nullptr)
	{
		// Chunk probably unloaded in the meantime
		return;
	}
	Chunk->SetLight(a_BlockLight, a_SkyLight);
}





bool cChunkMap::GetChunkData(cChunkCoords a_Coords, cChunkDataCallback & a_Callback) const
{
	if (!a_Callback.Coords(a_Coords.m_ChunkX, a_Coords.m_ChunkZ))
	{
		// The callback doesn't want the data
		return false;
	}
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_Coords.m_ChunkX, a_Coords.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		// The chunk is not present
		return false;
	}
	Chunk->GetAllData(a_Callback);
	return true;
}





bool cChunkMap::IsChunkQueued(int a_ChunkX, int a_ChunkZ) const
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	return (Chunk != nullptr) && Chunk->IsQueued();
}





bool cChunkMap::IsWeatherSunnyAt(int a_BlockX, int a_BlockZ) const
{
	int ChunkX, ChunkZ, BlockY = 0;
	cChunkDef::AbsoluteToRelative(a_BlockX, BlockY, a_BlockZ, ChunkX, ChunkZ);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return m_World->IsWeatherSunny();
	}

	return Chunk->IsWeatherSunnyAt(a_BlockX, a_BlockZ);
}





bool cChunkMap::IsWeatherWetAt(int a_BlockX, int a_BlockZ) const
{
	int ChunkX, ChunkZ, BlockY = 0;
	cChunkDef::AbsoluteToRelative(a_BlockX, BlockY, a_BlockZ, ChunkX, ChunkZ);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return m_World->IsWeatherWet();
	}

	return Chunk->IsWeatherWetAt(a_BlockX, a_BlockZ);
}





bool cChunkMap::IsWeatherWetAt(const Vector3i a_Position) const
{
	const auto ChunkPosition = cChunkDef::BlockToChunk(a_Position);
	const auto Position = cChunkDef::AbsoluteToRelative(a_Position, ChunkPosition);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkPosition.m_ChunkX, ChunkPosition.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return m_World->IsWeatherWet();
	}

	return Chunk->IsWeatherWetAt(Position);
}





bool cChunkMap::IsChunkValid(int a_ChunkX, int a_ChunkZ) const
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	return (Chunk != nullptr) && Chunk->IsValid();
}





bool cChunkMap::HasChunkAnyClients(int a_ChunkX, int a_ChunkZ) const
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	return (Chunk != nullptr) && Chunk->HasAnyClients();
}





std::optional<int> cChunkMap::GetHeight(int a_BlockX, int a_BlockZ)
{
	// Returns std::nullopt if chunk not loaded / generated.
	cCSLock Lock(m_CSChunks);
	int ChunkX, ChunkZ, BlockY = 0;
	cChunkDef::AbsoluteToRelative(a_BlockX, BlockY, a_BlockZ, ChunkX, ChunkZ);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return std::nullopt;
	}

	return Chunk->GetHeight(a_BlockX, a_BlockZ);
}





void cChunkMap::FastSetBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta)
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		Chunk->FastSetBlock(relPos, a_BlockType, a_BlockMeta);
	}
}





void cChunkMap::CollectPickupsByEntity(cEntity & a_Entity)
{
	cCSLock Lock(m_CSChunks);

	auto BoundingBox = a_Entity.GetBoundingBox();
	BoundingBox.Expand(1, 0.5, 1);

	ForEachEntityInBox(BoundingBox, [&a_Entity](cEntity & Entity)
	{
		// Only pickups and projectiles can be picked up:
		if (Entity.IsPickup())
		{
			/*
			LOG("Pickup %d being collected by player \"%s\", distance %f",
				(*itr)->GetUniqueID(), a_Player->GetName().c_str(), SqrDist
			);
			*/
			static_cast<cPickup &>(Entity).CollectedBy(a_Entity);
		}
		else if (Entity.IsProjectile() && a_Entity.IsPlayer())
		{
			static_cast<cProjectileEntity &>(Entity).CollectedBy(static_cast<cPlayer&>(a_Entity));
		}

		// The entities will MarkDirty when they Destroy themselves
		return false;
	});
}





BLOCKTYPE cChunkMap::GetBlock(Vector3i a_BlockPos) const
{
	if (!cChunkDef::IsValidHeight(a_BlockPos))
	{
		return 0;
	}

	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	// Query the chunk, if loaded:
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		return Chunk->GetBlock(relPos);
	}
	return 0;
}





NIBBLETYPE cChunkMap::GetBlockMeta(Vector3i a_BlockPos) const
{
	if (!cChunkDef::IsValidHeight(a_BlockPos))
	{
		return 0;
	}

	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	// Query the chunk, if loaded:
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		return Chunk->GetMeta(relPos);
	}
	return 0;
}





NIBBLETYPE cChunkMap::GetBlockSkyLight(Vector3i a_BlockPos) const
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	// Query the chunk, if loaded:
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		return Chunk->GetSkyLight(relPos);
	}
	return 0;
}





NIBBLETYPE cChunkMap::GetBlockBlockLight(Vector3i a_BlockPos) const
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	// Query the chunk, if loaded:
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		return Chunk->GetBlockLight(relPos);
	}
	return 0;
}





void cChunkMap::SetBlockMeta(Vector3i a_BlockPos, NIBBLETYPE a_BlockMeta)
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	// Query the chunk, if loaded:
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		Chunk->SetMeta(relPos, a_BlockMeta);
	}
}





void cChunkMap::SetBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta)
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		Chunk->SetBlock(relPos, a_BlockType, a_BlockMeta);
	}
}





bool cChunkMap::GetBlockTypeMeta(Vector3i a_BlockPos, BLOCKTYPE & a_BlockType, NIBBLETYPE & a_BlockMeta) const
{
	if (!cChunkDef::IsValidHeight(a_BlockPos))
	{
		// Initialise the params to fulfil our contract.
		a_BlockType = 0;
		a_BlockMeta = 0;
		return false;
	}

	auto chunkCoord = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkCoord);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkCoord.m_ChunkX, chunkCoord.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		Chunk->GetBlockTypeMeta(relPos, a_BlockType, a_BlockMeta);
		return true;
	}

	// Initialise the params to fulfil our contract.
	a_BlockType = 0;
	a_BlockMeta = 0;
	return false;
}





bool cChunkMap::GetBlockInfo(Vector3i a_BlockPos, BLOCKTYPE & a_BlockType, NIBBLETYPE & a_Meta, NIBBLETYPE & a_SkyLight, NIBBLETYPE & a_BlockLight) const
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);

	// Query the chunk, if loaded:
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		Chunk->GetBlockInfo(relPos, a_BlockType, a_Meta, a_SkyLight, a_BlockLight);
		return true;
	}
	return false;
}





void cChunkMap::ReplaceTreeBlocks(const sSetBlockVector & a_Blocks)
{
	cCSLock Lock(m_CSChunks);
	for (sSetBlockVector::const_iterator itr = a_Blocks.begin(); itr != a_Blocks.end(); ++itr)
	{
		const auto Chunk = FindChunk(itr->m_ChunkX, itr->m_ChunkZ);
		if ((Chunk == nullptr) || !Chunk->IsValid())
		{
			continue;
		}
		Vector3i relPos(itr->m_RelX, itr->m_RelY, itr->m_RelZ);
		switch (Chunk->GetBlock(relPos))
		{
			CASE_TREE_OVERWRITTEN_BLOCKS:
			{
				Chunk->SetBlock(relPos, itr->m_BlockType, itr->m_BlockMeta);
				break;
			}
			case E_BLOCK_LEAVES:
			case E_BLOCK_NEW_LEAVES:
			{
				if ((itr->m_BlockType == E_BLOCK_LOG) || (itr->m_BlockType == E_BLOCK_NEW_LOG))
				{
					Chunk->SetBlock(relPos, itr->m_BlockType, itr->m_BlockMeta);
				}
				break;
			}
		}
	}  // for itr - a_Blocks[]
}





EMCSBiome cChunkMap::GetBiomeAt(int a_BlockX, int a_BlockZ) const
{
	int ChunkX, ChunkZ, X = a_BlockX, Y = 0, Z = a_BlockZ;
	cChunkDef::AbsoluteToRelative(X, Y, Z, ChunkX, ChunkZ);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		return Chunk->GetBiomeAt(X, Z);
	}
	return EMCSBiome::biInvalidBiome;
}





bool cChunkMap::SetBiomeAt(int a_BlockX, int a_BlockZ, EMCSBiome a_Biome)
{
	int ChunkX, ChunkZ, X = a_BlockX, Y = 0, Z = a_BlockZ;
	cChunkDef::AbsoluteToRelative(X, Y, Z, ChunkX, ChunkZ);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk != nullptr) && Chunk->IsValid())
	{
		Chunk->SetBiomeAt(X, Z, a_Biome);
		return true;
	}
	return false;
}





bool cChunkMap::SetAreaBiome(int a_MinX, int a_MaxX, int a_MinZ, int a_MaxZ, EMCSBiome a_Biome)
{
	// Translate coords to relative:
	int Y = 0;
	int MinChunkX, MinChunkZ, MinX = a_MinX, MinZ = a_MinZ;
	int MaxChunkX, MaxChunkZ, MaxX = a_MaxX, MaxZ = a_MaxZ;
	cChunkDef::AbsoluteToRelative(MinX, Y, MinZ, MinChunkX, MinChunkZ);
	cChunkDef::AbsoluteToRelative(MaxX, Y, MaxZ, MaxChunkX, MaxChunkZ);

	// Go through all chunks, set:
	bool res = true;
	cCSLock Lock(m_CSChunks);
	for (int x = MinChunkX; x <= MaxChunkX; x++)
	{
		int MinRelX = (x == MinChunkX) ? MinX : 0;
		int MaxRelX = (x == MaxChunkX) ? MaxX : cChunkDef::Width - 1;
		for (int z = MinChunkZ; z <= MaxChunkZ; z++)
		{
			int MinRelZ = (z == MinChunkZ) ? MinZ : 0;
			int MaxRelZ = (z == MaxChunkZ) ? MaxZ : cChunkDef::Width - 1;
			const auto Chunk = FindChunk(x, z);
			if ((Chunk != nullptr) && Chunk->IsValid())
			{
				Chunk->SetAreaBiome(MinRelX, MaxRelX, MinRelZ, MaxRelZ, a_Biome);
			}
			else
			{
				res = false;
			}
		}  // for z
	}  // for x
	return res;
}





bool cChunkMap::GetBlocks(sSetBlockVector & a_Blocks, bool a_ContinueOnFailure)
{
	bool res = true;
	cCSLock Lock(m_CSChunks);
	for (sSetBlockVector::iterator itr = a_Blocks.begin(); itr != a_Blocks.end(); ++itr)
	{
		const auto Chunk = FindChunk(itr->m_ChunkX, itr->m_ChunkZ);
		if ((Chunk == nullptr) || !Chunk->IsValid())
		{
			if (!a_ContinueOnFailure)
			{
				return false;
			}
			res = false;
			continue;
		}
		if (!cChunkDef::IsValidHeight(itr->GetRelativePos()))
		{
			continue;
		}
		itr->m_BlockType = Chunk->GetBlock(itr->m_RelX, itr->m_RelY, itr->m_RelZ);
		itr->m_BlockMeta = Chunk->GetMeta(itr->m_RelX, itr->m_RelY, itr->m_RelZ);
	}
	return res;
}





bool cChunkMap::DigBlock(Vector3i a_BlockPos)
{
	auto chunkCoords = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkCoords);

	{
		cCSLock Lock(m_CSChunks);
		const auto Chunk = FindChunk(chunkCoords.m_ChunkX, chunkCoords.m_ChunkZ);
		if ((Chunk == nullptr) || !Chunk->IsValid())
		{
			return false;
		}

		Chunk->SetBlock(relPos, E_BLOCK_AIR, 0);
	}
	return true;
}





cItems cChunkMap::PickupsFromBlock(Vector3i a_BlockPos, const cEntity * a_Digger, const cItem * a_Tool)
{
	auto chunkCoords = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkCoords);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkCoords.m_ChunkX, chunkCoords.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return {};
	}
	return Chunk->PickupsFromBlock(relPos, a_Digger, a_Tool);
}





void cChunkMap::SendBlockTo(int a_X, int a_Y, int a_Z, const cPlayer & a_Player)
{
	int ChunkX, ChunkZ;
	cChunkDef::AbsoluteToRelative(a_X, a_Y, a_Z, ChunkX, ChunkZ);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkX, ChunkZ);
	if ((Chunk != nullptr) && (Chunk->IsValid()))
	{
		Chunk->SendBlockTo(a_X, a_Y, a_Z, a_Player.GetClientHandle());
	}
}





void cChunkMap::CompareChunkClients(int a_ChunkX1, int a_ChunkZ1, int a_ChunkX2, int a_ChunkZ2, cClientDiffCallback & a_Callback)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk1 = FindChunk(a_ChunkX1, a_ChunkZ1);
	if (Chunk1 == nullptr)
	{
		return;
	}
	const auto Chunk2 = FindChunk(a_ChunkX2, a_ChunkZ2);
	if (Chunk2 == nullptr)
	{
		return;
	}

	CompareChunkClients(Chunk1, Chunk2, a_Callback);
}





void cChunkMap::CompareChunkClients(cChunk * a_Chunk1, cChunk * a_Chunk2, cClientDiffCallback & a_Callback)
{
	const auto & Clients1 = a_Chunk1->GetAllClients();
	const auto & Clients2 = a_Chunk2->GetAllClients();

	// Find "removed" clients:
	for (auto * Client : Clients1)
	{
		bool Found = (std::find(Clients2.begin(), Clients2.end(), Client) != Clients2.end());
		if (!Found)
		{
			a_Callback.Removed(Client);
		}
	}  // for Client - Clients1[]

	// Find "added" clients:
	for (auto * Client : Clients2)
	{
		bool Found = (std::find(Clients1.begin(), Clients1.end(), Client) != Clients1.end());
		if (!Found)
		{
			a_Callback.Added(Client);
		}
	}  // for Client - Clients2[]
}





bool cChunkMap::AddChunkClient(int a_ChunkX, int a_ChunkZ, cClientHandle * a_Client)
{
	cCSLock Lock(m_CSChunks);
	return GetChunk(a_ChunkX, a_ChunkZ).AddClient(a_Client);
}





void cChunkMap::RemoveChunkClient(int a_ChunkX, int a_ChunkZ, cClientHandle * a_Client)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	ASSERT(Chunk != nullptr);
	Chunk->RemoveClient(a_Client);
}





void cChunkMap::RemoveClientFromChunks(cClientHandle * a_Client)
{
	cCSLock Lock(m_CSChunks);
	for (auto & Chunk : m_Chunks)
	{
		Chunk.second.RemoveClient(a_Client);
	}
}





void cChunkMap::AddEntity(OwnedEntity a_Entity)
{
	cCSLock Lock(m_CSChunks);
	if (FindChunk(a_Entity->GetChunkX(), a_Entity->GetChunkZ()) == nullptr)
	{
		LOGWARNING("%s: Entity at %p (%s, ID %d) spawning in a non-existent chunk.",
			__FUNCTION__, static_cast<void *>(a_Entity.get()), a_Entity->GetClass(), a_Entity->GetUniqueID()
		);
	}

	const auto EntityPtr = a_Entity.get();
	ASSERT(EntityPtr->GetWorld() == m_World);

	auto & Chunk = ConstructChunk(a_Entity->GetChunkX(), a_Entity->GetChunkZ());
	Chunk.AddEntity(std::move(a_Entity));

	EntityPtr->OnAddToWorld(*m_World);
	ASSERT(!EntityPtr->IsTicking());
	EntityPtr->SetIsTicking(true);

	cPluginManager::Get()->CallHookSpawnedEntity(*m_World, *EntityPtr);
}





void cChunkMap::AddPlayer(std::unique_ptr<cPlayer> a_Player)
{
	cCSLock Lock(m_CSChunks);
	auto & Chunk = ConstructChunk(a_Player->GetChunkX(), a_Player->GetChunkZ());  // Always construct the chunk for players
	ASSERT(!Chunk.HasEntity(a_Player->GetUniqueID()));
	Chunk.AddEntity(std::move(a_Player));
}





bool cChunkMap::HasEntity(UInt32 a_UniqueID) const
{
	cCSLock Lock(m_CSChunks);
	for (const auto & Chunk : m_Chunks)
	{
		if (Chunk.second.IsValid() && Chunk.second.HasEntity(a_UniqueID))
		{
			return true;
		}
	}
	return false;
}





OwnedEntity cChunkMap::RemoveEntity(cEntity & a_Entity)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = a_Entity.GetParentChunk();

	if (Chunk == nullptr)
	{
		return nullptr;
	}

	// Remove the entity no matter whether the chunk itself is valid or not (#1190)
	return Chunk->RemoveEntity(a_Entity);
}





bool cChunkMap::ForEachEntity(cEntityCallback a_Callback) const
{
	cCSLock Lock(m_CSChunks);
	for (const auto & Chunk : m_Chunks)
	{
		if (Chunk.second.IsValid() && !Chunk.second.ForEachEntity(a_Callback))
		{
			return false;
		}
	}
	return true;
}





bool cChunkMap::ForEachEntityInChunk(int a_ChunkX, int a_ChunkZ, cEntityCallback a_Callback)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return false;
	}
	return Chunk->ForEachEntity(a_Callback);
}





bool cChunkMap::ForEachEntityInBox(const cBoundingBox & a_Box, cEntityCallback a_Callback)
{
	// Calculate the chunk range for the box:
	int MinChunkX = FloorC(a_Box.GetMinX() / cChunkDef::Width);
	int MinChunkZ = FloorC(a_Box.GetMinZ() / cChunkDef::Width);
	int MaxChunkX = FloorC((a_Box.GetMaxX() + cChunkDef::Width) / cChunkDef::Width);
	int MaxChunkZ = FloorC((a_Box.GetMaxZ() + cChunkDef::Width) / cChunkDef::Width);

	// Iterate over each chunk in the range:
	cCSLock Lock(m_CSChunks);
	for (int z = MinChunkZ; z <= MaxChunkZ; z++)
	{
		for (int x = MinChunkX; x <= MaxChunkX; x++)
		{
			const auto Chunk = FindChunk(x, z);
			if ((Chunk == nullptr) || !Chunk->IsValid())
			{
				continue;
			}
			if (!Chunk->ForEachEntityInBox(a_Box, a_Callback))
			{
				return false;
			}
		}  // for x
	}  // for z
	return true;
}





bool cChunkMap::DoWithEntityByID(UInt32 a_UniqueID, cEntityCallback a_Callback) const
{
	cCSLock Lock(m_CSChunks);
	bool res = false;
	for (const auto & Chunk : m_Chunks)
	{
		if (Chunk.second.IsValid() && Chunk.second.DoWithEntityByID(a_UniqueID, a_Callback, res))
		{
			return res;
		}
	}
	return false;
}





bool cChunkMap::ForEachBlockEntityInChunk(int a_ChunkX, int a_ChunkZ, cBlockEntityCallback a_Callback)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return false;
	}
	return Chunk->ForEachBlockEntity(a_Callback);
}





bool cChunkMap::DoWithBlockEntityAt(const Vector3i a_Position, cBlockEntityCallback a_Callback)
{
	const auto ChunkPosition = cChunkDef::BlockToChunk(a_Position);
	const auto Relative = cChunkDef::AbsoluteToRelative(a_Position, ChunkPosition);
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkPosition.m_ChunkX, ChunkPosition.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return false;
	}
	return Chunk->DoWithBlockEntityAt(Relative, a_Callback);
}





void cChunkMap::PrepareChunk(int a_ChunkX, int a_ChunkZ, std::unique_ptr<cChunkCoordCallback> a_Callback)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);

	// If the chunk is not prepared, queue it in the lighting thread, that will do all the needed processing:
	if ((Chunk == nullptr) || !Chunk->IsValid() || !Chunk->IsLightValid())
	{
		m_World->GetLightingThread().QueueChunk(a_ChunkX, a_ChunkZ, std::move(a_Callback));
		return;
	}

	// The chunk is present and lit, just call the callback, report as success:
	if (a_Callback != nullptr)
	{
		a_Callback->Call({a_ChunkX, a_ChunkZ}, true);
	}
}





void cChunkMap::GenerateChunk(int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	GetChunk(a_ChunkX, a_ChunkZ);  // Touches the chunk, loading or generating it
}





void cChunkMap::ChunkLoadFailed(int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	ASSERT(Chunk != nullptr);  // Chunk cannot have unloaded since it is marked as queued
	Chunk->MarkLoadFailed();
}





void cChunkMap::MarkChunkRegenerating(int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	ConstructChunk(a_ChunkX, a_ChunkZ).MarkRegenerating();
}





bool cChunkMap::IsChunkLighted(int a_ChunkX, int a_ChunkZ)
{
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(a_ChunkX, a_ChunkZ);
	if (Chunk == nullptr)
	{
		// Not present
		return false;
	}
	return Chunk->IsLightValid();
}





bool cChunkMap::ForEachChunkInRect(int a_MinChunkX, int a_MaxChunkX, int a_MinChunkZ, int a_MaxChunkZ, cChunkDataCallback & a_Callback)
{
	bool Result = true;
	cCSLock Lock(m_CSChunks);
	for (int z = a_MinChunkZ; z <= a_MaxChunkZ; z++)
	{
		for (int x = a_MinChunkX; x <= a_MaxChunkX; x++)
		{
			const auto Chunk = FindChunk(x, z);
			if ((Chunk == nullptr) || !Chunk->IsValid())
			{
				// Not present / not valid
				Result = false;
				continue;
			}
			if (!a_Callback.Coords(x, z))
			{
				continue;
			}
			Chunk->GetAllData(a_Callback);
		}
	}
	return Result;
}





bool cChunkMap::ForEachLoadedChunk(cFunctionRef<bool(int, int)> a_Callback) const
{
	cCSLock Lock(m_CSChunks);
	for (const auto & Chunk : m_Chunks)
	{
		if (Chunk.second.IsValid())
		{
			if (a_Callback(Chunk.first.m_ChunkX, Chunk.first.m_ChunkZ))
			{
				return false;
			}
		}
	}
	return true;
}





bool cChunkMap::WriteBlockArea(cBlockArea & a_Area, int a_MinBlockX, int a_MinBlockY, int a_MinBlockZ, int a_DataTypes)
{
	// Convert block coords to chunks coords:
	int MinChunkX, MaxChunkX;
	int MinChunkZ, MaxChunkZ;
	int MinBlockX = a_MinBlockX;
	int MinBlockY = a_MinBlockY;
	int MinBlockZ = a_MinBlockZ;
	int MaxBlockX = a_MinBlockX + a_Area.GetSizeX();
	int MaxBlockY = a_MinBlockY + a_Area.GetSizeY();
	int MaxBlockZ = a_MinBlockZ + a_Area.GetSizeZ();
	cChunkDef::AbsoluteToRelative(MinBlockX, MinBlockY, MinBlockZ, MinChunkX, MinChunkZ);
	cChunkDef::AbsoluteToRelative(MaxBlockX, MaxBlockY, MaxBlockZ, MaxChunkX, MaxChunkZ);

	// Iterate over chunks, write data into each:
	bool Result = true;
	cCSLock Lock(m_CSChunks);
	for (int z = MinChunkZ; z <= MaxChunkZ; z++)
	{
		for (int x = MinChunkX; x <= MaxChunkX; x++)
		{
			const auto Chunk = FindChunk(x, z);
			if ((Chunk == nullptr) || !Chunk->IsValid())
			{
				// Not present / not valid
				Result = false;
				continue;
			}
			Chunk->WriteBlockArea(a_Area, a_MinBlockX, a_MinBlockY, a_MinBlockZ, a_DataTypes);
		}  // for x
	}  // for z
	return Result;
}





void cChunkMap::GetChunkStats(int & a_NumChunksValid, int & a_NumChunksDirty) const
{
	a_NumChunksValid = 0;
	a_NumChunksDirty = 0;
	cCSLock Lock(m_CSChunks);
	for (const auto & Chunk : m_Chunks)
	{
		a_NumChunksValid++;
		if (Chunk.second.IsDirty())
		{
			a_NumChunksDirty++;
		}
	}
}





int cChunkMap::GrowPlantAt(Vector3i a_BlockPos, int a_NumStages)
{
	auto chunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto relPos = cChunkDef::AbsoluteToRelative(a_BlockPos, chunkPos);
	cCSLock lock(m_CSChunks);
	const auto Chunk = FindChunk(chunkPos.m_ChunkX, chunkPos.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return 0;
	}
	return Chunk->GrowPlantAt(relPos, a_NumStages);
}





void cChunkMap::SetNextBlockToTick(const Vector3i a_BlockPos)
{
	auto ChunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto RelPos = cChunkDef::AbsoluteToRelative(a_BlockPos, ChunkPos);

	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkPos.m_ChunkX, ChunkPos.m_ChunkZ);
	if (Chunk != nullptr)
	{
		Chunk->SetNextBlockToTick(RelPos);
	}
}





void cChunkMap::CollectMobCensus(cMobCensus & a_ToFill)
{
	cCSLock Lock(m_CSChunks);
	for (auto & Chunk : m_Chunks)
	{
		// We do count every Mobs in the world. But we are assuming that every chunk not loaded by any client
		// doesn't affect us. Normally they should not have mobs because every "too far" mobs despawn
		// If they have (f.i. when player disconnect) we assume we don't have to make them live or despawn
		if (Chunk.second.IsValid() && Chunk.second.HasAnyClients())
		{
			Chunk.second.CollectMobCensus(a_ToFill);
		}
	}
}





void cChunkMap::SpawnMobs(cMobSpawner & a_MobSpawner)
{
	cCSLock Lock(m_CSChunks);
	for (auto & Chunk : m_Chunks)
	{
		// We only spawn close to players
		if (Chunk.second.IsValid() && Chunk.second.HasAnyClients())
		{
			Chunk.second.SpawnMobs(a_MobSpawner);
		}
	}
}





void cChunkMap::Tick(std::chrono::milliseconds a_Dt)
{
	cCSLock Lock(m_CSChunks);

	// Do the magic of updating the world:
	for (auto & Chunk : m_Chunks)
	{
		if (Chunk.second.ShouldBeTicked())
		{
			Chunk.second.Tick(a_Dt);
		}
	}

	// Finally, only after all chunks are ticked, tell the client about all aggregated changes:
	for (auto & Chunk : m_Chunks)
	{
		Chunk.second.BroadcastPendingChanges();
	}
}





void cChunkMap::TickBlock(const Vector3i a_BlockPos)
{
	auto ChunkPos = cChunkDef::BlockToChunk(a_BlockPos);
	auto RelPos = cChunkDef::AbsoluteToRelative(a_BlockPos, ChunkPos);
	cCSLock Lock(m_CSChunks);
	const auto Chunk = FindChunk(ChunkPos.m_ChunkX, ChunkPos.m_ChunkZ);
	if ((Chunk == nullptr) || !Chunk->IsValid())
	{
		return;
	}
	Chunk->TickBlock(RelPos);
}





void cChunkMap::UnloadUnusedChunks(void)
{
	cCSLock Lock(m_CSChunks);
	for (auto itr = m_Chunks.begin(); itr != m_Chunks.end();)
	{
		if (
			itr->second.CanUnload() &&  // Can unload
			!cPluginManager::Get()->CallHookChunkUnloading(*GetWorld(), itr->first.m_ChunkX, itr->first.m_ChunkZ)  // Plugins agree
		)
		{
			// First notify plugins:
			cPluginManager::Get()->CallHookChunkUnloaded(*m_World, itr->first.m_ChunkX, itr->first.m_ChunkZ);

			// Notify entities within the chunk, while everything's still valid:
			itr->second.OnUnload();

			// Kill the chunk:
			itr = m_Chunks.erase(itr);
		}
		else
		{
			++itr;
		}
	}
}





void cChunkMap::SaveAllChunks(void) const
{
	cCSLock Lock(m_CSChunks);
	for (const auto & Chunk : m_Chunks)
	{
		if (Chunk.second.IsValid() && Chunk.second.IsDirty())
		{
			GetWorld()->GetStorage().QueueSaveChunk(Chunk.first.m_ChunkX, Chunk.first.m_ChunkZ);
		}
	}
}





size_t cChunkMap::GetNumChunks(void) const
{
	cCSLock Lock(m_CSChunks);
	return m_Chunks.size();
}





size_t cChunkMap::GetNumUnusedDirtyChunks(void) const
{
	cCSLock Lock(m_CSChunks);
	size_t res = 0;
	for (const auto & Chunk : m_Chunks)
	{
		if (Chunk.second.IsValid() && Chunk.second.CanUnloadAfterSaving())
		{
			res += 1;
		}
	}
	return res;
}





void cChunkMap::ChunkValidated(void)
{
	m_evtChunkValid.Set();
}





void cChunkMap::SetChunkAlwaysTicked(int a_ChunkX, int a_ChunkZ, bool a_AlwaysTicked)
{
	cCSLock Lock(m_CSChunks);
	GetChunk(a_ChunkX, a_ChunkZ).SetAlwaysTicked(a_AlwaysTicked);
}





void cChunkMap::TrackInDeadlockDetect(cDeadlockDetect & a_DeadlockDetect, const AString & a_WorldName)
{
	a_DeadlockDetect.TrackCriticalSection(m_CSChunks, fmt::format(FMT_STRING("World {} chunkmap"), a_WorldName));
}





void cChunkMap::UntrackInDeadlockDetect(cDeadlockDetect & a_DeadlockDetect)
{
	a_DeadlockDetect.UntrackCriticalSection(m_CSChunks);
}





void cChunkMap::AddChunkStay(cChunkStay & a_ChunkStay)
{
	cCSLock Lock(m_CSChunks);

	// Add it to the list:
	ASSERT(std::find(m_ChunkStays.begin(), m_ChunkStays.end(), &a_ChunkStay) == m_ChunkStays.end());  // Has not yet been added
	m_ChunkStays.push_back(&a_ChunkStay);

	// Schedule all chunks to be loaded / generated, and mark each as locked:
	const cChunkCoordsVector & WantedChunks = a_ChunkStay.GetChunks();
	for (cChunkCoordsVector::const_iterator itr = WantedChunks.begin(); itr != WantedChunks.end(); ++itr)
	{
		auto & Chunk = GetChunk(itr->m_ChunkX, itr->m_ChunkZ);
		Chunk.Stay(true);
		if (Chunk.IsValid())
		{
			if (a_ChunkStay.ChunkAvailable(itr->m_ChunkX, itr->m_ChunkZ))
			{
				// The chunkstay wants to be deactivated, disable it and bail out:
				a_ChunkStay.Disable();
				return;
			}
		}
	}  // for itr - WantedChunks[]
}





/** Removes the specified cChunkStay descendant from the internal list of ChunkStays. */
void cChunkMap::DelChunkStay(cChunkStay & a_ChunkStay)
{
	cCSLock Lock(m_CSChunks);

	// Remove from the list of active chunkstays:
	bool HasFound = false;
	for (cChunkStays::iterator itr = m_ChunkStays.begin(), end = m_ChunkStays.end(); itr != end; ++itr)
	{
		if (*itr == &a_ChunkStay)
		{
			m_ChunkStays.erase(itr);
			HasFound = true;
			break;
		}
	}  // for itr - m_ChunkStays[]

	if (!HasFound)
	{
		ASSERT(!"Removing a cChunkStay that hasn't been added!");
		return;
	}

	// Unmark all contained chunks:
	const cChunkCoordsVector & Chunks = a_ChunkStay.GetChunks();
	for (cChunkCoordsVector::const_iterator itr = Chunks.begin(), end = Chunks.end(); itr != end; ++itr)
	{
		const auto Chunk = FindChunk(itr->m_ChunkX, itr->m_ChunkZ);
		ASSERT(Chunk != nullptr);  // Stayed chunks cannot unload
		Chunk->Stay(false);
	}  // for itr - Chunks[]
	a_ChunkStay.OnDisabled();
}
