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

#include "NetherPortalScanner.h"
#include "BlockInfo.h"
#include "Entities/Entity.h"
#include "World.h"





const double cNetherPortalScanner::OutOffset = 0.5;
const double cNetherPortalScanner::AcrossOffset = 0.5;





cNetherPortalScanner::cNetherPortalScanner(cEntity & a_MovingEntity, cWorld & a_DestinationWorld, Vector3d a_DestPosition, int a_MaxY) :
	m_EntityID(a_MovingEntity.GetUniqueID()),
	m_SourceWorld(*a_MovingEntity.GetWorld()),
	m_World(a_DestinationWorld),
	m_FoundPortal(false),
	m_BuildPlatform(true),
	m_Dir(Direction::X),
	m_PortalLoc(a_DestPosition.Floor()),
	m_Position(a_DestPosition),
	m_MaxY(a_MaxY)
{
	int MinX = FloorC((m_Position.x - SearchRadius) / cChunkDef::Width);
	int MinZ = FloorC((m_Position.z - SearchRadius) / cChunkDef::Width);
	int MaxX = CeilC((m_Position.x + SearchRadius) / cChunkDef::Width);
	int MaxZ = CeilC((m_Position.z + SearchRadius) / cChunkDef::Width);
	for (int x = MinX; x < MaxX; x++)
	{
		for (int z = MinZ; z < MaxZ; z++)
		{
			Add(x, z);
		}
	}
	Enable(*a_DestinationWorld.GetChunkMap());
}





void cNetherPortalScanner::OnChunkAvailable(int a_ChunkX, int a_ChunkZ)
{
	class PortalSearchCallback : public cChunkDataCallback
	{
	public:

		PortalSearchCallback(const int a_ChunkX, const int a_ChunkZ, bool & a_FoundPortal, Vector3i & a_PortalLoc, const Vector3d a_Position, const int a_MaxY) :
			m_ChunkX(a_ChunkX),
			m_ChunkZ(a_ChunkZ),
			m_FoundPortal(a_FoundPortal),
			m_PortalLoc(a_PortalLoc),
			m_Position(a_Position),
			m_MaxY(a_MaxY)
		{
		}

	private:

		virtual void ChunkData(const ChunkBlockData & a_BlockData, const ChunkLightData &) override
		{
			for (size_t Y = 0; Y < cChunkDef::NumSections; ++Y)
			{
				const auto Blocks = a_BlockData.GetSection(Y);
				if (Blocks == nullptr)
				{
					continue;
				}

				// Iterate through all of the blocks in the chunk:
				for (size_t i = 0; i < ChunkBlockData::SectionBlockCount; i++)
				{
					if ((*Blocks)[i] != E_BLOCK_NETHER_PORTAL)
					{
						continue;
					}

					Vector3i Coordinate = cChunkDef::IndexToCoordinate(i + Y * ChunkBlockData::SectionBlockCount);
					if (Coordinate.y >= m_MaxY)
					{
						// This is above the map, don't consider it:
						continue;
					}

					Vector3d PortalLoc = Vector3d(Coordinate.x + m_ChunkX * cChunkDef::Width, Coordinate.y, Coordinate.z + m_ChunkZ * cChunkDef::Width);
					if (!m_FoundPortal)
					{
						m_FoundPortal = true;
						m_PortalLoc = PortalLoc;
					}
					else if ((PortalLoc - m_Position).SqrLength() < (m_PortalLoc - m_Position).SqrLength())
					{
						// Found a closer portal, use that instead:
						m_PortalLoc = PortalLoc;
					}
				}
			}
		}

		int m_ChunkX, m_ChunkZ;

		bool & m_FoundPortal;
		Vector3i & m_PortalLoc;
		const Vector3d m_Position;

		const int m_MaxY;
	} Callback(a_ChunkX, a_ChunkZ, m_FoundPortal, m_PortalLoc, m_Position, m_MaxY);

	[[maybe_unused]] const bool Result = m_World.GetChunkData({ a_ChunkX, a_ChunkZ }, Callback);
	ASSERT(Result);
}





bool cNetherPortalScanner::IsValidBuildLocation(Vector3i a_BlockPos)
{
	// Check the base
	for (int i = 0; i < SearchSolidBaseWidth; i++)
	{
		for (int j = 0; j < PortalLength; j++)
		{
			BLOCKTYPE blocktype = m_World.GetBlock(a_BlockPos.addedXZ(i, j));
			if (!cBlockInfo::IsSolid(blocktype))
			{
				return false;
			}

			// Check the airspace
			for (int k = 1; k < PortalHeight; k++)
			{
				blocktype = m_World.GetBlock(a_BlockPos + Vector3i(i, k, j));
				if (blocktype != E_BLOCK_AIR)
				{
					return false;
				}
			}
		}
	}
	return true;
}





