#pragma warning disable 0162
using UnityEngine;
using Pathfinding.Serialization;
using Pathfinding.Collections;
using UnityEngine.Assertions;
using Unity.Mathematics;
using Pathfinding.Util;
using Unity.Burst;
using Unity.Profiling;

namespace Pathfinding {
	/// <summary>Interface for something that holds a triangle based navmesh</summary>
	public interface INavmeshHolder : ITransformedGraph {
		void GetNodes(System.Action<GraphNode> del);

		/// <summary>Position of vertex number i in the world</summary>
		Int3 GetVertex(int i);

		/// <summary>
		/// Position of vertex number i in coordinates local to the graph.
		/// The up direction is always the +Y axis for these coordinates.
		/// </summary>
		Int3 GetVertexInGraphSpace(int i);

		int GetVertexArrayIndex(int index);

		/// <summary>Transforms coordinates from graph space to world space</summary>
		void GetTileCoordinates(int tileIndex, out int x, out int z);
	}

	/// <summary>Node represented by a triangle</summary>
	[Unity.Burst.BurstCompile]
	// Sealing the class provides a nice performance boost (~5-10%) during pathfinding, because the JIT can inline more things and use non-virtual calls.
	public sealed class TriangleMeshNode : MeshNode {
		public TriangleMeshNode () {
			HierarchicalNodeIndex = 0;
			NodeIndex = DestroyedNodeIndex;
		}

		public TriangleMeshNode (AstarPath astar) {
			astar.InitializeNode(this);
		}

		/// <summary>
		/// Legacy compatibility.
		/// Enabling this will make pathfinding use node centers, which leads to less accurate paths (but it's faster).
		/// </summary>
		public const bool InaccuratePathSearch = false;
		internal override int PathNodeVariants => InaccuratePathSearch ? 1 : 3;

		/// <summary>Internal vertex index for the first vertex</summary>
		public int v0;

		/// <summary>Internal vertex index for the second vertex</summary>
		public int v1;

		/// <summary>Internal vertex index for the third vertex</summary>
		public int v2;

		/// <summary>Holds INavmeshHolder references for all graph indices to be able to access them in a performant manner</summary>
		static INavmeshHolder[] _navmeshHolders = new INavmeshHolder[0];

		/// <summary>Used for synchronised access to the <see cref="_navmeshHolders"/> array</summary>
		static readonly System.Object lockObject = new System.Object();

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
		public static INavmeshHolder GetNavmeshHolder (uint graphIndex) {
			return _navmeshHolders[(int)graphIndex];
		}

		/// <summary>
		/// Tile index in the recast or navmesh graph that this node is part of.
		/// See: <see cref="NavmeshBase.GetTiles"/>
		/// </summary>
		public int TileIndex => (v0 >> NavmeshBase.TileIndexOffset) & NavmeshBase.TileIndexMask;

		/// <summary>
		/// Sets the internal navmesh holder for a given graph index.
		/// Warning: Internal method
		/// </summary>
		public static void SetNavmeshHolder (int graphIndex, INavmeshHolder graph) {
			// We need to lock to make sure that
			// the resize operation is thread safe
			lock (lockObject) {
				if (graphIndex >= _navmeshHolders.Length) {
					var gg = new INavmeshHolder[graphIndex+1];
					_navmeshHolders.CopyTo(gg, 0);
					_navmeshHolders = gg;
				}
				_navmeshHolders[graphIndex] = graph;
			}
		}

		public static void ClearNavmeshHolder (int graphIndex, INavmeshHolder graph) {
			lock (lockObject) {
				if (graphIndex < _navmeshHolders.Length && _navmeshHolders[graphIndex] == graph) {
					_navmeshHolders[graphIndex] = null;
				}
			}
		}

		/// <summary>Set the position of this node to the average of its 3 vertices</summary>
		public void UpdatePositionFromVertices () {
			Int3 a, b, c;

			GetVertices(out a, out b, out c);
			position = (a + b + c) * 0.333333f;
		}

