using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.Assertions;

namespace Pathfinding {
	using Pathfinding.Pooling;
	using Pathfinding.Collections;
	using Unity.Burst;
	using Unity.Collections;
	using Unity.Mathematics;
	using UnityEngine.Profiling;

	/// <summary>
	/// Implements the funnel algorithm as well as various related methods.
	/// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
	/// See: Usually you do not use this class directly. Instead use the <see cref="FunnelModifier"/> component.
	///
	/// <code>
	/// using UnityEngine;
	/// using Pathfinding;
	/// using Pathfinding.Drawing;
	///
	/// public class FunnelExample : MonoBehaviour {
	///     public Transform target = null;
	///
	///     void Update () {
	///         var path = ABPath.Construct(transform.position, target.position);
	///
	///         AstarPath.StartPath(path);
	///         path.BlockUntilCalculated();
	///
	///         // Apply some default adjustments to the path
	///         // not necessary if you are using the Seeker component
	///         new StartEndModifier().Apply(path);
	///
	///         // Split the path into segments and links
	///         var parts = Funnel.SplitIntoParts(path);
	///         // Optionally simplify the path to make it straighter
	///         var nodes = path.path;
	///         Funnel.Simplify(parts, ref nodes, ref path.traversalCosts, ref path.traversalConstraint);
	///
	///         using (Draw.WithLineWidth(2)) {
	///             // Go through all the parts and draw them in the scene view
	///             for (int i = 0; i < parts.Count; i++) {
	///                 var part = parts[i];
	///                 if (part.type == Funnel.PartType.OffMeshLink) {
	///                     // Draw off-mesh links as a single line
	///                     Draw.Line(part.startPoint, part.endPoint, Color.cyan);
	///                 } else {
	///                     // Calculate the shortest path through the funnel
	///                     var portals = Funnel.ConstructFunnelPortals(nodes, part);
	///                     var pathThroghPortals = Funnel.Calculate(portals, splitAtEveryPortal: false);
	///                     Draw.Polyline(pathThroghPortals, Color.black);
	///                 }
	///             }
	///         }
	///     }
	/// }
	/// </code>
	///
	/// In the image you can see the output from the code example above. The cyan lines represent off-mesh links.
	///
	/// [Open online documentation to see images]
	/// </summary>
	[BurstCompile]
	public static class Funnel {
		/// <summary>Funnel in which the path to the target will be</summary>
		public struct FunnelPortals {
			public List<Vector3> left;
			public List<Vector3> right;
		}

		/// <summary>The type of a <see cref="PathPart"/></summary>
		public enum PartType {
			/// <summary>An off-mesh link between two nodes in the same or different graphs</summary>
			OffMeshLink,
			/// <summary>A sequence of adjacent nodes in the same graph</summary>
			NodeSequence,
		}

		/// <summary>
		/// Part of a path.
		/// This is either a sequence of adjacent triangles
		/// or a link.
		/// See: NodeLink2
		/// </summary>
		public struct PathPart {
			/// <summary>Index of the first node in this part</summary>
			public int startIndex;
			/// <summary>Index of the last node in this part</summary>
			public int endIndex;
			/// <summary>Exact start-point of this part or off-mesh link</summary>
			public Vector3 startPoint;
			/// <summary>Exact end-point of this part or off-mesh link</summary>
			public Vector3 endPoint;
			/// <summary>If this is an off-mesh link or a sequence of nodes in a single graph</summary>
			public PartType type;
		}

		/// <summary>Splits the path into a sequence of parts which are either off-mesh links or sequences of adjacent triangles</summary>

