
#include "Globals.h"

#include "../BlockInfo.h"
#include "../BoundingBox.h"
#include "../Chunk.h"
#include "Floater.h"
#include "Player.h"
#include "../ClientHandle.h"




////////////////////////////////////////////////////////////////////////////////
// cFloaterEntityCollisionCallback
class cFloaterEntityCollisionCallback
{
public:
	cFloaterEntityCollisionCallback(cFloater * a_Floater, const Vector3d & a_Pos, const Vector3d & a_NextPos) :
		m_Floater(a_Floater),
		m_Pos(a_Pos),
		m_NextPos(a_NextPos),
		m_MinCoeff(1),
		m_HitEntity(nullptr)
	{
	}
	bool operator () (cEntity & a_Entity)
	{
		if (!a_Entity.IsMob())  // Floaters can only pull mobs not other entities.
		{
			return false;
		}

		auto EntBox = a_Entity.GetBoundingBox();

		double LineCoeff;
		eBlockFace Face;
		EntBox.Expand(m_Floater->GetWidth() / 2, m_Floater->GetHeight() / 2, m_Floater->GetWidth() / 2);
		if (!EntBox.CalcLineIntersection(m_Pos, m_NextPos, LineCoeff, Face))
		{
			// No intersection whatsoever
			return false;
		}

		if (LineCoeff < m_MinCoeff)
		{
			// The entity is closer than anything we've stored so far, replace it as the potential victim
			m_MinCoeff = LineCoeff;
			m_HitEntity = &a_Entity;
		}

		// Don't break the enumeration, we want all the entities
		return false;
	}

	/** Returns the nearest entity that was hit, after the enumeration has been completed */
	cEntity * GetHitEntity(void) const { return m_HitEntity; }

	/** Returns true if the callback has encountered a true hit */
	bool HasHit(void) const { return (m_MinCoeff < 1); }

protected:
	cFloater * m_Floater;
	const Vector3d & m_Pos;
	const Vector3d & m_NextPos;
	double m_MinCoeff;  // The coefficient of the nearest hit on the Pos line

	// Although it's bad(tm) to store entity ptrs from a callback, we can afford it here, because the entire callback
	// is processed inside the tick thread, so the entities won't be removed in between the calls and the final processing
	cEntity * m_HitEntity;  // The nearest hit entity
} ;





cFloater::cFloater(Vector3d a_Pos, Vector3d a_Speed, UInt32 a_PlayerID, int a_CountDownTime) :
	Super(etFloater, a_Pos, 0.25f, 0.25f),
	m_BitePos(a_Pos),
	m_CanPickupItem(false),
	m_PickupCountDown(0),
	m_CountDownTime(a_CountDownTime),
	m_PlayerID(a_PlayerID),
	m_AttachedMobID(cEntity::INVALID_ID)
{
	SetSpeed(a_Speed);
}





void cFloater::SpawnOn(cClientHandle & a_Client)
{
	a_Client.SendSpawnEntity(*this);
}





void cFloater::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
{
	HandlePhysics(a_Dt, a_Chunk);

	PREPARE_REL_AND_CHUNK(GetPosition().Floor(), a_Chunk);
	if (!RelSuccess)
	{
		return;
	}

	auto & Random = GetRandomProvider();
	if (IsBlockWater(Chunk->GetBlock(Rel)) && (Chunk->GetMeta(Rel) == 0))
	{
		if (!m_CanPickupItem && (m_AttachedMobID == cEntity::INVALID_ID))  // Check if you can't already pickup a fish and if the floater isn't attached to a mob.
		{
			if (m_CountDownTime <= 0)
			{
				m_BitePos = GetPosition();
				m_World->BroadcastSoundEffect("entity.bobber.splash", GetPosition(), 1, 1);
				AddSpeedY(-10);
				m_CanPickupItem = true;
				m_PickupCountDown = 20;
				m_CountDownTime = Random.RandInt(100, 900);
				LOGD("Floater %i can be picked up", GetUniqueID());
			}
			else if (m_CountDownTime == 20)  // Calculate the position where the particles should spawn and start producing them.
			{
				LOGD("Started producing particles for floater %i", GetUniqueID());
				m_ParticlePos.Set(GetPosX() + Random.RandInt(-4, 4), GetPosY(), GetPosZ() + Random.RandInt(-4, 4));
				m_World->BroadcastParticleEffect("splash", static_cast<Vector3f>(m_ParticlePos), Vector3f{}, 0, 15);
			}
			else if (m_CountDownTime < 20)
			{
				m_ParticlePos = (m_ParticlePos + (GetPosition() - m_ParticlePos) / 6);
				m_World->BroadcastParticleEffect("splash", static_cast<Vector3f>(m_ParticlePos), Vector3f{}, 0, 15);
			}

			m_CountDownTime--;
			if (Chunk->IsWeatherWetAt(Rel))
			{
				if (Random.RandBool(0.25))  // 25% chance of an extra countdown when being rained on.
				{
					m_CountDownTime--;
				}
			}
			else  // if the floater is underground it has a 50% chance of not decreasing the countdown.
			{
				if (Random.RandBool())
				{
					m_CountDownTime++;
				}
			}
		}
	}

	// Check water at the top of floater otherwise it floats into the air above the water
	if (
		const auto Above = Rel.addedY(FloorC(GetPosY() + GetHeight()));
		(Above.y < cChunkDef::Height) && IsBlockWater(m_World->GetBlock(Above))
	)
	{
		SetSpeedY(0.7);
	}

	if (CanPickup())  // Make sure the floater "loses its fish"
	{
		m_PickupCountDown--;
		if (m_PickupCountDown == 0)
		{
			m_CanPickupItem = false;
			LOGD("The fish is gone. Floater %i can not pick an item up.", GetUniqueID());
		}
	}

	if ((GetSpeed().Length() > 4) && (m_AttachedMobID == cEntity::INVALID_ID))
	{
		cFloaterEntityCollisionCallback Callback(this, GetPosition(), GetPosition() + GetSpeed() / 20);

		a_Chunk.ForEachEntity(Callback);
		if (Callback.HasHit())
		{
			AttachTo(*Callback.GetHitEntity());
			Callback.GetHitEntity()->TakeDamage(*this);  // TODO: the player attacked the mob not the floater.
			m_AttachedMobID = Callback.GetHitEntity()->GetUniqueID();
		}
	}

	if (!m_World->DoWithEntityByID(m_PlayerID, [](cEntity &) { return true; }))  // The owner doesn't exist anymore. Destroy the floater entity.
	{
		Destroy();
	}

	if (m_AttachedMobID != cEntity::INVALID_ID)
	{
		if (!m_World->DoWithEntityByID(m_AttachedMobID, [](cEntity &) { return true; }))
		{
			// The mob the floater was attached to doesn't exist anymore.
			m_AttachedMobID = cEntity::INVALID_ID;
		}
	}

	SetSpeedX(GetSpeedX() * 0.95);
	SetSpeedZ(GetSpeedZ() * 0.95);

	BroadcastMovementUpdate();
}
