/********************************************************************
	Minecraft: Pocket Edition - Decompilation Project
	Copyright (C) 2023 iProgramInCpp

	The following code is licensed under the BSD 1 clause license.
	SPDX-License-Identifier: BSD-1-Clause
 ********************************************************************/
#include "PathFinder.hpp"
#include "world/level/Level.hpp"
#include "world/tile/DoorTile.hpp"
#include "world/entity/Entity.hpp"

static int dword_1CD868;
static int dword_1CD86C;
static int dword_1CD870;

constexpr int MakeNodeHash(int x, int y, int z)
{
	// NOTE: Same as in Java Edition Beta 1.3_01
	return (y & 0xFF) | 
		((x & 0x7FFF) << 8) | 
		((z & 0x7FFF) << 24) | 
		(x < 0 ? 0x80000000 : 0) | 
		(z < 0 ? 0x8000 : 0);
}

PathFinder::PathFinder()
{
	m_pLevel = nullptr;
}

PathFinder::~PathFinder()
{
	// NOTE: In v0.2.0, this is it. We're going to do more:
	for (size_t i = 0; i < m_nodeSpillover.size(); i++)
		delete m_nodeSpillover[i];

	m_nodeSpillover.clear();
}

int PathFinder::isFree(Entity* pEntity, int x, int y, int z, const Node* node)
{
	for (int x1 = x; x1 < x + node->m_x; x1++)
	{
		for (int y1 = y; y1 < y + node->m_y; y1++)
		{
			for (int z1 = z; z1 < z + node->m_z; z1++)
			{
				TileID id = m_pLevel->getTile(x1, y1, z1);
				if (id <= 0)
					continue;

				if (id == Tile::door_iron->m_ID || id == Tile::door_wood->m_ID)
				{
					if (!DoorTile::isOpen(m_pLevel->getData(x1, y1, z1)))
						return 0;

					continue;
				}

				Material* pMtl = Tile::tiles[id]->m_pMaterial;
				if (pMtl->blocksMotion())
					return 0;

				if (pMtl == Material::water)
					return -1;
				if (pMtl == Material::lava)
					return -2;
			}
		}
	}

	return 1; // Totally free!
}

Node* PathFinder::getNode(Entity* pEntity, int x, int y, int z, const Node* node, int a)
{
	Node* pNode = nullptr;
	if (isFree(pEntity, x, y, z, node) == 1)
		pNode = getNode(x, y, z);

	if (a > 0 && !pNode && isFree(pEntity, x, y + a, z, node) == 1)
	{
		y += a;
		pNode = getNode(x, y, z);
	}

	if (!pNode || y < 0)
		return nullptr;

	int limit = y - 4;
	while (true)
	{
		int is_free = isFree(pEntity, x, --y, z, node);

		if (is_free != 1)
		{
			if (is_free == -2)
				pNode = nullptr;
			break;
		}

		if (y == limit)
		{
			pNode = nullptr;
			break;
		}

		if (!y)
			break;

		pNode = getNode(x, y, z);
	}

	return pNode;
}

Node* PathFinder::getNode(int x, int y, int z)
{
	NodeMap::iterator iter = m_nodeMap.find(MakeNodeHash(x, y, z));

	if (iter != m_nodeMap.end())
		return iter->second;

	Node* pNode = new_Node(x, y, z);
	dword_1CD868++;
	m_nodeMap[MakeNodeHash(x, y, z)] = pNode;

	return pNode;
}

int PathFinder::getNeighbors(Entity* pEntity, Node* node1, const Node* node2, Node* node3, float maxDist)
{
	int nr = 0;
	bool isf = isFree(pEntity, node1->m_x, node1->m_y, node1->m_z, node2) == 1;

	Node* n1 = getNode(pEntity, node1->m_x, node1->m_y, node1->m_z + 1, node2, isf);
	Node* n2 = getNode(pEntity, node1->m_x - 1, node1->m_y, node1->m_z, node2, isf);
	Node* n3 = getNode(pEntity, node1->m_x + 1, node1->m_y, node1->m_z, node2, isf);
	Node* n4 = getNode(pEntity, node1->m_x, node1->m_y, node1->m_z - 1, node2, isf);

	if (n1 && !n1->field_1A && n1->distanceTo(node3) < maxDist) field_10038[nr++] = n1;
	if (n2 && !n2->field_1A && n2->distanceTo(node3) < maxDist) field_10038[nr++] = n2;
	if (n3 && !n3->field_1A && n3->distanceTo(node3) < maxDist) field_10038[nr++] = n3;
	if (n4 && !n4->field_1A && n4->distanceTo(node3) < maxDist) field_10038[nr++] = n4;

	return nr;
}

