/*
* Copyright (c) 2016-2019 Irlan Robson https://irlanrobson.github.io
*
* This software is provided 'as-is', without any express or implied
* warranty.  In no event will the authors be held liable for any damages
* arising from the use of this software.
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
*/

#ifndef CHARACTER_TEST_H
#define CHARACTER_TEST_H

struct Plane
{
	b3Vec3 n;
	b3Vec3 p;
};

class CharacterController
{
public:
	CharacterController(b3SphereShape* shape)
	{
		m_characterShape = shape;
		m_characterBody = m_characterShape->GetBody();
		m_world = m_characterBody->GetWorld();
		m_translation.SetZero();
		m_isGrounded = false;
		
		UpdateGrounded();
	}

	~CharacterController()
	{

	}

	bool IsGrounded() const
	{
		return m_isGrounded;
	}

	void Move(const b3Vec3& translation)
	{
		m_translation += translation;
	}

	void Step()
	{
		Solve();

		UpdateGrounded();
	}
private:
	b3Sphere GetCharacterSphere() const
	{
		b3Sphere sphere;
		sphere.vertex = m_characterBody->GetWorldPoint(m_characterShape->m_center);
		sphere.radius = m_characterShape->m_radius;
		return sphere;
	}
	
	void UpdateGrounded()
	{
		b3StackArray<b3Shape*, 32> shapes;
		CollectStaticShapes(shapes);

		b3StackArray<Plane, 32> planes;
		CollectOverlapPlanes(planes, shapes);

		m_isGrounded = false;
		
		for (u32 i = 0; i < planes.Count(); ++i)
		{
			Plane plane = planes[i];

			float32 cosine = b3Dot(b3Vec3_y, plane.n);
			
			// ~80 degrees
			const float32 kTol = 0.17f;
			
			if (cosine > kTol)
			{
 				m_isGrounded = true;
				break;
			}
		}
	}

	void CollectStaticShapes(b3Array<b3Shape*>& shapes) const
	{
		for (b3Body* b = m_world->GetBodyList().m_head; b; b = b->GetNext())
		{
			if (b == m_characterBody)
			{
				continue;
			}

			if (b->GetType() != e_staticBody)
			{
				continue;
			}

			for (b3Shape* s = b->GetShapeList().m_head; s; s = s->GetNext())
			{
				shapes.PushBack(s);
			}
		}
	}

	void CollectOverlapPlanes(b3Array<Plane>& planes,
		const b3Array<b3Shape*>& shapes) const
	{
		b3Sphere sphere2 = GetCharacterSphere();

		for (u32 i = 0; i < shapes.Count(); ++i)
		{
			b3Shape* shape1 = shapes[i];
			b3Body* body1 = shape1->GetBody();
			b3Transform xf1 = body1->GetTransform();

			if (shape1->GetType() == e_meshShape)
			{
				b3MeshShape* meshShape1 = (b3MeshShape*)shape1;
				const b3Mesh* mesh1 = meshShape1->m_mesh;

				for (u32 j = 0; j < mesh1->triangleCount; ++j)
				{
					b3TestSphereOutput output;
					bool overlap = meshShape1->TestSphere(&output, sphere2, xf1, j);

					if (overlap)
					{
						Plane plane;
						plane.n = output.normal;
						plane.p = output.point;

						planes.PushBack(plane);
					}
				}
			}
			else
			{
				b3TestSphereOutput output;
				bool overlap = shape1->TestSphere(&output, sphere2, xf1);

				if (overlap)
				{
					Plane plane;
					plane.n = output.normal;
					plane.p = output.point;

					planes.PushBack(plane);
				}
			}
		}
	}

