//-----------------------------------------------------------------------------------------------
// Raycast2D.hpp
//
// Raycast approach inspired by "A Fast Voxel Traversal Algorithm for Ray Tracing" by John Amanatides & Andrew Woo:
// http://www.cse.chalmers.se/edu/course/TDA361/grid.pdf
//
#pragma once
#ifndef INCLUDED_Raycast2D
#define INCLUDED_Raycast2D

//-----------------------------------------------------------------------------------------------
#include "Engine/Core/MathBase.hpp"
#include "Engine/Core/Vector2.hpp"


//-----------------------------------------------------------------------------------------------
typedef bool (Raycast2DIsTileSolidFunction)( const IntVector2& tileCoords );


/////////////////////////////////////////////////////////////////////////////////////////////////
class TraceResult2D
{
public:
	TraceResult2D();
	bool DidImpact() const { return m_impactTileCoords.x != -1 && m_impactTileCoords.y != -1; }

public:
	Vector2			m_impactPosition;
	IntVector2		m_impactTileCoords;
	Vector2			m_impactSurfaceNormal;
	float			m_impactFraction;
	bool			m_didImpact;
};


//-----------------------------------------------------------------------------------------------
void Raycast2D( const Vector2& start, const Vector2& end, TraceResult2D& traceResult, Raycast2DIsTileSolidFunction* isTileSolidCallback );

//-----------------------------------------------------------------------------------------------
template< typename T_CallbackObjectType, typename T_CallbackMethodType >
void Raycast2D( const Vector2& start, const Vector2& end, TraceResult2D& traceResult, T_CallbackObjectType& callbackObject, T_CallbackMethodType isTileSolidCallbackMethod );


//-----------------------------------------------------------------------------------------------
template< typename T_CallbackObjectType, typename T_CallbackMethodType >
void Raycast2D( const Vector2& start, const Vector2& end, TraceResult2D& traceResult, T_CallbackObjectType& callbackObject, T_CallbackMethodType isTileSolidCallbackMethod )
{
	IntVector2 currentTileCoords( static_cast< int >( floor( start.x ) ), static_cast< int >( floor( start.y ) ) );

	if( (callbackObject.*isTileSolidCallbackMethod)( currentTileCoords ) )
	{
		// Ray start point is inside solid
		traceResult.m_didImpact = true;
		traceResult.m_impactTileCoords = currentTileCoords;
		traceResult.m_impactFraction = 0.f;
		traceResult.m_impactPosition = start;
		traceResult.m_impactSurfaceNormal.SetXY( 0.f, 0.f );
		return;
	}

	const float VERY_LARGE_FLOAT = FLT_MAX;
	const Vector2 rayDisplacement = end - start;
	const float tDeltaX = rayDisplacement.x != 0.f ? abs(1.f / rayDisplacement.x) : VERY_LARGE_FLOAT;
	const float tDeltaY = rayDisplacement.y != 0.f ? abs(1.f / rayDisplacement.y) : VERY_LARGE_FLOAT;
	const IntVector2 tileStepDirection( rayDisplacement.x < 0.f ? -1 : 1, rayDisplacement.y < 0.f ? -1 : 1 );
	const int offsetToLeadingEdgeX = (tileStepDirection.x + 1) >> 1;
	const int offsetToLeadingEdgeY = (tileStepDirection.y + 1) >> 1;
	const float firstVerticalIntersectionX = static_cast< float >( currentTileCoords.x + offsetToLeadingEdgeX );
	const float firstHorizontalIntersectionY = static_cast< float >( currentTileCoords.y + offsetToLeadingEdgeY);
	float tOfNextXCrossing = abs(firstVerticalIntersectionX - start.x) * tDeltaX;
	float tOfNextYCrossing = abs(firstHorizontalIntersectionY - start.y) * tDeltaY;

	for( ;; )
	{
		if( tOfNextXCrossing < tOfNextYCrossing )
		{
			if( tOfNextXCrossing > 1.f )
				break;

			currentTileCoords.x += tileStepDirection.x;
			if( (callbackObject.*isTileSolidCallbackMethod)( currentTileCoords ) )
			{
				// Impact on solid tile!
				traceResult.m_didImpact = true;
				traceResult.m_impactTileCoords = currentTileCoords;
				traceResult.m_impactFraction = tOfNextXCrossing;
				traceResult.m_impactPosition = start + (rayDisplacement * tOfNextXCrossing);
				traceResult.m_impactSurfaceNormal.SetXY( static_cast< float >( -tileStepDirection.x ), 0.f );
				return;
			}

			tOfNextXCrossing += tDeltaX;
		}
		else
		{
			if( tOfNextYCrossing > 1.f )
				break;

			currentTileCoords.y += tileStepDirection.y;
			if( (callbackObject.*isTileSolidCallbackMethod)( currentTileCoords ) )
			{
				// Impact on solid tile!
				traceResult.m_didImpact = true;
				traceResult.m_impactTileCoords = currentTileCoords;
				traceResult.m_impactFraction = tOfNextYCrossing;
				traceResult.m_impactPosition = start + (rayDisplacement * tOfNextYCrossing);
				traceResult.m_impactSurfaceNormal.SetXY( 0.f, static_cast< float >( -tileStepDirection.y ) );
				return;
			}

			tOfNextYCrossing += tDeltaY;
		}
	}

	traceResult.m_didImpact = false;
	traceResult.m_impactTileCoords.SetXY( -1, -1 );
	traceResult.m_impactFraction = 1.f;
	traceResult.m_impactPosition = end;
	traceResult.m_impactSurfaceNormal.SetXY( 0.f, 0.f );
	return;
}




#endif // INCLUDED_Raycast2D