		/// <summary>
		/// Return a number identifying a vertex.
		/// This number does not necessarily need to be a index in an array but two different vertices (in the same graph) should
		/// not have the same vertex numbers.
		/// </summary>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
		[IgnoredByDeepProfiler]
		public int GetVertexIndex (int i) {
			return i == 0 ? v0 : (i == 1 ? v1 : v2);
		}

		/// <summary>
		/// Return a number specifying an index in the source vertex array.
		/// The vertex array can for example be contained in a recast tile, or be a navmesh graph, that is graph dependant.
		/// This is slower than GetVertexIndex, if you only need to compare vertices, use GetVertexIndex.
		/// </summary>
		public int GetVertexArrayIndex (int i) {
			return GetNavmeshHolder(GraphIndex).GetVertexArrayIndex(i == 0 ? v0 : (i == 1 ? v1 : v2));
		}

		/// <summary>Returns all 3 vertices of this node in world space</summary>
		public void GetVertices (out Int3 v0, out Int3 v1, out Int3 v2) {
			// Get the object holding the vertex data for this node
			// This is usually a graph or a recast graph tile
			var holder = GetNavmeshHolder(GraphIndex);

			v0 = holder.GetVertex(this.v0);
			v1 = holder.GetVertex(this.v1);
			v2 = holder.GetVertex(this.v2);
		}