	void CollectSweepPlanes(b3Array<Plane>& planes,
		const b3Array<b3Shape*>& shapes,
		const b3Vec3& translation2) const
	{
		if (b3LengthSquared(translation2) < B3_EPSILON * B3_EPSILON)
		{
			return;
		}

		b3Transform xf2 = m_characterBody->GetTransform();

		b3ShapeGJKProxy proxy2;
		proxy2.Set(m_characterShape, 0);

		for (u32 i = 0; i < shapes.Count(); ++i)
		{
			b3Shape* shape1 = shapes[i];
			b3Body* body1 = shape1->GetBody();

			b3Transform xf1 = body1->GetTransform();

			if (shape1->GetType() == e_meshShape)
			{
				b3MeshShape* meshShape1 = (b3MeshShape*)shape1;
				const b3Mesh* mesh1 = meshShape1->m_mesh;

				for (u32 j = 0; j < mesh1->triangleCount; ++j)
				{
					b3ShapeGJKProxy proxy1;
					proxy1.Set(shape1, j);

					b3GJKShapeCastOutput output;
					bool hit = b3GJKShapeCast(&output, xf1, proxy1, xf2, proxy2, translation2);

					if (hit)
					{
						Plane plane;
						plane.n = output.normal;
						plane.p = output.point;

						planes.PushBack(plane);
					}
				}
			}
			else
			{
				b3ShapeGJKProxy proxy1;
				proxy1.Set(shape1, 0);

				b3GJKShapeCastOutput output;
				bool hit = b3GJKShapeCast(&output, xf1, proxy1, xf2, proxy2, translation2);

				if (hit)
				{
					Plane plane;
					plane.n = output.normal;
					plane.p = output.point;

					planes.PushBack(plane);
				}
			}
		}
	}

	b3Vec3 SolvePositionConstraints(const b3Array<Plane>& planes, const b3Vec3& position) const
	{
		b3Vec3 localCenter = m_characterBody->GetLocalCenter();
		b3Vec3 c = position;
		b3Quat q = m_characterBody->GetOrientation();

		const u32 kMaxPositionIterations = 10;

		for (u32 i = 0; i < kMaxPositionIterations; ++i)
		{
			for (u32 j = 0; j < planes.Count(); ++j)
			{
				Plane nc = planes[j];
				b3Plane plane(nc.n, nc.p);

				b3Transform xf;
				xf.rotation = b3QuatMat33(q);
				xf.position = c - b3Mul(xf.rotation, localCenter);

				b3Sphere sphere;
				sphere.vertex = b3Mul(xf, m_characterShape->m_center);
				sphere.radius = m_characterShape->m_radius;

				float32 separation = b3Distance(sphere.vertex, plane);

				if (separation > sphere.radius)
				{
					continue;
				}

				separation -= sphere.radius;

				float32 C = b3Clamp(B3_BAUMGARTE * (separation + B3_LINEAR_SLOP), -B3_MAX_LINEAR_CORRECTION, 0.0f);
				
				b3Vec3 normal = plane.normal;
				b3Vec3 P = C * normal;

				c -= P;
			}
		}

		return c;
	}

	void Solve()
	{
		b3StackArray<b3Shape*, 32> shapes;
		CollectStaticShapes(shapes);

		// Collect collision planes
		b3StackArray<Plane, 32> planes;
		CollectOverlapPlanes(planes, shapes);

		b3Vec3 startPosition = m_characterBody->GetWorldCenter();

		b3Vec3 targetPosition = startPosition + m_translation;

		m_translation.SetZero();

		b3Vec3 solvePosition = SolvePositionConstraints(planes, targetPosition);

		b3Vec3 oldSolvePosition = solvePosition;

		for (;;)
		{
			// Collect collision planes
			b3Vec3 translation = solvePosition - startPosition;
			
			CollectSweepPlanes(planes, shapes, translation);

			solvePosition = SolvePositionConstraints(planes, targetPosition);

			const float32 tolerance = 0.05f;

			if (b3DistanceSquared(oldSolvePosition, solvePosition) < tolerance * tolerance)
			{
				break;
			}

			oldSolvePosition = solvePosition;
		}

		// Synchronize body
		b3Quat orientation = m_characterBody->GetOrientation();

		b3Vec3 axis;
		float32 angle;
		orientation.GetAxisAngle(&axis, &angle);

		m_characterBody->SetTransform(solvePosition, axis, angle);
	}

	b3World* m_world;
	b3Body* m_characterBody;
	b3SphereShape* m_characterShape;
	b3Vec3 m_translation;
	bool m_isGrounded;
};