bool PathFinder::inlined_0(Path& path, Node* nodeEnd)
{
	if (dword_1CD870 < dword_1CD868)
		dword_1CD870 = dword_1CD868;

	int number = 1;
	Node* temp = nodeEnd;
	while (temp->field_10)
	{
		temp = temp->field_10;
		number++;
	}

	Node** pathNodes = new Node*[number];
	int index = number - 1;

	pathNodes[index--] = nodeEnd;

	while (nodeEnd->field_10)
	{
		pathNodes[index--] = nodeEnd->field_10;
		nodeEnd = nodeEnd->field_10;
	}

	path.setNodes(pathNodes, number);
	return true;
}

bool PathFinder::findPath(Path& path, Entity* pEntity, Node* nodeStart, Node* nodeEnd, const Node* node3, float fp)
{
	dword_1CD868 = 0;

	nodeStart->field_4 = 0;
	nodeStart->field_C = nodeStart->field_8 = nodeStart->distanceTo(nodeEnd);

	m_binaryHeap.clear();
	m_binaryHeap.insert(nodeStart);

	Node* nodep = nodeStart;

	while (true)
	{
		if (!m_binaryHeap.size())
			break;

		Node* pNode = m_binaryHeap.removeTop();
		if (pNode->equals(nodeEnd))
			return inlined_0(path, nodeEnd);

		if (nodep->distanceTo(nodeEnd) > pNode->distanceTo(nodeEnd))
			nodep = pNode;

		pNode->field_1A = true;

		int numNeighbors = getNeighbors(pEntity, pNode, node3, nodeEnd, fp);
		for (int i = 0; i < numNeighbors; i++)
		{
			Node* otherNode = field_10038[i];

			if (!otherNode->field_1A)
			{
				float dist = pNode->field_4 + pNode->distanceTo(otherNode);
				if (otherNode->field_0 < 0 || otherNode->field_4 > dist)
				{
					otherNode->field_10 = pNode;
					otherNode->field_4 = dist;
					otherNode->field_8 = otherNode->distanceTo(nodeEnd);

					if (otherNode->field_0 < 0)
					{
						otherNode->field_C = otherNode->field_4 + otherNode->field_8;
						m_binaryHeap.insert(otherNode);
					}
					else
					{
						// Update distance
						m_binaryHeap.setDistance(otherNode, otherNode->field_4 + otherNode->field_8);
					}
				}
			}
		}
	}

	if (nodep != nodeStart)
		return inlined_0(path, nodeEnd);

	return false; // no path found
}

bool PathFinder::findPath(Path& path, Entity* pEntity, float x, float y, float z, float d)
{
	// uh?
	m_nodeMap.clear();

	m_nodeCount = 0;
	// not treating spillover btw? or what

	int x1 = Mth::floor(pEntity->m_hitbox.min.x);
	int y1 = Mth::floor(pEntity->m_hitbox.min.y);
	int z1 = Mth::floor(pEntity->m_hitbox.min.z);
	Node* node1 = getNode(x1, y1, z1);

	int x2 = Mth::floor(x - 0.5f * pEntity->field_88);
	int y2 = Mth::floor(y);
	int z2 = Mth::floor(z - 0.5f * pEntity->field_88);
	Node* node2 = nullptr;

	if (!m_pLevel->getTile(x2, y2 - 1, z2))
	{
		for (int x3 = x2; x3 <= Mth::floor(x + 0.5f * pEntity->field_88); x3++)
		{
			for (int z3 = z2; z3 <= Mth::floor(y + 0.5f * pEntity->field_88); z3++)
			{
				if (m_pLevel->getTile(x3, y2 - 1, z3))
				{
					node2 = getNode(x3, y2, z3);
					break; // breaking out of the z3 loop only.  Intended to break out of x3 too?
				}
			}
		}
	}
	
	if (!node2)
		node2 = getNode(x2, y2, z2);

	int x4 = Mth::floor(pEntity->field_88 + 1.0f);
	int y4 = Mth::floor(pEntity->field_8C + 1.0f);
	int z4 = Mth::floor(pEntity->field_88 + 1.0f);

	Node node3;
	node3.setPos(x4, y4, z4);
	node3.setHash(MakeNodeHash(x4, y4, z4));

	bool foundPath = findPath(path, pEntity, node1, node2, &node3, d);

	if (m_nodeCount > 2048)
	{
		// huh.
		for (size_t i = 0; i < m_nodeSpillover.size(); i++)
			delete m_nodeSpillover[i];

		m_nodeSpillover.clear();
	}

	return foundPath;
}

Node* PathFinder::new_Node(int x, int y, int z)
{
	int nodeID = m_nodeCount++;

	Node* pNode;

	if (m_nodeCount < MAX_NODE_COUNT)
	{
		// Allocate from node reserve
		pNode = &m_nodeReserve[nodeID];
		pNode->init();
		pNode->setPos(x, y, z);
		pNode->setHash(MakeNodeHash(x, y, z));
	}
	else
	{
		pNode = new Node;
		pNode->setPos(x, y, z);
		pNode->setHash(MakeNodeHash(x, y, z));
		m_nodeSpillover.push_back(pNode);
	}

	return pNode;
}