		public static List<PathPart> SplitIntoParts (Path path) {
			var nodes = path.path;

			var result = ListPool<PathPart>.Claim();

			if (nodes == null || nodes.Count == 0) {
				return result;
			}

			// Loop through the path and split it into
			// parts joined by links
			for (int i = 0; i < nodes.Count; i++) {
				var node = nodes[i];
				if (node is TriangleMeshNode || node is GridNodeBase) {
					var startIndex = i;
					uint currentGraphIndex = node.GraphIndex;

					// Loop up until we find a node in another graph
					// Ignore NodeLink3 nodes
					while (i < nodes.Count && (nodes[i].GraphIndex == currentGraphIndex || nodes[i] is NodeLink3Node)) i++;

					i--;
					var endIndex = i;
					result.Add(new PathPart {
						type = PartType.NodeSequence,
						startIndex = startIndex,
						endIndex = endIndex,
						// If this is the first part in the path, use the exact start point
						// otherwise use the position of the node right before the start of this
						// part which is likely the end of the link to this part
						startPoint = startIndex == 0 ? path.vectorPath[0] : (Vector3)nodes[startIndex-1].position,
						endPoint = endIndex == nodes.Count-1 ? path.vectorPath[path.vectorPath.Count-1] : (Vector3)nodes[endIndex+1].position,
					});
				} else if (node is LinkNode) {
					var startIndex = i;
					var currentGraphIndex = node.GraphIndex;

					while (i < nodes.Count && nodes[i].GraphIndex == currentGraphIndex) i++;
					i--;

					if (i - startIndex == 0) {
						// The link is a single node.
						// Just ignore it. It can happen in very rare circumstances with some path types.
						// For example, a RandomPath can stop at the first node of a node link, without including the other end of the link.

						if (startIndex > 0 && startIndex + 1 < nodes.Count && nodes[startIndex - 1] == nodes[startIndex + 1]) {
							// We can also move to a node link node and then immediately move back to the previous node in rare circumstances.
							// Since triangle nodes are represented as 3 nodes during pathfinding, this is a possibility.
							// (TODO: How can this happen in practice? It has been empirically observed on a standard graph, but the edge costs must be kinda weird for it to happen?)

							// [A, LinkNode, A] => [A]
							nodes.RemoveRange(startIndex, 2);
							i--;
							throw new System.Exception("Link node connected back to the previous node in the path. This should not happen.");
						} else {
							// [A, LinkNode] => [A]
							// [LinkNode, A] => [A]
							Assert.IsTrue(startIndex == 0 || startIndex == nodes.Count - 1);
							nodes.RemoveAt(startIndex);
							i--;
						}

						continue;
					} else if (i - startIndex != 1) {
						throw new System.Exception("Off mesh link included more than two nodes: " + (i - startIndex + 1));
					}

					result.Add(new PathPart {
						type = PartType.OffMeshLink,
						startIndex = startIndex,
						endIndex = i,
						startPoint = (Vector3)nodes[startIndex].position,
						endPoint = (Vector3)nodes[i].position,
					});
				} else {
					throw new System.Exception("Unsupported node type or null node");
				}
			}

			// The path should always start and stop on regular nodes
			if (result[0].type == PartType.OffMeshLink) {
				result.RemoveAt(0);
			}
			if (result[result.Count - 1].type == PartType.OffMeshLink) {
				result.RemoveAt(result.Count - 1);
			}

			Assert.IsTrue(result.Count > 0);
			Assert.AreEqual(result[0].startIndex, 0);
			Assert.AreEqual(result[0].type, PartType.NodeSequence);
			Assert.AreEqual(result[result.Count-1].type, PartType.NodeSequence);
			Assert.AreEqual(result[result.Count-1].endIndex, nodes.Count - 1);

			return result;
		}


		public static void Simplify (List<PathPart> parts, ref List<GraphNode> nodes, ref TraversalCosts traversalCosts, ref TraversalConstraint traversalConstraint) {
			List<GraphNode> resultNodes = ListPool<GraphNode>.Claim();

			for (int i = 0; i < parts.Count; i++) {
				var part = parts[i];

				// We are changing the nodes list, so indices may change
				var newPart = part;
				newPart.startIndex = resultNodes.Count;

				if (part.type == PartType.NodeSequence) {
					if (nodes[part.startIndex].Graph is IRaycastableGraph graph) {
						Simplify(part, graph, nodes, resultNodes, ref traversalCosts, ref traversalConstraint);
						newPart.endIndex = resultNodes.Count - 1;
						parts[i] = newPart;
						continue;
					}
				}

				for (int j = part.startIndex; j <= part.endIndex; j++) {
					resultNodes.Add(nodes[j]);
				}
				newPart.endIndex = resultNodes.Count - 1;
				parts[i] = newPart;
			}

			ListPool<GraphNode>.Release(ref nodes);
			nodes = resultNodes;
		}