class CharacterTest : public Test
{
public:
	CharacterTest()
	{
		{
			b3BodyDef bdef;
			b3Body* body = m_world.CreateBody(bdef);

			b3HullShape hs;
			hs.m_hull = &m_groundHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.position.Set(-10.0f, 3.0f, 0.0f);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(2.0f, 2.0f, 5.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.position.Set(-10.0f, 9.0f, 0.0f);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(1.0f, 4.0f, 1.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.position.Set(-10.0f, 2.7f, 7.0f);
			bdef.orientation.Set(b3Vec3_x, 0.25f * B3_PI);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(2.0f, 0.25f, 3.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.position.Set(-10.0f, 2.7f, -7.0f);
			bdef.orientation.Set(b3Vec3_x, -0.25f * B3_PI);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(2.0f, 0.25f, 3.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}

		{
			b3BodyDef bdef;
			bdef.position.Set(10.0f, 3.0f, 0.0f);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(2.0f, 2.0f, 5.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.position.Set(10.0f, 9.0f, 0.0f);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(1.0f, 4.0f, 1.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}

		{
			b3BodyDef bdef;
			bdef.position.Set(10.0f, 2.7f, 7.0f);
			bdef.orientation.Set(b3Vec3_x, 0.25f * B3_PI);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(2.0f, 0.25f, 3.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}

		{
			b3BodyDef bdef;
			bdef.position.Set(10.0f, 2.7f, -7.0f);
			bdef.orientation.Set(b3Vec3_x, -0.25f * B3_PI);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(2.0f, 0.25f, 3.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.position.Set(0.0f, 4.0f, 0.0f);

			b3Body* body = m_world.CreateBody(bdef);

			static b3BoxHull boxHull;
			boxHull.Set(8.0f, 1.0f, 2.0f);

			b3HullShape hs;
			hs.m_hull = &boxHull;

			b3ShapeDef sdef;
			sdef.shape = &hs;

			b3Shape* shape = body->CreateShape(sdef);
		}
		
		{
			b3BodyDef bdef;
			bdef.type = b3BodyType::e_kinematicBody;
			bdef.position.Set(0.0f, 1.0f, 20.0f);

			b3Body* body = m_world.CreateBody(bdef);

			b3SphereShape sphere;
			sphere.m_center.SetZero();
			sphere.m_radius = 1.0f;

			b3ShapeDef sdef;
			sdef.shape = &sphere;

			b3SphereShape* shape = (b3SphereShape*)body->CreateShape(sdef);

			m_characterController = new CharacterController(shape);
		}
	}

	~CharacterTest()
	{
		delete m_characterController;
	}

	void Step()
	{
		g_draw->DrawString(b3Color_white, "Arrows - Walk");
		g_draw->DrawString(b3Color_white, "Space - Jump");

		float32 dt = g_testSettings->inv_hertz;

		const float32 walkSpeed = 10.0f;
		const float32 jumpSpeed = 10.0f;
		const float32 gravityAcc = 9.8f;
		
		static b3Vec3 velocity = b3Vec3_zero;

		velocity.x = 0.0f;
		velocity.z = 0.0f;

		extern GLFWwindow* g_window;

		bool leftDown = glfwGetKey(g_window, GLFW_KEY_LEFT);
		bool rightDown = glfwGetKey(g_window, GLFW_KEY_RIGHT);
		bool downDown = glfwGetKey(g_window, GLFW_KEY_DOWN);
		bool upDown = glfwGetKey(g_window, GLFW_KEY_UP);
		bool spaceDown = glfwGetKey(g_window, GLFW_KEY_SPACE);

		bool isGrounded = m_characterController->IsGrounded();

		// Walk
		if (leftDown)
		{
			velocity.x -= walkSpeed;
		}

		if (rightDown)
		{
			velocity.x += walkSpeed;
		}

		if (upDown)
		{
			velocity.z -= walkSpeed;
		}

		if (downDown)
		{
			velocity.z += walkSpeed;
		}

		if (isGrounded)
		{	
			velocity.y = 0.0f;

			// Jump
			if (spaceDown)
			{
				velocity.y += jumpSpeed;
			}
		}
		else
		{
			// Integrate gravity
			velocity.y -= dt * gravityAcc;
		}
		
		// Compute translation
		b3Vec3 translation = dt * velocity;
		
		// Move character
		m_characterController->Move(translation);

		// Step controllers
		m_characterController->Step();

		// Step world
		Test::Step();
	}

	static Test* Create()
	{
		return new CharacterTest();
	}

	CharacterController* m_characterController;
};

#endif