//
// Lol Engine
//
// Copyright: (c) 2010-2012 Sam Hocevar <sam@hocevar.net>
//            (c) 2009-2012 Cédric Lecacheur <jordx@free.fr>
//            (c) 2009-2012 Benjamin Huet <huet.benjamin@gmail.com>
//   This program is free software; you can redistribute it and/or
//   modify it under the terms of the Do What The Fuck You Want To
//   Public License, Version 2, as published by Sam Hocevar. See
//   http://sam.zoy.org/projects/COPYING.WTFPL for more details.
//

#if defined HAVE_CONFIG_H
#   include "config.h"
#endif

#define USE_LOL_CTRLR_CHARAC

#ifdef HAVE_PHYS_USE_BULLET
#include "core.h"
#include <stdio.h>
#include "../Include/LolBtPhysicsIntegration.h"
#include "../Include/LolPhysics.h"
#include "../Include/EasyCharacterController.h"
#include "../Include/BulletCharacterController.h"
//#include "LinearMath/btIDebugDraw.h"
//#include "BulletCollision/CollisionDispatch/btGhostObject.h"
//#include "BulletCollision/CollisionShapes/btMultiSphereShape.h"
//#include "BulletCollision/BroadphaseCollision/btOverlappingPairCache.h"
//#include "BulletCollision/BroadphaseCollision/btCollisionAlgorithm.h"
//#include "BulletCollision/CollisionDispatch/btCollisionWorld.h"
//#include "LinearMath/btDefaultMotionState.h"
#endif //HAVE_PHYS_USE_BULLET


namespace lol
{

namespace phys
{

#ifdef USE_LOL_CTRLR_CHARAC
#ifdef HAVE_PHYS_USE_BULLET

//SweepCallback used for Swweep Tests.
class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback
{
public:
	ClosestNotMeConvexResultCallback(btCollisionObject* NewMe, const vec3& NewUp, float MinSlopeDot) :
					btCollisionWorld::ClosestConvexResultCallback(LOL2BTU_VEC3(vec3(.0f)), LOL2BTU_VEC3(vec3(.0f))),
					m_me(NewMe),
					m_up(NewUp),
					m_min_slope_dot(MinSlopeDot) { }

	virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& ConvexResult, bool NormalInWorld)
	{
		//We hit ourselves, FAIL
		if (ConvexResult.m_hitCollisionObject == m_me)
			return btScalar(1.f);

		vec3 WorldHitNomal(.0f);
		if (NormalInWorld)
			WorldHitNomal = BT2LOL_VEC3(ConvexResult.m_hitNormalLocal);
		else //need to transform Normal into worldspace
		{
			btVector3 TmpWorldHitNormal = ConvexResult.m_hitCollisionObject->getWorldTransform().getBasis() * ConvexResult.m_hitNormalLocal;
			WorldHitNomal = BT2LOL_VEC3(TmpWorldHitNormal);
		}

		float DotUp = dot(m_up, WorldHitNomal);
		//We hit below the accepted slope_dot, FAIL
		if (DotUp < m_min_slope_dot)
			return btScalar(1.f);

		//Continue to next.
		return ClosestConvexResultCallback::addSingleResult(ConvexResult, NormalInWorld);
	}
protected:
	btCollisionObject*	m_me;
	const vec3			m_up;
	float				m_min_slope_dot;
};

//When called, will try to remove Character controller from its collision.
bool BulletKinematicCharacterController::RecoverFromPenetration(btCollisionWorld* CollisionWorld)
{
	bool HasPenetration = false;

	//Retrieve all pair with us colliding.
	CollisionWorld->getDispatcher()->dispatchAllCollisionPairs(m_ghost_object->getOverlappingPairCache(), CollisionWorld->getDispatchInfo(), CollisionWorld->getDispatcher());
	m_current_position = BT2LOLU_VEC3(m_ghost_object->getWorldTransform().getOrigin());
	
	float MaxPen = .0f;
	for (int i = 0; i < m_ghost_object->getOverlappingPairCache()->getNumOverlappingPairs(); i++)
	{
		m_manifold_array.resize(0);

		//this is the equivalent of the "Touch algorithm". Maybe refactor ?
		btBroadphasePair* CollisionPair = &m_ghost_object->getOverlappingPairCache()->getOverlappingPairArray()[i];
		if (CollisionPair->m_algorithm)
			CollisionPair->m_algorithm->getAllContactManifolds(m_manifold_array);
		
		for (int j = 0; j < m_manifold_array.size(); ++j)
		{
			btPersistentManifold* CurMfold = m_manifold_array[j];
			//Normal direction differs if we're Body0
			float DirSign = CurMfold->getBody0() == m_ghost_object ? -1.f : 1.f;

			for (int k = 0; k < CurMfold->getNumContacts(); k++)
			{
				const btManifoldPoint& MfPoint = CurMfold->getContactPoint(k);
				float Dist = MfPoint.getDistance();
				if (Dist < .0f)
				{
					if (Dist < MaxPen)
					{
						MaxPen = Dist;
						m_touching_normal = BT2LOL_VEC3(MfPoint.m_normalWorldOnB) * DirSign;
					}
					m_current_position += BT2LOL_VEC3(MfPoint.m_normalWorldOnB) * DirSign * Dist * .2f;
					HasPenetration = true;
				}
			}
		}
	}

	btTransform GObjMx = m_ghost_object->getWorldTransform();
	GObjMx.setOrigin(LOL2BTU_VEC3(m_current_position));
	m_ghost_object->setWorldTransform(GObjMx);

	return HasPenetration;
}

//When the Controller hits a wall, we modify the target so the controller will MoveStep along the wall.
void BulletKinematicCharacterController::UpdateTargetOnHit(const vec3& HitNormal, float TangentMag, float NormalMag)
{
	vec3 Movedir = m_target_position - m_current_position;
	float MoveLength = (float)length(Movedir);

	if (MoveLength > SIMD_EPSILON)
	{
		Movedir = normalize(Movedir);

		vec3 ReflectDir = normalize(GetReflectedDir(Movedir, HitNormal));
		vec3 ParallelDir = ProjectDirOnNorm(ReflectDir, HitNormal);
		vec3 PerpindicularDir = ProjectDirOnNormPerpindicular(ReflectDir, HitNormal);

		m_target_position = m_current_position;

		if (NormalMag != .0f)
			m_target_position += PerpindicularDir * NormalMag * MoveLength;
	}
}

//Handles the Step-Up : Currently taking into account Stair step & Jump.
void BulletKinematicCharacterController::StepUp(btCollisionWorld* world)
{
	// phase 1: up
	vec3 UpDir = GetUpAxisDirections()[m_up_axis];
	btTransform SweepStart, SweepEnd;
	SweepStart.setIdentity();
	SweepEnd.setIdentity();

	m_target_position = m_current_position + UpDir * (m_step_height + (m_vertical_offset > 0.f ? m_vertical_offset : 0.f));

	/* FIXME: Handle HasPenetration properly */
	SweepStart.setOrigin(LOL2BTU_VEC3(m_current_position + UpDir * (m_convex_shape->getMargin() + m_added_margin)));
	SweepEnd.setOrigin(LOL2BTU_VEC3(m_target_position));

	ClosestNotMeConvexResultCallback SweepCallback(m_ghost_object, -UpDir, float(0.7071));
	SweepCallback.m_collisionFilterGroup = GetGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup;
	SweepCallback.m_collisionFilterMask = GetGhostObject()->getBroadphaseHandle()->m_collisionFilterMask;
	
	if (m_do_gobject_sweep_test)
		m_ghost_object->convexSweepTest(m_convex_shape, SweepStart, SweepEnd, SweepCallback, world->getDispatchInfo().m_allowedCcdPenetration);
	else
		world->convexSweepTest(m_convex_shape, SweepStart, SweepEnd, SweepCallback);
	
	if (SweepCallback.hasHit())
	{
		// Only modify the position if the hit was a slope and not a wall or ceiling.
		if(SweepCallback.m_hitNormalWorld.dot(LOL2BTU_VEC3(UpDir)) > .0f)
		{
			// we moved up only a Fraction of the step height
			m_current_step_offset = m_step_height * SweepCallback.m_closestHitFraction;
			btVector3 InterpolPos; //TODO : REPLACE BY INTERPOLATE3/LERP(VEC3)
			InterpolPos.setInterpolate3(LOL2BTU_VEC3(m_current_position), LOL2BTU_VEC3(m_target_position), SweepCallback.m_closestHitFraction);
			m_current_position = BT2LOLU_VEC3(InterpolPos);
		}
		m_vertical_velocity = .0f;
		m_vertical_offset = .0f;
	}
	else
	{
		m_current_step_offset = m_step_height;
		m_current_position = m_target_position;
	}
}

//Handles the actual Movement. It actually moves in the 3 dimensions, function name is confusing.
void BulletKinematicCharacterController::StepForwardAndStrafe(btCollisionWorld* CollisionWorld, const vec3& MoveStep)
{
	// phase 2: forward and strafe
	m_target_position = m_current_position + MoveStep;
	btTransform SweepStart, SweepEnd;
	SweepStart.setIdentity();
	SweepEnd.setIdentity();

	float Fraction = 1.f;
	float SqDist = .0f;

	if (m_touching_contact && dot(m_normalized_direction, m_touching_normal) > .0f)
		UpdateTargetOnHit(m_touching_normal);

	//Let's loop on movement, until Movement fraction if below 0.01, which means we've reached our destination.
	//Or until we'tried 10 times.
	int MaxMoveLoop = 10;
	while (Fraction > .01f && MaxMoveLoop-- > 0)
	{
		SweepStart.setOrigin(LOL2BTU_VEC3(m_current_position));
		SweepEnd.setOrigin(LOL2BTU_VEC3(m_target_position));
		vec3 SweepDirNeg(m_current_position - m_target_position);

		ClosestNotMeConvexResultCallback SweepCallback(m_ghost_object, SweepDirNeg, .0f);
		SweepCallback.m_collisionFilterGroup = GetGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup;
		SweepCallback.m_collisionFilterMask = GetGhostObject()->getBroadphaseHandle()->m_collisionFilterMask;

		//The sweep test is done with an added margin, so we use it and then discard it
		float SavedMargin = m_convex_shape->getMargin();
		m_convex_shape->setMargin(SavedMargin + m_added_margin); //Apply Added Margin
		if (m_do_gobject_sweep_test)
			m_ghost_object->convexSweepTest (m_convex_shape, SweepStart, SweepEnd, SweepCallback, CollisionWorld->getDispatchInfo().m_allowedCcdPenetration);
		else
			CollisionWorld->convexSweepTest (m_convex_shape, SweepStart, SweepEnd, SweepCallback, CollisionWorld->getDispatchInfo().m_allowedCcdPenetration);
		m_convex_shape->setMargin(SavedMargin); //Restore saved margin

		Fraction -= SweepCallback.m_closestHitFraction;

		if (SweepCallback.hasHit())
		{	
			//We moved only a Fraction
			float HitDist = (float)length(BT2LOLU_VEC3(SweepCallback.m_hitPointWorld) - m_current_position);

			UpdateTargetOnHit(BT2LOL_VEC3(SweepCallback.m_hitNormalWorld));
			vec3 NewDir = m_target_position - m_current_position;
			SqDist = sqlength(NewDir);
			if (SqDist > SIMD_EPSILON)
			{
				NewDir = normalize(NewDir);
				//See Quake2: "If velocity is against original velocity, stop ead to avoid tiny oscilations in sloping corners."
				if (dot(NewDir, m_normalized_direction) <= .0f)
					break;
			}
			else
				break;
		}
		else //We moved whole way
			m_current_position = m_target_position;
	}
}

//Handles the Step-down : We go back on the ground at the end of the MoveStep.
void BulletKinematicCharacterController::StepDown(btCollisionWorld* CollisionWorld, float DeltaTime)
{
	// phase 3: down
	vec3 UpDir = GetUpAxisDirections()[m_up_axis];
	btTransform SweepStart, SweepEnd;
	SweepStart.setIdentity();
	SweepEnd.setIdentity();

	float DownVel = (m_vertical_velocity < 0.f ? -m_vertical_velocity : 0.f) * DeltaTime;
	if (DownVel > .0f && DownVel < m_step_height && (m_was_on_ground || !m_was_jumping))
		DownVel = m_step_height;

	vec3 StepDrop = UpDir * (m_current_step_offset + DownVel);
	m_target_position -= StepDrop;

	SweepStart.setOrigin(LOL2BTU_VEC3(m_current_position));
	SweepEnd.setOrigin(LOL2BTU_VEC3(m_target_position));

	ClosestNotMeConvexResultCallback SweepCallback(m_ghost_object, UpDir, m_max_slope_cosine);
	SweepCallback.m_collisionFilterGroup = GetGhostObject()->getBroadphaseHandle()->m_collisionFilterGroup;
	SweepCallback.m_collisionFilterMask = GetGhostObject()->getBroadphaseHandle()->m_collisionFilterMask;
	
	if (m_do_gobject_sweep_test)
		m_ghost_object->convexSweepTest(m_convex_shape, SweepStart, SweepEnd, SweepCallback, CollisionWorld->getDispatchInfo().m_allowedCcdPenetration);
	else
		CollisionWorld->convexSweepTest(m_convex_shape, SweepStart, SweepEnd, SweepCallback, CollisionWorld->getDispatchInfo().m_allowedCcdPenetration);

	if (SweepCallback.hasHit())
	{
		// we dropped a Fraction of the height -> hit floor
		btVector3 InterpolPos; //TODO : REPLACE BY INTERPOLATE3/LERP(VEC3)
		InterpolPos.setInterpolate3(LOL2BTU_VEC3(m_current_position), LOL2BTU_VEC3(m_target_position), SweepCallback.m_closestHitFraction);
		m_current_position = BT2LOLU_VEC3(InterpolPos);
		m_vertical_velocity = .0f;
		m_vertical_offset = .0f;
		m_was_jumping = false;
	}
	else // we dropped the full height
		m_current_position = m_target_position;
}

//The PreStepis done in order to recover from any HasPenetration.
void BulletKinematicCharacterController::PreStep(btCollisionWorld* CollisionWorld)
{
	int MaxPenetrationLoop = 0;
	m_touching_contact = false;

	while (RecoverFromPenetration(CollisionWorld))
	{
		MaxPenetrationLoop++;
		m_touching_contact = true;
		if (MaxPenetrationLoop > 4)
			break;
	}

	m_current_position = BT2LOLU_VEC3(m_ghost_object->getWorldTransform().getOrigin());
	m_target_position = m_current_position;
}

//And so we step :
//StepUpfirst, then movement, then StepDownon the ground.
void BulletKinematicCharacterController::PlayerStep(btCollisionWorld* CollisionWorld, float DeltaTime)
{
	// quick check...
	if (!m_use_walk_direction && m_velocity_time_interval <= .0f)
		return;		// no motion

	m_was_on_ground = OnGround();

	// Update fall velocity.
	m_vertical_velocity -= m_gravity * DeltaTime;
	if(m_vertical_velocity > .0f && m_vertical_velocity > m_jump_speed)
		m_vertical_velocity = m_jump_speed;

	if(m_vertical_velocity < .0f && btFabs(m_vertical_velocity) > btFabs(m_fall_speed))
		m_vertical_velocity = -btFabs(m_fall_speed);
	m_vertical_offset = m_vertical_velocity * DeltaTime;

	btTransform NewTransform;
	NewTransform = m_ghost_object->getWorldTransform();

	vec3 MoveStep(.0f);
	if (m_use_walk_direction)
		MoveStep = m_walk_direction;
	else
	{
		//Still have some time left for moving!
		float dtMoving = (DeltaTime < m_velocity_time_interval) ? DeltaTime : m_velocity_time_interval;
		m_velocity_time_interval -= DeltaTime;

		// how far will we MoveStep while we are moving?
		MoveStep = m_walk_direction * dtMoving;
	}

	//Okay, step !
	StepUp(CollisionWorld);
	StepForwardAndStrafe(CollisionWorld, MoveStep);
	StepDown(CollisionWorld, DeltaTime);

	//Movement finished, update World transform
	NewTransform.setOrigin(LOL2BTU_VEC3(m_current_position));
	m_ghost_object->setWorldTransform(NewTransform);
}

//should MoveStep Jump logic in EasyCC
void BulletKinematicCharacterController::Jump()
{
	if (!CanJump())
		return;

	m_vertical_velocity = m_jump_speed;
	m_was_jumping = true;
}

#endif // HAVE_PHYS_USE_BULLET
#endif // USE_LOL_CTRLR_CHARAC

	} /* namespace phys */

} /* namespace lol */