		/// <summary>
		/// Simplifies a funnel path using linecasting.
		/// Running time is roughly O(n^2 log n) in the worst case (where n = end-start)
		/// Actually it depends on how the graph looks, so in theory the actual upper limit on the worst case running time is O(n*m log n) (where n = end-start and m = nodes in the graph)
		/// but O(n^2 log n) is a much more realistic worst case limit.
		///
		/// Requires graph to implement IRaycastableGraph
		/// </summary>
		public static void Simplify (PathPart part, IRaycastableGraph graph, List<GraphNode> nodes, List<GraphNode> result, ref TraversalCosts traversalCosts, ref TraversalConstraint traversalConstraint) {
			var start = part.startIndex;
			var end = part.endIndex;
			var startPoint = part.startPoint;
			var endPoint = part.endPoint;

			if (graph == null) throw new System.ArgumentNullException(nameof(graph));

			if (start > end) {
				throw new System.ArgumentException("start > end");
			}

			static bool PathTraversable (List<GraphNode> nodes, int start, int end, ref TraversalConstraint traversalConstraint) {
				for (int i = start; i <= end; i++) {
					if (i == start ? !traversalConstraint.CanTraverse(nodes[i]) : !traversalConstraint.CanTraverse(nodes[i-1], nodes[i])) return false;
				}
				return true;
			}

			static void PathCost (List<GraphNode> nodes, int start, int end, ref TraversalCosts traversalCosts, out long connectionCost, out float avgCost) {
				// This is very much an approximation of the actual cost of the path. But hopefully it's good enough for most cases.
				connectionCost = 0;
				avgCost = 0;
				for (int i = start; i < end; i++) {
					connectionCost += traversalCosts.GetConnectionCost(nodes[i], nodes[i+1]);
					avgCost += traversalCosts.GetTraversalCostMultiplier(nodes[i]);
				}
				avgCost += traversalCosts.GetTraversalCostMultiplier(nodes[end]);
				avgCost /= (end - start) + 1;
			}

			// Do a straight line of sight check to see if the path can be simplified to a single line
			{
				if (!graph.Linecast(startPoint, endPoint, out GraphHitInfo hit) && hit.node == nodes[end]) {
					graph.Linecast(startPoint, endPoint, out hit, result);

					PathCost(nodes, start, end, ref traversalCosts, out long oldConnectionCost, out float oldAvgCost);
					PathCost(result, 0, result.Count - 1, ref traversalCosts, out long newConnectionCost, out float newAvgCost);

					// Allow 10% more traversal cost on average per node.
					// This is very much an approximation of the actual cost of the path. But hopefully it's good enough for most cases.
					if ((newConnectionCost > oldConnectionCost) || (newAvgCost > 1.1f*oldAvgCost) || !PathTraversable(result, 0, result.Count - 1, ref traversalConstraint)) {
						// The straight line penalties are much higher than the original path.
						// Revert the simplification
						result.Clear();
					} else {
						// The straight line simplification looks good.
						// We are done here.
						return;
					}
				}
			}

			int ostart = start;

			int count = 0;
			while (true) {
				if (count++ > 1000) {
					Debug.LogError("Was the path really long or have we got cought in an infinite loop?");
					break;
				}

				if (start == end) {
					result.Add(nodes[end]);
					RemoveBacktracking(result, ostart, result.Count-2);
					return;
				}

				int resCount = result.Count;

				// Run a binary search to find the furthest node that we have a clear line of sight to
				int mx = end+1;
				int mn = start+1;
				bool anySucceded = false;
				while (mx > mn+1) {
					int mid = (mx+mn)/2;

					Vector3 sp = start == ostart ? startPoint : (Vector3)nodes[start].position;
					Vector3 ep = mid == end ? endPoint : (Vector3)nodes[mid].position;

					// Check if there is an obstacle between these points, or if there is no obstacle, but we didn't end up at the right node.
					// The second case can happen for example in buildings with multiple floors.
					if (graph.Linecast(sp, ep, out GraphHitInfo hit) || hit.node != nodes[mid]) {
						mx = mid;
					} else {
						anySucceded = true;
						mn = mid;
					}
				}

				if (!anySucceded) {
					result.Add(nodes[start]);
					RemoveBacktracking(result, ostart, result.Count-2);

					// It is guaranteed that mn = start+1
					start = mn;
				} else {
					// Replace a part of the path with the straight path to the furthest node we had line of sight to.
					// Need to redo the linecast to get the trace (i.e. list of nodes along the line of sight).
					Vector3 sp = start == ostart ? startPoint : (Vector3)nodes[start].position;
					Vector3 ep = mn == end ? endPoint : (Vector3)nodes[mn].position;
					graph.Linecast(sp, ep, out _, result);

					PathCost(nodes, start, end, ref traversalCosts, out long oldConnectionCost, out float oldAvgCost);
					PathCost(result, 0, result.Count - 1, ref traversalCosts, out long newConnectionCost, out float newAvgCost);

					// Allow 10% more traversal cost on average per node.
					// This is very much an approximation of the actual cost of the path. But hopefully it's good enough for most cases.
					if (result[result.Count-1] != nodes[mn] || (newConnectionCost > oldConnectionCost) || (newAvgCost > 1.1f*oldAvgCost) || !PathTraversable(result, 0, result.Count - 1, ref traversalConstraint)) {
						// Linecast hit the wrong node or it is a lot more expensive than the original path
						result.RemoveRange(resCount, result.Count-resCount);

						result.Add(nodes[start]);
						start++;
					} else {
						// In some rare cases, doing the raycast simplification may cause it to backtrack so that the path goes:
						// A -> B -> C -> B -> D
						// While this is technically allowed, it will cause a weird and suboptimal path. So we should try to avoid it.
						RemoveBacktracking(result, ostart, resCount);

						// Remove nodes[end], otherwise we will get a duplicate node when the next raycast happens
						result.RemoveAt(result.Count-1);
						start = mn;
					}
				}
			}
		}

		/// <summary>
		/// Removes backtracking in the path.
		/// This can happen when the path goes A -> B -> C -> B -> D.
		/// This method will replace B -> C -> B with just B, when passed aroundIndex=C.
		/// </summary>
		static void RemoveBacktracking (List<GraphNode> nodes, int listStartIndex, int aroundIndex) {
			while (aroundIndex - 1 > listStartIndex && aroundIndex + 1 < nodes.Count && nodes[aroundIndex-1] == nodes[aroundIndex+1]) {
				nodes.RemoveRange(aroundIndex, 2);
				aroundIndex--;
			}
		}

		public static FunnelPortals ConstructFunnelPortals (List<GraphNode> nodes, PathPart part) {
			if (nodes == null || nodes.Count == 0) {
				return new FunnelPortals { left = ListPool<Vector3>.Claim(0), right = ListPool<Vector3>.Claim(0) };
			}

			if (part.endIndex < part.startIndex || part.startIndex < 0 || part.endIndex > nodes.Count) throw new System.ArgumentOutOfRangeException();

			// Claim temporary lists and try to find lists with a high capacity
			var left = ListPool<Vector3>.Claim(nodes.Count+1);
			var right = ListPool<Vector3>.Claim(nodes.Count+1);

			// Add start point
			left.Add(part.startPoint);
			right.Add(part.startPoint);

			// Loop through all nodes in the path (except the last one)
			for (int i = part.startIndex; i < part.endIndex; i++) {
				// Get the portal between path[i] and path[i+1] and add it to the left and right lists
				if (nodes[i].GetPortal(nodes[i+1], out var lp, out var rp)) {
					left.Add(lp);
					right.Add(rp);
				} else {
					// Fallback, just use the positions of the nodes
					left.Add((Vector3)nodes[i].position);
					right.Add((Vector3)nodes[i].position);

					left.Add((Vector3)nodes[i+1].position);
					right.Add((Vector3)nodes[i+1].position);
				}
			}

			// Add end point
			left.Add(part.endPoint);
			right.Add(part.endPoint);

			return new FunnelPortals { left = left, right = right };
		}