		/// <summary>Returns all 3 vertices of this node in graph space</summary>
		public void GetVerticesInGraphSpace (out Int3 v0, out Int3 v1, out Int3 v2) {
			// Get the object holding the vertex data for this node
			// This is usually a graph or a recast graph tile
			var holder = GetNavmeshHolder(GraphIndex);

			v0 = holder.GetVertexInGraphSpace(this.v0);
			v1 = holder.GetVertexInGraphSpace(this.v1);
			v2 = holder.GetVertexInGraphSpace(this.v2);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
		[IgnoredByDeepProfiler]
		public override Int3 GetVertex (int i) {
			return GetNavmeshHolder(GraphIndex).GetVertex(GetVertexIndex(i));
		}

		public Int3 GetVertexInGraphSpace (int i) {
			return GetNavmeshHolder(GraphIndex).GetVertexInGraphSpace(GetVertexIndex(i));
		}

		public override int GetVertexCount () {
			// A triangle has 3 vertices
			return 3;
		}

		/// <summary>
		/// Projects the given point onto the plane of this node's surface.
		///
		/// The point will be projected down to a plane that contains the surface of the node.
		/// If the point is not contained inside the node, it is projected down onto this plane anyway.
		/// </summary>
		public Vector3 ProjectOnSurface (Vector3 point) {
			Int3 a, b, c;

			GetVertices(out a, out b, out c);
			var pa = (Vector3)a;
			var pb = (Vector3)b;
			var pc = (Vector3)c;
			var up = Vector3.Cross(pb-pa, pc-pa).normalized;
			return point - up * Vector3.Dot(up, point-pa);
		}

		public override Vector3 ClosestPointOnNode (Vector3 p) {
			Int3 a, b, c;

			GetVertices(out a, out b, out c);
			return Pathfinding.Polygon.ClosestPointOnTriangle((float3)(Vector3)a, (float3)(Vector3)b, (float3)(Vector3)c, (float3)p);
		}

		/// <summary>
		/// Closest point on the node when seen from above.
		/// This method is mostly for internal use as the <see cref="Pathfinding.NavmeshBase.Linecast"/> methods use it.
		///
		/// - The returned point is the closest one on the node to p when seen from above (relative to the graph).
		///   This is important mostly for sloped surfaces.
		/// - The returned point is an Int3 point in graph space.
		/// - It is guaranteed to be inside the node, so if you call <see cref="ContainsPointInGraphSpace"/> with the return value from this method the result is guaranteed to be true.
		///
		/// This method is slower than e.g <see cref="ClosestPointOnNode"/> or <see cref="ClosestPointOnNodeXZ"/>.
		/// However they do not have the same guarantees as this method has.
		/// </summary>
		internal Int3 ClosestPointOnNodeXZInGraphSpace (Vector3 p) {
			// Get the vertices that make up the triangle
			Int3 a, b, c;

			GetVerticesInGraphSpace(out a, out b, out c);

			// Convert p to graph space
			p = GetNavmeshHolder(GraphIndex).transform.InverseTransform(p);

			// Find the closest point on the triangle to p when looking at the triangle from above (relative to the graph)
			var closest = Pathfinding.Polygon.ClosestPointOnTriangleXZ((Vector3)a, (Vector3)b, (Vector3)c, p);

			// Make sure the point is actually inside the node
			var i3closest = (Int3)closest;
			if (ContainsPointInGraphSpace(i3closest)) {
				// Common case
				return i3closest;
			} else {
				// Annoying...
				// The closest point when converted from floating point coordinates to integer coordinates
				// is not actually inside the node. It needs to be inside the node for some methods
				// (like for example Linecast) to work properly.

				// Try the 8 integer coordinates around the closest point
				// and check if any one of them are completely inside the node.
				// This will most likely succeed as it should be very close.
				for (int dx = -1; dx <= 1; dx++) {
					for (int dz = -1; dz <= 1; dz++) {
						if ((dx != 0 || dz != 0)) {
							var candidate = new Int3(i3closest.x + dx, i3closest.y, i3closest.z + dz);
							if (ContainsPointInGraphSpace(candidate)) return candidate;
						}
					}
				}

				// Happens veery rarely.
				// Pick the closest vertex of the triangle.
				// The vertex is guaranteed to be inside the triangle.
				var da = (a - i3closest).sqrMagnitudeLong;
				var db = (b - i3closest).sqrMagnitudeLong;
				var dc = (c - i3closest).sqrMagnitudeLong;
				return da < db ? (da < dc ? a : c) : (db < dc ? b : c);
			}
		}

		public override Vector3 ClosestPointOnNodeXZ (Vector3 p) {
			// Get all 3 vertices for this node
			GetVertices(out Int3 tp1, out Int3 tp2, out Int3 tp3);
			return Polygon.ClosestPointOnTriangleXZ((Vector3)tp1, (Vector3)tp2, (Vector3)tp3, p);
		}

		/// <summary>
		/// Checks if point is inside the node when seen from above.
		///
		/// Note that <see cref="ContainsPointInGraphSpace"/> is faster than this method as it avoids
		/// some coordinate transformations. If you are repeatedly calling this method
		/// on many different nodes but with the same point then you should consider
		/// transforming the point first and then calling ContainsPointInGraphSpace.
		///
		/// <code>
		/// Int3 p = (Int3)graph.transform.InverseTransform(point);
		///
		/// node.ContainsPointInGraphSpace(p);
		/// </code>
		/// </summary>
		public override bool ContainsPoint (Vector3 p) {
			return ContainsPointInGraphSpace((Int3)GetNavmeshHolder(GraphIndex).transform.InverseTransform(p));
		}

		/// <summary>Checks if point is inside the node when seen from above, as defined by the movement plane</summary>
		public bool ContainsPoint (Vector3 p, NativeMovementPlane movementPlane) {
			// Get all 3 vertices for this node
			GetVertices(out var a, out var b, out var c);
			var pa = (int3)a;
			var pb = (int3)b;
			var pc = (int3)c;
			var pp = (int3)(Int3)p;
			return Polygon.ContainsPoint(ref pa, ref pb, ref pc, ref pp, ref movementPlane);
		}

		/// <summary>
		/// Checks if point is inside the node in graph space.
		///
		/// In graph space the up direction is always the Y axis so in principle
		/// we project the triangle down on the XZ plane and check if the point is inside the 2D triangle there.
		/// </summary>
		public override bool ContainsPointInGraphSpace (Int3 p) {
			// Get all 3 vertices for this node
			GetVerticesInGraphSpace(out var a, out var b, out var c);

			if ((long)(b.x - a.x) * (long)(p.z - a.z) - (long)(p.x - a.x) * (long)(b.z - a.z) > 0) return false;

			if ((long)(c.x - b.x) * (long)(p.z - b.z) - (long)(p.x - b.x) * (long)(c.z - b.z) > 0) return false;

			if ((long)(a.x - c.x) * (long)(p.z - c.z) - (long)(p.x - c.x) * (long)(a.z - c.z) > 0) return false;

			return true;
			// Equivalent code, but the above code is faster
			//return Polygon.IsClockwiseMargin (a,b, p) && Polygon.IsClockwiseMargin (b,c, p) && Polygon.IsClockwiseMargin (c,a, p);

			//return Polygon.ContainsPoint(g.GetVertex(v0),g.GetVertex(v1),g.GetVertex(v2),p);
		}

		public static readonly Unity.Profiling.ProfilerMarker MarkerDecode = new Unity.Profiling.ProfilerMarker("Decode");
		public static readonly Unity.Profiling.ProfilerMarker MarkerGetVertices = new Unity.Profiling.ProfilerMarker("GetVertex");
		public static readonly Unity.Profiling.ProfilerMarker MarkerClosest = new Unity.Profiling.ProfilerMarker("MarkerClosest");

		public override Int3 DecodeVariantPosition (uint pathNodeIndex, uint fractionAlongEdge) {
			var edge = (int)(pathNodeIndex - NodeIndex);
			var p1 = GetVertex(edge);
			var p2 = GetVertex((edge + 1) % 3);
			InterpolateEdge(ref p1, ref p2, fractionAlongEdge, out var pos);
			return pos;
		}

		[BurstCompile(FloatMode = FloatMode.Fast)]
		static void InterpolateEdge (ref Int3 p1, ref Int3 p2, uint fractionAlongEdge, out Int3 pos) {
			var p = (int3)math.lerp((float3)(int3)p1, (float3)(int3)p2, PathNode.UnQuantizeFractionAlongEdge(fractionAlongEdge));
			pos = new Int3(p.x, p.y, p.z);
		}

		public override void OpenAtPoint (ref Path.SearchContext ctx, uint pathNodeIndex, Int3 point, uint gScore) {
			if (InaccuratePathSearch) {
				Open(ref ctx, pathNodeIndex, gScore);
			} else {
				OpenAtPoint(ref ctx, pathNodeIndex, point, -1, gScore);
			}
		}

		public override void Open (ref Path.SearchContext ctx, uint pathNodeIndex, uint gScore) {
			if (InaccuratePathSearch) {
				var pn = ctx.pathHandler.pathNodes[pathNodeIndex];
				if (pn.flag1) ctx.OpenCandidateConnectionsToEndNode(position, pathNodeIndex, NodeIndex, gScore, ctx.traversalCosts.GetTraversalCostMultiplier(this));

				if (connections != null) {
					// Iterate over all adjacent nodes
					for (int i = connections.Length-1; i >= 0; i--) {
						var conn = connections[i];
						var other = conn.node;
						if (conn.isOutgoing && other.NodeIndex != pn.parentIndex) {
							ctx.OpenCandidateConnection(pathNodeIndex, other.NodeIndex, gScore + (uint)System.Math.Round(conn.cost * ctx.traversalCosts.GetTraversalCostMultiplier(this)) + ctx.traversalCosts.GetConnectionCost(this, other), 0, other.position);
						}
					}
				}
				return;
			}
			// One path node variant is created for each side of the triangle
			// This particular path node represents just one of the sides of the triangle.
			var edge = (int)(pathNodeIndex - NodeIndex);
			OpenAtPoint(ref ctx, pathNodeIndex, DecodeVariantPosition(pathNodeIndex, ctx.pathHandler.pathNodes[pathNodeIndex].fractionAlongEdge), edge, gScore);
		}

		void OpenAtPoint (ref Path.SearchContext ctx, uint pathNodeIndex, Int3 pos, int edge, uint gScore) {
			var pathHandler = ctx.pathHandler;
			var pn = pathHandler.pathNodes[pathNodeIndex];
			var ourTraversalCostFactor = ctx.traversalCosts.GetTraversalCostMultiplier(this);
			if (pn.flag1) ctx.OpenCandidateConnectionsToEndNode(pos, pathNodeIndex, NodeIndex, gScore, ourTraversalCostFactor);
			int visitedEdges = 0;
			bool cameFromOtherEdgeInThisTriangle = pn.parentIndex >= NodeIndex && pn.parentIndex < NodeIndex + 3;

			if (connections != null) {
				// Iterate over all adjacent nodes
				for (int i = connections.Length-1; i >= 0; i--) {
					var conn = connections[i];
					if (!conn.isOutgoing) continue;
					var other = conn.node;

					// Check if we are moving from a side of this triangle, to the corresponding side on an adjacent triangle.
					if (conn.isEdgeShared) {
						var sharedEdgeOnOtherNode = conn.adjacentShapeEdge;
						var adjacentPathNodeIndex = other.NodeIndex + (uint)sharedEdgeOnOtherNode;

						// Skip checking our parent node. This is purely a performance optimization.
						if (adjacentPathNodeIndex == pn.parentIndex) continue;

						if (conn.shapeEdge == edge) {
							// Make sure we can traverse the neighbour
							if (ctx.traversalConstraint.CanTraverse(this, other)) {
								var tOther = other as TriangleMeshNode;

								// Fast path out if we know we have already searched this node and we cannot improve it
								if (!ctx.ShouldConsiderPathNode(adjacentPathNodeIndex)) {
									continue;
								}
								var otherGScore = gScore + ctx.traversalCosts.GetConnectionCost(this, other);

								if (conn.edgesAreIdentical) {
									// The edge on the other node is identical to this edge (but reversed).
									// This means that no other node can reach the other node through that edge.
									// This is great, because we can then skip adding that node to the heap just
									// to immediatelly pop it again. This is a performance optimization.
									ctx.SkipOverNode(ref ctx, adjacentPathNodeIndex, pathNodeIndex, PathNode.ReverseFractionAlongEdge(pn.fractionAlongEdge), uint.MaxValue, otherGScore);
									tOther.OpenAtPoint(ref ctx, adjacentPathNodeIndex, pos, sharedEdgeOnOtherNode, otherGScore);
								} else {
									// The edge on the other node is not quite identical to this edge. It will be colinear, though.
									OpenSingleEdge(ref ctx, pathNodeIndex, tOther, sharedEdgeOnOtherNode, pos, otherGScore, ourTraversalCostFactor);
								}
							}
						} else {
							// The other node is a node which shares a different edge with this node.
							// We will consider this connection at another time.

							// However, we will consider the move to another side of this triangle,
							// namely to the side that *is* shared with the other node.
							// If a side of this triangle doesn't share an edge with any connection, we will
							// not bother searching it (we will not reach this part of the code), because
							// we know its a dead end.

							// If we came from another side of this triangle, it is completely redundant to try to move back to
							// another edge in this triangle, because we could always have reached it faster from the parent.
							// We also make sure we don't attempt to move to the same edge twice, as that's just a waste of time.
							if (!cameFromOtherEdgeInThisTriangle && (visitedEdges & (1 << conn.shapeEdge)) == 0) {
								visitedEdges |= 1 << conn.shapeEdge;
								OpenSingleEdge(ref ctx, pathNodeIndex, this, conn.shapeEdge, pos, gScore, ourTraversalCostFactor);
							}
						}
					} else if (!cameFromOtherEdgeInThisTriangle) { // If we came from another edge of this triangle, we will already been able to explore this connection via a more optimal route, so we can skip it now.
						// This is a connection to some other node type, most likely. For example an off-mesh link.

						if (ctx.traversalConstraint.CanTraverse(this, other) && ctx.ShouldConsiderPathNode(other.NodeIndex)) {
							var connectionCost = (uint)System.Math.Round((other.position - pos).magnitude * ourTraversalCostFactor);

							if (edge != -1) {
								// We are moving from an edge of this triangle, but our parent node is (likely) an adjacent triangle.
								// This can happen due to the performance optimization above where we skip adding a node to the heap.
								ctx.OpenCandidateConnection(pathNodeIndex, other.NodeIndex, gScore + connectionCost, 0, other.position);
							} else {
								// In some situations we may be moving directly from one off-mesh link to another one without
								// passing through any concrete nodes in between. In this case we need to create a temporary node
								// to allow the correct path to be reconstructed later. The only important part of the temporary
								// node is that we save this node as the associated node.
								// This is somewhat ugly.
								// Fortunately, this case only happens if there is more than 1 off-mesh link connected to a single
								// node, which is quite rare in most games.
								// In this case, pathNodeIndex will be another node's index, not a path node belonging to this node.
								var viaNode = pathHandler.AddTemporaryNode(new TemporaryNode {
									associatedNode = NodeIndex,
									position = pos,
									targetIndex = 0,
									type = TemporaryNodeType.Ignore,
								});
								ref var viaPathNode = ref pathHandler.pathNodes[viaNode];
								viaPathNode.pathID = ctx.pathID;
								viaPathNode.parentIndex = pathNodeIndex;
								ctx.OpenCandidateConnection(viaNode, other.NodeIndex, gScore + connectionCost, 0, other.position);
							}
						}
					}
				}
			}

			if (edge == -1) {
				// If we have entered this node via an off-mesh link, or if this was the first node in the path,
				// then we must consider moving directly from #pos to the end point of the path.
				// Otherwise we would just consider paths that first to the side of the triangle and then back to the end point of the path.
				// Note: flag1 checks if this node is connected to the end node of the path.
				if (pathHandler.pathNodes[NodeIndex].flag1) {
					// Note: If we entered this node via an off-mesh link, then #pathNodeIndex may not belong to this node,
					// but instead to the off-mesh link. This is fine. The path can still be reconstructed correctly later.
					ctx.OpenCandidateConnectionsToEndNode(pos, pathNodeIndex, NodeIndex, gScore, ourTraversalCostFactor);
				}

				// Sometimes we may enter a node via e.g. an off-mesh link that doesn't have any other adjacent triangles.
				// In this case we still want to visit at least one side of the triangle, to ensure that paths like the
				// ConstantPath notice that the node has been visited.
				// The code above would otherwise skip this node completely.
				if (visitedEdges == 0) {
					OpenSingleEdge(ref ctx, pathNodeIndex, this, 0, pos, gScore, ourTraversalCostFactor);
				}
			}
		}

		void OpenSingleEdge (ref Path.SearchContext ctx, uint pathNodeIndex, TriangleMeshNode other, int sharedEdgeOnOtherNode, Int3 pos, uint gScore, float traversalCostFactor) {
			var adjacentPathNodeIndex = other.NodeIndex + (uint)sharedEdgeOnOtherNode;

			// Fast path out if we know we have already searched this node and we cannot improve it
			if (!ctx.ShouldConsiderPathNode(adjacentPathNodeIndex)) {
				return;
			}

			var s1 = other.GetVertex(sharedEdgeOnOtherNode);
			var s2 = other.GetVertex((sharedEdgeOnOtherNode + 1) % 3);

			var pathHandler = ctx.pathHandler;

			OpenSingleEdgeBurst(
				ref s1,
				ref s2,
				ref pos,
				ctx.pathID,
				pathNodeIndex,
				adjacentPathNodeIndex,
				other.NodeIndex,
				gScore,
				traversalCostFactor,
				ref pathHandler.pathNodes,
				ref pathHandler.heap,
				ref ctx.heuristicObjective
				);
		}

		[Unity.Burst.BurstCompile]
		static void OpenSingleEdgeBurst (ref Int3 s1, ref Int3 s2, ref Int3 pos, ushort pathID, uint pathNodeIndex, uint candidatePathNodeIndex, uint candidateNodeIndex, uint candidateG, float traversalCostFactor, ref UnsafeSpan<PathNode> pathNodes, ref BinaryHeap heap, ref HeuristicObjective heuristicObjective) {
			CalculateBestEdgePosition(ref s1, ref s2, ref pos, out var closestPointAlongEdge, out var quantizedFractionAlongEdge, out var cost);
			candidateG += (uint)(cost * traversalCostFactor);

			var pars = new Path.SearchContext.OpenCandidateParams {
				pathID = pathID,
				parentPathNode = pathNodeIndex,
				targetPathNode = candidatePathNodeIndex,
				targetNodeIndex = candidateNodeIndex,
				targetG = candidateG,
				fractionAlongEdge = quantizedFractionAlongEdge,
				targetNodePosition = closestPointAlongEdge,
				pathNodes = pathNodes,
			};
			Path.SearchContext.OpenCandidateConnectionBurst(ref pars, ref heap, ref heuristicObjective);
		}

		[Unity.Burst.BurstCompile]
		static void CalculateBestEdgePosition (ref Int3 s1, ref Int3 s2, ref Int3 pos, out int3 closestPointAlongEdge, out uint quantizedFractionAlongEdge, out float cost) {
			// Find the closest point on the other edge. From here on, we will let the position of that path node be this closest point.
			// This is much better than using the edge midpoint, and also better than any interpolation between closestFractionAlongEdge
			// and the midpoint (0.5).
			// In my tests, using the edge midpoint leads to path costs that are rougly 1.3-1.6 times greater than the real distance,
			// but using the closest point leads to path costs that are only 1.1-1.2 times greater than the real distance.
			// Using triangle centers is the worst option, it leads to path costs that are roughly 1.6-2.0 times greater than the real distance.
			// Triangle centers were always used before version 4.3.67.
			var v1 = (float3)(int3)s1;
			var v2 = (float3)(int3)s2;
			var posi = (int3)pos;
			var closestFractionAlongEdge = math.clamp(VectorMath.ClosestPointOnLineFactor(v1, v2, (float3)posi), 0, 1);
			quantizedFractionAlongEdge = PathNode.QuantizeFractionAlongEdge(closestFractionAlongEdge);
			closestFractionAlongEdge = PathNode.UnQuantizeFractionAlongEdge(quantizedFractionAlongEdge);
			var closestPointAlongEdgeV = math.lerp(v1, v2, closestFractionAlongEdge);
			closestPointAlongEdge = (int3)closestPointAlongEdgeV;

			var diff = posi - closestPointAlongEdge;
			cost = new Int3(diff.x, diff.y, diff.z).magnitude;
		}

		/// <summary>
		/// Returns the edge which is shared with other.
		///
		/// If there is no shared edge between the two nodes, then -1 is returned.
		///
		/// The vertices in the edge can be retrieved using
		/// <code>
		/// var edge = node.SharedEdge(other);
		/// var a = node.GetVertex(edge);
		/// var b = node.GetVertex((edge+1) % node.GetVertexCount());
		/// </code>
		///
		/// See: <see cref="GetPortal"/> which also handles edges that are shared over tile borders and some types of node links
		/// </summary>
		public int SharedEdge (GraphNode other) {
			var edge = -1;

			if (connections != null) {
				for (int i = 0; i < connections.Length; i++) {
					if (connections[i].node == other && connections[i].isEdgeShared) edge = connections[i].shapeEdge;
				}
			}
			return edge;
		}

		public override bool GetPortal (GraphNode toNode, out Vector3 left, out Vector3 right) {
			return GetPortal(toNode, out left, out right, out _, out _);
		}

		public bool GetPortalInGraphSpace (TriangleMeshNode toNode, out Int3 a, out Int3 b, out int aIndex, out int bIndex) {
			aIndex = -1;
			bIndex = -1;
			a = Int3.zero;
			b = Int3.zero;

			// If the nodes are in different graphs, this function has no idea on how to find a shared edge.
			if (toNode.GraphIndex != GraphIndex) return false;

			int edge = -1;
			int otherEdge = -1;
			if (connections != null) {
				for (int i = 0; i < connections.Length; i++) {
					if (connections[i].node == toNode && connections[i].isEdgeShared) {
						edge = connections[i].shapeEdge;
						otherEdge = connections[i].adjacentShapeEdge;
					}
				}
			}

			// -1: No connection was found between the nodes
			if (edge == -1) return false;

			aIndex = edge;
			bIndex = (edge + 1) % 3;

			// Get the vertices of the shared edge for the first node
			var graph = GetNavmeshHolder(GraphIndex);
			a = graph.GetVertexInGraphSpace(GetVertexIndex(aIndex));
			b = graph.GetVertexInGraphSpace(GetVertexIndex(bIndex));

			// Get tiles the nodes are contained in
			int tileIndex1 = TileIndex;
			int tileIndex2 = toNode.TileIndex;

			if (tileIndex1 != tileIndex2) {
				// When the nodes are in different tiles, the edges may not be completely identical
				// so another technique is needed.

				// When the nodes are in different tiles, they might not share exactly the same edge
				// so we clamp the portal to the segment of the edges which they both have..

				// Get the vertices of the shared edge for the second node
				Int3 v2a = toNode.GetVertexInGraphSpace(otherEdge);
				Int3 v2b = toNode.GetVertexInGraphSpace((otherEdge+1) % 3);
				graph.GetTileCoordinates(tileIndex1, out var tileX1, out var tileZ1);
				graph.GetTileCoordinates(tileIndex2, out var tileX2, out var tileZ2);
				var axis = tileX1 == tileX2 ? 0 : 2;
				Assert.IsTrue(axis == 0 ? tileX1 == tileX2 : tileZ1 == tileZ2);
				// This tile-edge aligned coordinate of the vertices should ideally be identical.
				// But somewhere in the pipeline some errors may crop up, and thus they may be off by one.
				// TODO: Fix this.
				Assert.IsTrue(Mathf.Abs(a[2 - axis] - b[2 - axis]) <= 1);
				var mn = Mathf.Min(v2a[axis], v2b[axis]);
				var mx = Mathf.Max(v2a[axis], v2b[axis]);

				a[axis] = Mathf.Clamp(a[axis], mn, mx);
				b[axis] = Mathf.Clamp(b[axis], mn, mx);
			}

			return true;
		}

		public bool GetPortal (GraphNode toNode, out Vector3 left, out Vector3 right, out int aIndex, out int bIndex) {
			if (toNode is TriangleMeshNode toTriNode && GetPortalInGraphSpace(toTriNode, out var a, out var b, out aIndex, out bIndex)) {
				var graph = GetNavmeshHolder(GraphIndex);
				// All triangles should be laid out in clockwise order so b is the rightmost vertex (seen from this node)
				left = graph.transform.Transform((Vector3)a);
				right = graph.transform.Transform((Vector3)b);
				return true;
			} else {
				aIndex = -1;
				bIndex = -1;
				left = Vector3.zero;
				right = Vector3.zero;
				return false;
			}
		}

		/// <summary>TODO: This is the area in XZ space, use full 3D space for higher correctness maybe?</summary>
		public override float SurfaceArea () {
			var holder = GetNavmeshHolder(GraphIndex);

			return System.Math.Abs(VectorMath.SignedTriangleAreaTimes2XZ(holder.GetVertex(v0), holder.GetVertex(v1), holder.GetVertex(v2))) * 0.5f;
		}

		public override Vector3 RandomPointOnSurface () {
			// Find a random point inside the triangle
			// This generates uniformly distributed trilinear coordinates
			// See http://mathworld.wolfram.com/TrianglePointPicking.html
			float2 r;

			do {
				r = AstarMath.ThreadSafeRandomFloat2();
			} while (r.x+r.y > 1);

			// Pick the point corresponding to the trilinear coordinate
			GetVertices(out var v0, out var v1, out var v2);
			return ((Vector3)(v1-v0))*r.x + ((Vector3)(v2-v0))*r.y + (Vector3)v0;
		}

		public override void SerializeNode (GraphSerializationContext ctx) {
			base.SerializeNode(ctx);
			ctx.writer.Write(v0);
			ctx.writer.Write(v1);
			ctx.writer.Write(v2);
		}

		public override void DeserializeNode (GraphSerializationContext ctx) {
			base.DeserializeNode(ctx);
			v0 = ctx.reader.ReadInt32();
			v1 = ctx.reader.ReadInt32();
			v2 = ctx.reader.ReadInt32();
		}
	}
}
