#region License

/* MoonWorks - Game Development Framework
 * Copyright 2021 Evan Hemsley
 */

/* Derived from code by Ethan Lee (Copyright 2009-2021).
 * Released under the Microsoft Public License.
 * See fna.LICENSE for details.

 * Derived from code by the Mono.Xna Team (Copyright 2006).
 * Released under the MIT License. See monoxna.LICENSE for details.
 */

#endregion

#region Using Statements
using System;
using System.Diagnostics;

#endregion

namespace MoonWorks.Math.Float
{
	[Serializable]
	[DebuggerDisplay("{DebugDisplayString,nq}")]
	public struct Ray : IEquatable<Ray>
	{
		#region Internal Properties

		internal string DebugDisplayString
		{
			get
			{
				return string.Concat(
					"Pos( ", Position.DebugDisplayString, " ) \r\n",
					"Dir( ", Direction.DebugDisplayString, " )"
				);
			}
		}

		#endregion

		#region Public Fields

		public Vector3 Position;
		public Vector3 Direction;

		#endregion


		#region Public Constructors

		public Ray(Vector3 position, Vector3 direction)
		{
			Position = position;
			Direction = direction;
		}

		#endregion


		#region Public Methods

		public override bool Equals(object obj)
		{
			return (obj is Ray) && Equals((Ray) obj);
		}


		public bool Equals(Ray other)
		{
			return (this.Position.Equals(other.Position) &&
					this.Direction.Equals(other.Direction));
		}


		public override int GetHashCode()
		{
			return Position.GetHashCode() ^ Direction.GetHashCode();
		}

		// Adapted from http://www.scratchapixel.com/lessons/3d-basic-lessons/lesson-7-intersecting-simple-shapes/ray-box-intersection/
		public float? Intersects(BoundingBox box)
		{
			float? tMin = null, tMax = null;

			if (MathHelper.WithinEpsilon(Direction.X, 0.0f))
			{
				if (Position.X < box.Min.X || Position.X > box.Max.X)
				{
					return null;
				}
			}
			else
			{
				tMin = (box.Min.X - Position.X) / Direction.X;
				tMax = (box.Max.X - Position.X) / Direction.X;

				if (tMin > tMax)
				{
					float? temp = tMin;
					tMin = tMax;
					tMax = temp;
				}
			}

			if (MathHelper.WithinEpsilon(Direction.Y, 0.0f))
			{
				if (Position.Y < box.Min.Y || Position.Y > box.Max.Y)
				{
					return null;
				}
			}
			else
			{
				float tMinY = (box.Min.Y - Position.Y) / Direction.Y;
				float tMaxY = (box.Max.Y - Position.Y) / Direction.Y;

				if (tMinY > tMaxY)
				{
					float temp = tMinY;
					tMinY = tMaxY;
					tMaxY = temp;
				}

				if ((tMin.HasValue && tMin > tMaxY) ||
					(tMax.HasValue && tMinY > tMax))
				{
					return null;
				}

				if (!tMin.HasValue || tMinY > tMin) tMin = tMinY;
				if (!tMax.HasValue || tMaxY < tMax) tMax = tMaxY;
			}

			if (MathHelper.WithinEpsilon(Direction.Z, 0.0f))
			{
				if (Position.Z < box.Min.Z || Position.Z > box.Max.Z)
				{
					return null;
				}
			}
			else
			{
				float tMinZ = (box.Min.Z - Position.Z) / Direction.Z;
				float tMaxZ = (box.Max.Z - Position.Z) / Direction.Z;

				if (tMinZ > tMaxZ)
				{
					float temp = tMinZ;
					tMinZ = tMaxZ;
					tMaxZ = temp;
				}

				if ((tMin.HasValue && tMin > tMaxZ) ||
					(tMax.HasValue && tMinZ > tMax))
				{
					return null;
				}

				if (!tMin.HasValue || tMinZ > tMin) tMin = tMinZ;
				if (!tMax.HasValue || tMaxZ < tMax) tMax = tMaxZ;
			}

			/* Having a positive tMin and a negative tMax means the ray is inside the
			 * box we expect the intesection distance to be 0 in that case.
			 */
			if ((tMin.HasValue && tMin < 0) && tMax > 0) return 0;

			/* A negative tMin means that the intersection point is behind the ray's
			 * origin. We discard these as not hitting the AABB.
			 */
			if (tMin < 0) return null;

			return tMin;
		}


		public void Intersects(ref BoundingBox box, out float? result)
		{
			result = Intersects(box);
		}

		public float? Intersects(BoundingSphere sphere)
		{
			float? result;
			Intersects(ref sphere, out result);
			return result;
		}

		public float? Intersects(Plane plane)
		{
			float? result;
			Intersects(ref plane, out result);
			return result;
		}

		public float? Intersects(BoundingFrustum frustum)
		{
			float? result;
			frustum.Intersects(ref this, out result);
			return result;
		}

		public void Intersects(ref Plane plane, out float? result)
		{
			float den = Vector3.Dot(Direction, plane.Normal);
			if (System.Math.Abs(den) < 0.00001f)
			{
				result = null;
				return;
			}

			result = (-plane.D - Vector3.Dot(plane.Normal, Position)) / den;

			if (result < 0.0f)
			{
				if (result < -0.00001f)
				{
					result = null;
					return;
				}

				result = 0.0f;
			}
		}

		public void Intersects(ref BoundingSphere sphere, out float? result)
		{
			// Find the vector between where the ray starts the the sphere's center.
			Vector3 difference = sphere.Center - this.Position;

			float differenceLengthSquared = difference.LengthSquared();
			float sphereRadiusSquared = sphere.Radius * sphere.Radius;

			float distanceAlongRay;

			/* If the distance between the ray start and the sphere's center is less than
			 * the radius of the sphere, it means we've intersected. Checking the
			 * LengthSquared is faster.
			 */
			if (differenceLengthSquared < sphereRadiusSquared)
			{
				result = 0.0f;
				return;
			}

			Vector3.Dot(ref this.Direction, ref difference, out distanceAlongRay);
			// If the ray is pointing away from the sphere then we don't ever intersect.
			if (distanceAlongRay < 0)
			{
				result = null;
				return;
			}

			/* Next we kinda use Pythagoras to check if we are within the bounds of the
			 * sphere.
			 * if x = radius of sphere
			 * if y = distance between ray position and sphere centre
			 * if z = the distance we've travelled along the ray
			 * if x^2 + z^2 - y^2 < 0, we do not intersect
			 */
			float dist = (
				sphereRadiusSquared +
				(distanceAlongRay * distanceAlongRay) -
				differenceLengthSquared
			);

			result = (dist < 0) ? null : distanceAlongRay - (float?) System.Math.Sqrt(dist);
		}

		#endregion

		#region Public Static Methods

		public static bool operator !=(Ray a, Ray b)
		{
			return !a.Equals(b);
		}


		public static bool operator ==(Ray a, Ray b)
		{
			return a.Equals(b);
		}


		public override string ToString()
		{
			return (
				"{{Position:" + Position.ToString() +
				" Direction:" + Direction.ToString() +
				"}}"
			);
		}

		#endregion
	}
}