		[BurstCompile]
		public struct FunnelState {
			/// <summary>Left side of the funnel</summary>
			public NativeCircularBuffer<float3> leftFunnel;
			/// <summary>Right side of the funnel</summary>
			public NativeCircularBuffer<float3> rightFunnel;
			/// <summary>
			/// Unwrapped version of the funnel portals in 2D space.
			///
			/// The input is a funnel like in the image below. It may be rotated and twisted.
			/// [Open online documentation to see images]
			/// The output will be a funnel in 2D space like in the image below. All twists and bends will have been straightened out.
			/// [Open online documentation to see images]
			///
			/// This array is used as a cache and the unwrapped portals are calculated on demand. Thus it may not contain all portals.
			/// </summary>
			public NativeCircularBuffer<float4> unwrappedPortals;

			/// <summary>
			/// If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal.
			///
			/// This is used to make the funnel fit a rotated graph better.
			/// It is ideally used for grid graphs, but navmesh/recast graphs are probably better off with it set to zero.
			///
			/// The vector should be normalized (unless zero), in world space, and should never be changed after the first portal has been added (unless the funnel is cleared first).
			/// </summary>
			public float3 projectionAxis;


			public FunnelState(int initialCapacity, Allocator allocator) {
				leftFunnel = new NativeCircularBuffer<float3>(initialCapacity, allocator);
				rightFunnel = new NativeCircularBuffer<float3>(initialCapacity, allocator);
				unwrappedPortals = new NativeCircularBuffer<float4>(initialCapacity, allocator);
				projectionAxis = float3.zero;
			}

			public FunnelState(FunnelPortals portals, Allocator allocator) : this(portals.left.Count, allocator) {
				if (portals.left.Count != portals.right.Count) throw new System.ArgumentException("portals.left.Count != portals.right.Count");
				for (int i = 0; i < portals.left.Count; i++) {
					PushEnd(portals.left[i], portals.right[i]);
				}
			}

			public FunnelState Clone () {
				return new FunnelState {
						   leftFunnel = leftFunnel.Clone(),
						   rightFunnel = rightFunnel.Clone(),
						   unwrappedPortals = unwrappedPortals.Clone(),
						   projectionAxis = projectionAxis,
				};
			}

			public void Clear () {
				leftFunnel.Clear();
				rightFunnel.Clear();
				unwrappedPortals.Clear();
				projectionAxis = float3.zero;
			}

			public void PopStart () {
				leftFunnel.PopStart();
				rightFunnel.PopStart();
				if (unwrappedPortals.Length > 0) unwrappedPortals.PopStart();
			}

			public void PopEnd () {
				leftFunnel.PopEnd();
				rightFunnel.PopEnd();
				unwrappedPortals.TrimTo(leftFunnel.Length);
			}

			public void Pop (bool fromStart) {
				if (fromStart) PopStart();
				else PopEnd();
			}

			public void PushStart (float3 newLeftPortal, float3 newRightPortal) {
				PushStart(ref leftFunnel, ref rightFunnel, ref unwrappedPortals, ref newLeftPortal, ref newRightPortal, ref projectionAxis);
			}

			/// <summary>True if a and b lie on different sides of the infinite line that passes through start and end</summary>
			static bool DifferentSidesOfLine (float3 start, float3 end, float3 a, float3 b) {
				var portal = math.normalizesafe(end - start);
				var d1 = a - start;
				var d2 = b - start;
				d1 -= portal * math.dot(d1, portal);
				d2 -= portal * math.dot(d2, portal);
				return math.dot(d1, d2) < 0;
			}

			/// <summary>
			/// True if it is reasonable that the given start point has passed the first portal in the funnel.
			///
			/// If this is true, it is most likely better to pop the start/end portal of the funnel first.
			///
			/// This can be used as a heuristic to determine if the agent has passed a portal and we should pop it,
			/// in those cases when node information is not available (e.g. because the path has been invalidated).
			/// </summary>
			public bool IsReasonableToPopStart (float3 startPoint, float3 endPoint) {
				if (leftFunnel.Length == 0) return false;

				var reference = 1;
				while (reference < leftFunnel.Length && VectorMath.IsColinear(leftFunnel.First, rightFunnel.First, leftFunnel[reference])) {
					reference++;
				}
				return !DifferentSidesOfLine(leftFunnel.First, rightFunnel.First, startPoint, reference < leftFunnel.Length ? leftFunnel[reference] : endPoint);
			}