bool cNetherPortalScanner::OnAllChunksAvailable(void)
{
	if (m_FoundPortal)
	{
		// Find the bottom of this portal
		while (m_World.GetBlock(m_PortalLoc) == E_BLOCK_NETHER_PORTAL)
		{
			m_PortalLoc.y -= 1;
		}
		m_PortalLoc.y += 1;

		// Figure out which way the portal is facing
		int BXP = m_World.GetBlock(m_PortalLoc.addedX(1));
		int BXM = m_World.GetBlock(m_PortalLoc.addedX(-1));
		if ((BXP == E_BLOCK_NETHER_PORTAL) || (BXM == E_BLOCK_NETHER_PORTAL))
		{
			// The long axis is along X
			m_Dir = Direction::X;
		}
		else
		{
			// The long axis is along Z
			m_Dir = Direction::Y;
		}
	}
	else
	{
		// Scan the area for a suitable location
		int minx = FloorC(m_Position.x) - BuildSearchRadius;
		int minz = FloorC(m_Position.z) - BuildSearchRadius;
		int maxx = FloorC(m_Position.x) + BuildSearchRadius;
		int maxz = FloorC(m_Position.z) + BuildSearchRadius;
		int maxy = m_MaxY;
		std::vector<Vector3i> Possibilities;
		int x, y, z;
		for (y = 0; y < maxy - PortalHeight; y++)
		{
			for (x = minx; x < maxx - PortalLength; x++)
			{
				for (z = minz; z < maxz - SearchSolidBaseWidth; z++)
				{
					Vector3i Location = Vector3i(x, y, z);
					if (IsValidBuildLocation(Location))
					{
						Possibilities.emplace_back(x, y, z);
					}
				}
			}
		}

		if (Possibilities.size() > 0)
		{
			m_BuildPlatform = false;

			// Find the nearest
			double DistanceToClosest = (Possibilities[0] - m_Position).SqrLength();
			Vector3i Closest = Possibilities[0];
			for (const auto & itr : Possibilities)
			{
				double Distance = (itr - m_Position).SqrLength();
				if (Distance < DistanceToClosest)
				{
					DistanceToClosest = Distance;
					Closest = itr;
				}
			}

			m_PortalLoc = Closest;
		}
	}
	return true;
}





void cNetherPortalScanner::BuildNetherPortal(Vector3i a_Location, Direction a_Direction, bool a_IncludePlatform)
{
	int x = a_Location.x;
	int y = a_Location.y;
	int z = a_Location.z;

	// Clear a 3x4x4 area starting right above the base
	for (int i = 0; i < SearchSolidBaseWidth; i++)
	{
		for (int j = 0; j < PortalLength; j++)
		{
			for (int k = 1; k < PortalHeight; k++)
			{
				if (a_Direction == Direction::Y)
				{
					m_World.SetBlock({ x + i, y + k, z + j }, E_BLOCK_AIR, 0);
				}
				else if (a_Direction == Direction::X)
				{
					m_World.SetBlock({ x + j, y + k, z + i }, E_BLOCK_AIR, 0);
				}
			}
		}
	}

	// Put in an obsidian base
	if (a_IncludePlatform)
	{
		for (int j = 0; j < PortalLength; j++)
		{
			// +2 on the short axis because that's where we deposit the entity
			if (a_Direction == Direction::Y)
			{
				m_World.SetBlock({ x + 2, y, z + j }, E_BLOCK_OBSIDIAN, 0);
			}
			else if (a_Direction == Direction::X)
			{
				m_World.SetBlock({ x + j, y, z + 2 }, E_BLOCK_OBSIDIAN, 0);
			}
		}
	}

	// Build an obsidian frame
	for (int i = 0; i < PortalHeight; i++)
	{
		if (a_Direction == Direction::Y)
		{
			m_World.SetBlock({ x + 1, y + i, z }, E_BLOCK_OBSIDIAN, 0);
			m_World.SetBlock({ x + 1, y + i, z + 3 }, E_BLOCK_OBSIDIAN, 0);
		}
		else if (a_Direction == Direction::X)
		{
			m_World.SetBlock({ x, y + i, z + 1 }, E_BLOCK_OBSIDIAN, 0);
			m_World.SetBlock({ x + 3, y + i, z + 1 }, E_BLOCK_OBSIDIAN, 0);
		}
	}
	for (int i = 0; i < PortalLength; i++)
	{
		if (a_Direction == Direction::Y)
		{
			m_World.SetBlock({ x + 1, y + 4, z + i }, E_BLOCK_OBSIDIAN, 0);
			m_World.SetBlock({ x + 1, y, z + i }, E_BLOCK_OBSIDIAN, 0);
		}
		else if (a_Direction == Direction::X)
		{
			m_World.SetBlock({ x + i, y + 4, z + 1 }, E_BLOCK_OBSIDIAN, 0);
			m_World.SetBlock({ x + i, y, z + 1 }, E_BLOCK_OBSIDIAN, 0);
		}
	}

	// Fill the frame (place a fire in the bottom)
	m_World.PlaceBlock(Vector3<int>(x + 1, y + 1, z + 1), E_BLOCK_FIRE, 0);
}





void cNetherPortalScanner::OnDisabled(void)
{
	// Now we actually move the player
	if (!m_FoundPortal)
	{
		// Build a new nether portal.
		FLOGD("Building nether portal at {0}", m_PortalLoc);
		BuildNetherPortal(m_PortalLoc, m_Dir, m_BuildPlatform);
		m_PortalLoc.x += 1;
		m_PortalLoc.y += 1;
		m_PortalLoc.z += 1;
	}

	// Put the entity near the opening
	Vector3d Position = m_PortalLoc;
	if (m_Dir == Direction::Y)
	{
		Position.x += OutOffset;
		Position.z += AcrossOffset;
	}
	else if (m_Dir == Direction::X)
	{
		Position.x += AcrossOffset;
		Position.z += OutOffset;
	}

	auto EntityID = m_EntityID;
	auto & DestinationWorld = m_World;
	auto DestinationPosition = Position;

	// Lookup our warping entity by ID
	// Necessary as they may have been destroyed in the meantime (#4582)
	// And since this is called from the destination world's thread queue a task on the source world
	m_SourceWorld.QueueTask(
		[EntityID, &DestinationWorld, DestinationPosition](cWorld & a_World)
		{
			a_World.DoWithEntityByID(
				EntityID,
				[&DestinationWorld, &DestinationPosition](cEntity & a_Entity)
				{
					FLOGD("Placing player at {0}", DestinationPosition);
					a_Entity.MoveToWorld(DestinationWorld, DestinationPosition, true, false);
					return true;
				}
			);
		}
	);

	delete this;
}