			/// <summary>Like <see cref="IsReasonableToPopStart"/> but for the end of the funnel</summary>
			public bool IsReasonableToPopEnd (float3 startPoint, float3 endPoint) {
				if (leftFunnel.Length == 0) return false;

				var reference = leftFunnel.Length - 1;
				while (reference >= 0 && VectorMath.IsColinear(leftFunnel.Last, rightFunnel.Last, leftFunnel[reference])) {
					reference--;
				}
				return !DifferentSidesOfLine(leftFunnel.Last, rightFunnel.Last, endPoint, reference >= 0 ? leftFunnel[reference] : startPoint);
			}

			[BurstCompile]
			static void PushStart (ref NativeCircularBuffer<float3> leftPortals, ref NativeCircularBuffer<float3> rightPortals, ref NativeCircularBuffer<float4> unwrappedPortals, ref float3 newLeftPortal, ref float3 newRightPortal, ref float3 projectionAxis) {
				if (unwrappedPortals.Length == 0) {
					leftPortals.PushStart(newLeftPortal);
					rightPortals.PushStart(newRightPortal);
					return;
				}

				var firstUnwrapped = unwrappedPortals.First;
				var unwrappedRight = Unwrap(leftPortals.First, rightPortals.First, firstUnwrapped.xy, firstUnwrapped.zw, newRightPortal, -1, projectionAxis);
				var unwrappedLeft = Unwrap(leftPortals.First, newRightPortal, firstUnwrapped.xy, unwrappedRight, newLeftPortal, -1, projectionAxis);
				leftPortals.PushStart(newLeftPortal);
				rightPortals.PushStart(newRightPortal);
				unwrappedPortals.PushStart(new float4(unwrappedLeft, unwrappedRight));
			}

			public void Splice (int startIndex, int toRemove, List<float3> newLeftPortal, List<float3> newRightPortal) {
				this.leftFunnel.Splice(startIndex, toRemove, newLeftPortal);
				this.rightFunnel.Splice(startIndex, toRemove, newRightPortal);
				unwrappedPortals.TrimTo(startIndex);
			}

			public void PushEnd (Vector3 newLeftPortal, Vector3 newRightPortal) {
				leftFunnel.PushEnd(newLeftPortal);
				rightFunnel.PushEnd(newRightPortal);
			}

			public void Push (bool toStart, Vector3 newLeftPortal, Vector3 newRightPortal) {
				if (toStart) PushStart(newLeftPortal, newRightPortal);
				else PushEnd(newLeftPortal, newRightPortal);
			}

			public void Dispose () {
				leftFunnel.Dispose();
				rightFunnel.Dispose();
				unwrappedPortals.Dispose();
			}

			/// <summary>
			/// Calculate the shortest path through the funnel.
			///
			/// Returns: The number of corners added to the result array.
			///
			/// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
			/// </summary>
			/// <param name="maxCorners">The maximum number of corners to add to the result array. Should be positive.</param>
			/// <param name="result">Output indices. Contains an index as well as possibly the \reflink{RightSideBit} set. Corresponds to an index of the left or right portals, depending on if \reflink{RightSideBit} is set. This must point to an array which is at least maxCorners long.</param>
			/// <param name="startPoint">Start point of the funnel. The agent will move from here to the best point along the first portal.</param>
			/// <param name="endPoint">End point of the funnel.</param>
			/// <param name="lastCorner">True if the final corner of the path was reached. If true, then the return value is guaranteed to be at most maxCorners - 1 (unless maxCorners = 0).</param>
			public int CalculateNextCornerIndices (int maxCorners, NativeArray<int> result, float3 startPoint, float3 endPoint, out bool lastCorner) {
				Assert.AreEqual(leftFunnel.Length, rightFunnel.Length);
				Assert.IsTrue(unwrappedPortals.Length <= leftFunnel.Length);
				if (result.Length < math.min(maxCorners, leftFunnel.Length)) throw new System.ArgumentException("result array may not be large enough to hold all corners");

				unsafe {
					// TODO: Pass this as ref instead?
					var resultsSpan = result.AsUnsafeSpan();
					return Calculate(ref unwrappedPortals, ref leftFunnel, ref rightFunnel, ref startPoint, ref endPoint, ref resultsSpan, maxCorners, ref projectionAxis, out lastCorner);
				}
			}

			public void CalculateNextCorners (int maxCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, NativeList<float3> result) {
				var indices = new NativeArray<int>(math.min(maxCorners, leftFunnel.Length), Allocator.Temp);
				var numCorners = CalculateNextCornerIndices(maxCorners, indices, startPoint, endPoint, out bool lastCorner);
				ConvertCornerIndicesToPath(indices, numCorners, splitAtEveryPortal, startPoint, endPoint, lastCorner, result);
				indices.Dispose();
			}

			public void ConvertCornerIndicesToPath (NativeArray<int> indices, int numCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList<float3> result) {
				if (result.Capacity < numCorners) result.Capacity = numCorners;

				Assert.IsTrue(numCorners == 0 || (indices[numCorners-1] & FunnelPortalIndexMask) < unwrappedPortals.Length);
				result.Add(startPoint);
				if (leftFunnel.Length == 0) {
					if (lastCorner) result.Add(endPoint);
					return;
				}

				if (splitAtEveryPortal) {
					float2 prev2D = Unwrap(leftFunnel[0], rightFunnel[0], unwrappedPortals[0].xy, unwrappedPortals[0].zw, startPoint, -1, projectionAxis);
					var prevIdx = -1;
					for (int i = 0; i < numCorners; i++) {
						var idx = indices[i] & FunnelPortalIndexMask;
						var rightSide = (indices[i] & RightSideBit) != 0;
						// Check intersections with every portal segment
						float2 next2D = rightSide ? unwrappedPortals[idx].zw : unwrappedPortals[idx].xy;
						CalculatePortalIntersections(prevIdx + 1, idx - 1, leftFunnel, rightFunnel, unwrappedPortals, prev2D, next2D, result);
						prevIdx = math.abs(idx);
						prev2D = next2D;

						result.Add(rightSide ? rightFunnel[idx] : leftFunnel[idx]);
					}
					if (lastCorner) {
						var next2D = Unwrap(leftFunnel.Last, rightFunnel.Last, unwrappedPortals.Last.xy, unwrappedPortals.Last.zw, endPoint, 1, projectionAxis);
						CalculatePortalIntersections(prevIdx + 1, unwrappedPortals.Length, leftFunnel, rightFunnel, unwrappedPortals, prev2D, next2D, result);
						result.Add(endPoint);
					}
				} else {
					for (int i = 0; i < numCorners; i++) {
						var idx = indices[i];
						result.Add((idx & RightSideBit) != 0 ? rightFunnel[idx & FunnelPortalIndexMask] : leftFunnel[idx & FunnelPortalIndexMask]);
					}
					if (lastCorner) result.Add(endPoint);
				}
			}

			public void ConvertCornerIndicesToPathProjected (UnsafeSpan<int> indices, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList<float3> result, float3 up) {
				var resultCount = indices.Length + 1 + (lastCorner ? 1 : 0);
				if (result.Capacity < resultCount) result.Capacity = resultCount;
				result.ResizeUninitialized(resultCount);
				var resultSpan = result.AsUnsafeSpan();
				ConvertCornerIndicesToPathProjected(ref this, ref indices, splitAtEveryPortal, in startPoint, in endPoint, lastCorner, in projectionAxis, ref resultSpan, in up);
			}

			public float4x3 UnwrappedPortalsToWorldMatrix (float3 up) {
				int startIndex = 0;
				while (startIndex < unwrappedPortals.Length && math.lengthsq(unwrappedPortals[startIndex].xy - unwrappedPortals[startIndex].zw) <= 0.00001f) startIndex++;
				if (startIndex >= unwrappedPortals.Length) return new float4x3(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1);
				var left2D = unwrappedPortals[startIndex].xy;
				var right2D = unwrappedPortals[startIndex].zw;
				var left3D = leftFunnel[startIndex];
				var right3D = rightFunnel[startIndex];
				var portal2D = right2D - left2D;
				var portal3D = right3D - left3D;
				var portal2DInv = portal2D * math.rcp(math.lengthsq(portal2D));
				// Matrix to rotate unwrapped portals so that portal2D maps to the x-axis (1,0)
				var mr = new float2x2(
					new float2(portal2DInv.x, -portal2DInv.y),
					new float2(portal2DInv.y, portal2DInv.x)
					);

				// Matrix to transform points in unwrapped-portal-space so left2D maps to (0,0) and right2D maps to (1,0)
				var offset = math.mul(mr, -left2D);
				var m1 = new float4x3(
					new float4(mr.c0.x, 0, mr.c0.y, 0),
					new float4(mr.c1.x, 0, mr.c1.y, 0),
					new float4(offset.x, 0, offset.y, 1)
					);

				// Matrix that maps (0,0,0) to left3D and (1,0,0) to right3D, as well as (0,1,0) to up.
				var m2 = new float4x4(
					new float4(portal3D, 0),
					new float4(up, 0),
					new float4(math.cross(portal3D, up), 0),
					new float4(left3D, 1)
					);

				// Matrix to transform points in unwrapped-portal-space to 3D space. Such that left2D maps to left3D.
				return math.mul(m2, m1);
			}

			[BurstCompile]
			public static void ConvertCornerIndicesToPathProjected (ref FunnelState funnelState, ref UnsafeSpan<int> indices, bool splitAtEveryPortal, in float3 startPoint, in float3 endPoint, bool lastCorner, in float3 projectionAxis, ref UnsafeSpan<float3> result, in float3 up) {
				Assert.IsTrue(indices.Length == 0 || (indices[indices.Length-1] & FunnelPortalIndexMask) < funnelState.unwrappedPortals.Length);
				int resultIndex = 0;
				result[resultIndex++] = startPoint;
				if (funnelState.leftFunnel.Length == 0) {
					if (lastCorner) result[resultIndex++] = endPoint;
					Assert.AreEqual(resultIndex, result.Length);
					return;
				}

				var unwrappedToWorld = funnelState.UnwrappedPortalsToWorldMatrix(up);

				if (splitAtEveryPortal) {
					throw new System.NotImplementedException();
				} else {
					for (int i = 0; i < indices.Length; i++) {
						var idx = indices[i];
						var corner = (idx & RightSideBit) != 0 ? funnelState.unwrappedPortals[idx & FunnelPortalIndexMask].zw : funnelState.unwrappedPortals[idx & FunnelPortalIndexMask].xy;
						result[resultIndex++] = math.mul(unwrappedToWorld, new float3(corner, 1)).xyz;
					}
					if (lastCorner) {
						float2 endPoint2D = Unwrap(funnelState.leftFunnel.Last, funnelState.rightFunnel.Last, funnelState.unwrappedPortals.Last.xy, funnelState.unwrappedPortals.Last.zw, endPoint, 1, projectionAxis);
						result[resultIndex++] = math.mul(unwrappedToWorld, new float3(endPoint2D, 1)).xyz;
					}
				}
				Assert.AreEqual(resultIndex, result.Length);
			}

			static void CalculatePortalIntersections (int startIndex, int endIndex, NativeCircularBuffer<float3> leftPortals, NativeCircularBuffer<float3> rightPortals, NativeCircularBuffer<float4> unwrappedPortals, float2 from, float2 to, NativeList<float3> result) {
				for (int j = startIndex; j < endIndex; j++) {
					var portal = unwrappedPortals[j];
					var left = portal.xy;
					var right = portal.zw;
					if (!VectorMath.LineLineIntersectionFactor(left, right - left, from, to - from, out float factor)) {
						// This really shouldn't happen
						factor = 0.5f;
					}
					result.Add(math.lerp(leftPortals[j], rightPortals[j], factor));
				}
			}
		}

		private static float2 Unwrap (float3 leftPortal, float3 rightPortal, float2 leftUnwrappedPortal, float2 rightUnwrappedPortal, float3 point, float sideMultiplier, float3 projectionAxis) {
			// TODO: On grid graphs this is kind of a weird way to do it.
			// We project all points onto a plane and then unwrap them.
			// It would be faster (and possibly more numerically accurate) to transform the points to graph space and then just use the xz coordinates.
			// This branch is extremely well predicted, since it will always be true for grid graphs, and always false for other graphs.
			if (!math.all(projectionAxis == 0)) {
				leftPortal -= projectionAxis * math.dot(leftPortal, projectionAxis);
				rightPortal -= projectionAxis * math.dot(rightPortal, projectionAxis);
				point -= projectionAxis * math.dot(point, projectionAxis);
			}

			var portal = rightPortal - leftPortal;
			var portalLengthInvSq = 1.0f / math.lengthsq(portal);
			if (float.IsPositiveInfinity(portalLengthInvSq)) {
				return leftUnwrappedPortal + new float2(-math.length(point - leftPortal), 0);
			}
			var distance = math.length(math.cross(point - leftPortal, portal)) * portalLengthInvSq;
			var projection = math.dot(point - leftPortal, portal) * portalLengthInvSq;

			// Weld corner vertices if they are close enough.
			// This is important for grid graphs, as if the unwrapped portals are not quite identical in the corners,
			// the grid simplification may fail to remove inner corners. This is because it will detect 2+ almost identical corners in each turn, instead of 1.
			// TODO: Unwrap grid portals in a different way. It really can be done much faster and more numerically robustly.
			// We should not use graph space directly, though, as grid graphs can move around (ProceduralGraphMover).
			if (distance < 0.002f) {
				if (math.abs(projection) < 0.002f) {
					return leftUnwrappedPortal;
				} else if (math.abs(projection - 1) < 0.002f) {
					return rightUnwrappedPortal;
				}
			}

			var unwrappedPortal = rightUnwrappedPortal - leftUnwrappedPortal;
			var unwrappedNormal = new float2(-unwrappedPortal.y, unwrappedPortal.x);
			return leftUnwrappedPortal + math.mad(unwrappedPortal, projection, unwrappedNormal * (distance * sideMultiplier));
		}

		/// <summary>True if b is to the right of or on the line from (0,0) to a</summary>
		private static bool RightOrColinear (Vector2 a, Vector2 b) {
			return (a.x*b.y - b.x*a.y) <= 0;
		}

		/// <summary>True if b is to the left of or on the line from (0,0) to a</summary>
		private static bool LeftOrColinear (Vector2 a, Vector2 b) {
			return (a.x*b.y - b.x*a.y) >= 0;
		}

		/// <summary>
		/// Calculate the shortest path through the funnel.
		///
		/// The path will be unwrapped into 2D space before the funnel algorithm runs.
		/// This makes it possible to support the funnel algorithm in XY space as well as in more complicated cases, such as on curved worlds.
		/// [Open online documentation to see images]
		///
		/// [Open online documentation to see images]
		///
		/// See: Unwrap
		/// </summary>
		/// <param name="funnel">The portals of the funnel. The first and last vertices portals must be single points (so for example left[0] == right[0]).</param>
		/// <param name="splitAtEveryPortal">If true, then a vertex will be inserted every time the path crosses a portal
		///  instead of only at the corners of the path. The result will have exactly one vertex per portal if this is enabled.
		///  This may introduce vertices with the same position in the output (esp. in corners where many portals meet).</param>
		public static List<Vector3> Calculate (FunnelPortals funnel, bool splitAtEveryPortal) {
			var state = new FunnelState(funnel, Allocator.Temp);
			var startPoint = state.leftFunnel.First;
			var endPoint = state.leftFunnel.Last;
			state.PopStart();
			state.PopEnd();
			var nativeResult = new NativeList<float3>(Allocator.Temp);
			state.CalculateNextCorners(int.MaxValue, splitAtEveryPortal, startPoint, endPoint, nativeResult);
			state.Dispose();
			var result = ListPool<Vector3>.Claim(nativeResult.Length);
			for (int i = 0; i < nativeResult.Length; i++) result.Add((Vector3)nativeResult[i]);
			nativeResult.Dispose();
			return result;
		}

		public const int RightSideBit = 1 << 30;
		public const int FunnelPortalIndexMask = RightSideBit - 1;

		/// <summary>
		/// Calculate the shortest path through the funnel.
		///
		/// Returns: The number of corners added to the funnelPath array.
		///
		/// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
		/// </summary>
		/// <param name="leftPortals">Left side of the funnel. Should not contain the start point.</param>
		/// <param name="rightPortals">Right side of the funnel. Should not contain the end point.</param>
		/// <param name="unwrappedPortals">Cache of unwrapped portal segments. This may be empty, but it will be filled with unwrapped portals and next time you run the algorithm it will be faster.</param>
		/// <param name="startPoint">Start point of the funnel. The agent will move from here to the best point between leftPortals[0] and rightPortals[0].</param>
		/// <param name="endPoint">End point of the funnel.</param>
		/// <param name="funnelPath">Output indices. Contains an index as well as possibly the \reflink{RightSideBit} set. Corresponds to an index into leftPortals or rightPortals depending on if \reflink{RightSideBit} is set. This must point to an array which is at least maxCorners long.</param>
		/// <param name="lastCorner">True if the final corner of the path was reached. If true, then the return value is guaranteed to be at most maxCorners - 1 (unless maxCorners = 0).</param>
		/// <param name="maxCorners">The first N corners of the optimized path will be calculated. Calculating fewer corners is faster. Pass int.MaxValue if you want to calculate all corners.</param>
		/// <param name="projectionAxis">If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal.</param>
		[BurstCompile]
		static unsafe int Calculate (ref NativeCircularBuffer<float4> unwrappedPortals, ref NativeCircularBuffer<float3> leftPortals, ref NativeCircularBuffer<float3> rightPortals, ref float3 startPoint, ref float3 endPoint, ref UnsafeSpan<int> funnelPath, int maxCorners, ref float3 projectionAxis, out bool lastCorner) {
			lastCorner = false;
			if (leftPortals.Length <= 0) {
				lastCorner = true;
				return 0;
			}
			if (maxCorners <= 0) return 0;

			int apexIndex = 0;
			int rightIndex = 0;
			int leftIndex = 0;

			int outputCount = 0;

			if (unwrappedPortals.Length == 0) {
				unwrappedPortals.PushEnd(new float4(new float2(0, 0), new float2(math.length(rightPortals[0] - leftPortals[0]))));
			}

			float2 portalApex = Unwrap(leftPortals[0], rightPortals[0], unwrappedPortals[0].xy, unwrappedPortals[0].zw, startPoint, -1, projectionAxis);
			float2 portalLeft = float2.zero;
			float2 portalRight = float2.zero;

			for (int i = 0; i <= leftPortals.Length; i++) {
				// Unwrap the funnel on the fly as needed
				float2 rLeft, rRight;
				if (i == unwrappedPortals.Length) {
					if (i == leftPortals.Length) {
						// The end point of the path
						rLeft = rRight = Unwrap(leftPortals[i-1], rightPortals[i-1], unwrappedPortals[i-1].xy, unwrappedPortals[i-1].zw, endPoint, 1, projectionAxis) - portalApex;
					} else {
						// The funnel needs unwrapping
						var unwrappedLeft = Unwrap(leftPortals[i-1], rightPortals[i-1], unwrappedPortals[i-1].xy, unwrappedPortals[i-1].zw, leftPortals[i], 1, projectionAxis);
						var unwrappedRight = Unwrap(leftPortals[i], rightPortals[i-1], unwrappedLeft, unwrappedPortals[i-1].zw, rightPortals[i], 1, projectionAxis);
						unwrappedPortals.PushEnd(new float4(unwrappedLeft, unwrappedRight));
						rLeft = unwrappedLeft - portalApex;
						rRight = unwrappedRight - portalApex;
					}
				} else {
					// Common case
					rLeft = unwrappedPortals[i].xy - portalApex;
					rRight = unwrappedPortals[i].zw - portalApex;
				}

				if (LeftOrColinear(portalRight, rRight)) {
					if (RightOrColinear(portalLeft, rRight)) {
						portalRight = rRight;
						rightIndex = i;
					} else {
						portalRight = portalLeft = float2.zero;
						i = apexIndex = rightIndex = leftIndex;
						portalApex = unwrappedPortals[i].xy;

						funnelPath[outputCount++] = apexIndex;
						if (outputCount >= maxCorners) return outputCount;
						continue;
					}
				}

				if (RightOrColinear(portalLeft, rLeft)) {
					if (LeftOrColinear(portalRight, rLeft)) {
						portalLeft = rLeft;
						leftIndex = i;
					} else {
						portalRight = portalLeft = float2.zero;
						i = apexIndex = leftIndex = rightIndex;
						portalApex = unwrappedPortals[i].zw;

						funnelPath[outputCount++] = apexIndex | RightSideBit;
						if (outputCount >= maxCorners) return outputCount;
						continue;
					}
				}
			}

			lastCorner = true;
			return outputCount;
		}
	}
}
