using UnityEngine;
using System.Collections.Generic;
using Unity.Mathematics;
using Pathfinding.Pooling;

namespace Pathfinding {
	/// <summary>
	/// Basic path, finds the shortest path from A to B.
	///
	/// This is the most basic path object it will try to find the shortest path between two points.
	/// Many other path types inherit from this type.
	/// See: Seeker.StartPath
	/// See: calling-pathfinding (view in online documentation for working links)
	/// See: getstarted (view in online documentation for working links)
	/// </summary>
	public class ABPath : Path {
		/// <summary>Start node of the path</summary>
		public GraphNode startNode => path.Count > 0 ? path[0] : null;

		/// <summary>End node of the path</summary>
		public GraphNode endNode => path.Count > 0 ? path[path.Count-1] : null;

		/// <summary>Start Point exactly as in the path request</summary>
		public Vector3 originalStartPoint;

		/// <summary>End Point exactly as in the path request</summary>
		public Vector3 originalEndPoint;

		/// <summary>
		/// Start point of the path.
		/// This is the closest point on the <see cref="startNode"/> to <see cref="originalStartPoint"/>
		/// </summary>
		public Vector3 startPoint;

		/// <summary>
		/// End point of the path.
		/// This is the closest point on the <see cref="endNode"/> to <see cref="originalEndPoint"/>
		/// </summary>
		public Vector3 endPoint;

		/// <summary>
		/// Total cost of this path as used by the pathfinding algorithm.
		///
		/// The cost is influenced by both the length of the path, as well as any tags or penalties on the nodes.
		/// By default, the cost to move 1 world unit is <see cref="Int3.Precision"/>.
		///
		/// If the path failed, the cost will be set to zero.
		///
		/// See: tags (view in online documentation for working links)
		/// </summary>
		public uint cost;

		/// <summary>
		/// Determines if a search for an end node should be done.
		/// Set by different path types.
		/// Since: Added in 3.0.8.3
		/// </summary>
		protected virtual bool hasEndPoint => true;

		/// <summary>
		/// True if this path type has a well defined end point, even before calculation starts.
		///
		/// This is for example true for the <see cref="ABPath"/> type, but false for the <see cref="RandomPath"/> type.
		/// </summary>
		public virtual bool endPointKnownBeforeCalculation => true;

		/// <summary>
		/// Calculate partial path if the target node cannot be reached.
		/// If the target node cannot be reached, the node which was closest (given by heuristic) will be chosen as target node
		/// and a partial path will be returned.
		/// This only works if a heuristic is used (which is the default).
		/// If a partial path is found, CompleteState is set to Partial.
		/// Note: It is not required by other path types to respect this setting
		///
		/// The <see cref="endNode"/> and <see cref="endPoint"/> will be modified and be set to the node which ends up being closest to the target.
		///
		/// Warning: Using this may make path calculations significantly slower if you have a big graph. The reason is that
		/// when the target node cannot be reached, the path must search through every single other node that it can reach in order
		/// to determine which one is closest. This may be expensive, and is why this option is disabled by default.
		/// </summary>
		public bool calculatePartial;

		/// <summary>
		/// Current best target for the partial path.
		/// This is the node with the lowest H score.
		/// </summary>
		protected uint partialBestTargetPathNodeIndex = GraphNode.InvalidNodeIndex;
		protected uint partialBestTargetHScore = uint.MaxValue;
		protected uint partialBestTargetGScore = uint.MaxValue;

		/// <summary>
		/// Optional ending condition for the path.
		///
		/// The ending condition determines when the path has been completed.
		/// Can be used to for example mark a path as complete when it is within a specific distance from the target.
		///
		/// If ending conditions are used that are not centered around the endpoint of the path,
		/// then you should also set the <see cref="heuristic"/> to None to ensure the path is still optimal.
		/// The performance impact of setting the <see cref="heuristic"/> to None is quite large, so you might want to try to run it with the default
		/// heuristic to see if the path is good enough for your use case anyway.
		///
		/// If null, no custom ending condition will be used. This means that the path will end when the target node has been reached.
		///
		/// Note: If the ending condition returns false for all nodes, the path will just keep searching until it has searched the whole graph. This can be slow.
		///
		/// See: <see cref="PathEndingCondition"/>
		/// </summary>
		public PathEndingCondition endingCondition;

		/// <summary>@{ @name Constructors</summary>

		/// <summary>
		/// Default constructor.
		/// Do not use this. Instead use the static Construct method which can handle path pooling.
		/// </summary>
		public ABPath () {}

		/// <summary>
		/// Construct a path with a start and end point.
		/// The delegate will be called when the path has been calculated.
		/// Do not confuse it with the Seeker callback as they are sent at different times.
		/// If you are using a Seeker to start the path you can set callback to null.
		///
		/// Returns: The constructed path object
		/// </summary>
		public static ABPath Construct (Vector3 start, Vector3 end, OnPathDelegate callback = null) {
			var p = PathPool.GetPath<ABPath>();

			p.Setup(start, end, callback);
			return p;
		}

		protected void Setup (Vector3 start, Vector3 end, OnPathDelegate callbackDelegate) {
			callback = callbackDelegate;
			UpdateStartEnd(start, end);
		}

		/// <summary>
		/// Creates a fake path.
		/// Creates a path that looks almost exactly like it would if the pathfinding system had calculated it.
		///
		/// This is useful if you want your agents to follow some known path that cannot be calculated using the pathfinding system for some reason.
		///
		/// <code>
		/// var path = ABPath.FakePath(new List<Vector3> { new Vector3(1, 2, 3), new Vector3(4, 5, 6) });
		///
		/// ai.SetPath(path);
		/// </code>
		///
		/// You can use it to combine existing paths like this:
		///
		/// <code>
		/// var a = Vector3.zero;
		/// var b = new Vector3(1, 2, 3);
		/// var c = new Vector3(2, 3, 4);
		/// var path1 = ABPath.Construct(a, b);
		/// var path2 = ABPath.Construct(b, c);
		///
		/// AstarPath.StartPath(path1);
		/// AstarPath.StartPath(path2);
		/// path1.BlockUntilCalculated();
		/// path2.BlockUntilCalculated();
		///
		/// // Combine the paths
		/// // Note: Skip the first element in the second path as that will likely be the last element in the first path
		/// var newVectorPath = path1.vectorPath.Concat(path2.vectorPath.Skip(1)).ToList();
		/// var newNodePath = path1.path.Concat(path2.path.Skip(1)).ToList();
		/// var combinedPath = ABPath.FakePath(newVectorPath, newNodePath);
		/// </code>
		/// </summary>
		public static ABPath FakePath (List<Vector3> vectorPath, List<GraphNode> nodePath = null) {
			var path = PathPool.GetPath<ABPath>();

			for (int i = 0; i < vectorPath.Count; i++) path.vectorPath.Add(vectorPath[i]);

			path.completeState = PathCompleteState.Complete;
			((IPathInternals)path).AdvanceState(PathState.Returned);

			if (vectorPath.Count > 0) {
				path.UpdateStartEnd(vectorPath[0], vectorPath[vectorPath.Count - 1]);
			}

			if (nodePath != null) {
				for (int i = 0; i < nodePath.Count; i++) path.path.Add(nodePath[i]);
			}

			return path;
		}

		/// <summary>@}</summary>

		/// <summary>
		/// Sets the start and end points.
		/// Sets <see cref="originalStartPoint"/>, <see cref="originalEndPoint"/>, <see cref="startPoint"/>, <see cref="endPoint"/>
		/// </summary>
		protected void UpdateStartEnd (Vector3 start, Vector3 end) {
			originalStartPoint = start;
			originalEndPoint = end;

			startPoint = start;
			endPoint = end;
		}

		/// <summary>
		/// Reset all values to their default values.
		/// All inheriting path types must implement this function, resetting ALL their variables to enable recycling of paths.
		/// Call this base function in inheriting types with base.Reset ();
		/// </summary>
		protected override void Reset () {
			base.Reset();

			originalStartPoint = Vector3.zero;
			originalEndPoint = Vector3.zero;
			startPoint = Vector3.zero;
			endPoint = Vector3.zero;
			calculatePartial = false;
			partialBestTargetPathNodeIndex = GraphNode.InvalidNodeIndex;
			partialBestTargetHScore = uint.MaxValue;
			partialBestTargetGScore = uint.MaxValue;
			cost = 0;
			endingCondition = null;
		}

#if !ASTAR_NO_GRID_GRAPH
		/// <summary>
		/// Applies a special case for grid nodes.
		///
		/// Assume the closest walkable node is a grid node.
		/// We will now apply a special case only for grid graphs.
		/// In tile based games, an obstacle often occupies a whole
		/// node. When a path is requested to the position of an obstacle
		/// (single unwalkable node) the closest walkable node will be
		/// one of the 8 nodes surrounding that unwalkable node
		/// but that node is not neccessarily the one that is most
		/// optimal to walk to so in this special case
		/// we mark all nodes around the unwalkable node as targets
		/// and when we search and find any one of them we simply exit
		/// and set that first node we found to be the 'real' end node
		/// because that will be the optimal node (this does not apply
		/// in general unless the heuristic is set to None, but
		/// for a single unwalkable node it does).
		/// This also applies if the nearest node cannot be traversed for
		/// some other reason like restricted tags.
		///
		/// Returns: True if the workaround was applied. If this happens, new temporary endpoints will have been added
		///
		/// Image below shows paths when this special case is applied. The path goes from the white sphere to the orange box.
		/// [Open online documentation to see images]
		///
		/// Image below shows paths when this special case has been disabled
		/// [Open online documentation to see images]
		/// </summary>
		protected virtual bool EndPointGridGraphSpecialCase (ref SearchContext ctx, NearestNodeConstraint nn, NNInfo closestWalkableEndNode, Vector3 originalEndPoint, int targetIndex) {
			var gridNode = closestWalkableEndNode.node as GridNode;

			if (gridNode != null) {
				if (gridNode.ContainsPoint(originalEndPoint)) {
					// Can't get any closer than this.
					// Performance optimization for the common case.
					return false;
				}

				var gridGraph = GridNode.GetGridGraph(gridNode.GraphIndex);

				// Find the closest node, not neccessarily walkable
				var nn2 = NearestNodeConstraint.None;
				nn2.distanceMetric = nn.distanceMetric;
				// We know we can find a node at least this close
				nn2.maxDistanceSqr = closestWalkableEndNode.distanceCostSqr + 0.001f;
				var endNNInfo2 = gridGraph.GetNearest(originalEndPoint, ref nn2);
				var gridNode2 = endNNInfo2.node as GridNode;

				// We know we can find a node here, since closestWalkableEndNode.node exists
				UnityEngine.Assertions.Assert.IsNotNull(gridNode2);

				// If the closest node is different from the closest walkable node
				// then we might want to apply the special case.
				//
				// Cases:
				// 1. point is outside grid graph -> should not apply special case.
				// 2. point is above an unwalkable node, which would have been the closest one according to the original distance metric, had it not been unwalkable -> should apply special case.
				// 3. point is above an unwalkable node, but it's not the closest one according to the original distance metric (e.g. when jumping off a ledge) -> should not apply special case.
				if (gridNode != gridNode2 && gridNode2 != null && gridNode2.ContainsPoint(originalEndPoint)) {
					// Calculate the coordinates of the nodes
					var x1 = gridNode.NodeInGridIndex % gridGraph.width;
					var z1 = gridNode.NodeInGridIndex / gridGraph.width;

					var x2 = gridNode2.NodeInGridIndex % gridGraph.width;
					var z2 = gridNode2.NodeInGridIndex / gridGraph.width;

					bool wasClose = false;
					switch (gridGraph.neighbours) {
					case NumNeighbours.Four:
						if ((x1 == x2 && System.Math.Abs(z1-z2) == 1) || (z1 == z2 && System.Math.Abs(x1-x2) == 1)) {
							// If 'O' is gridNode2, then gridNode is one of the nodes marked with an 'x'
							//    x
							//  x O x
							//    x
							wasClose = true;
						}
						break;
					case NumNeighbours.Eight:
						if (System.Math.Abs(x1-x2) <= 1 && System.Math.Abs(z1-z2) <= 1) {
							// If 'O' is gridNode2, then gridNode is one of the nodes marked with an 'x'
							//  x x x
							//  x O x
							//  x x x
							wasClose = true;
						}
						break;
					case NumNeighbours.Six:
						// Hexagon graph
						for (int i = 0; i < 6; i++) {
							var nx = x2 + GridGraph.neighbourXOffsets[GridGraph.hexagonNeighbourIndices[i]];
							var nz = z2 + GridGraph.neighbourZOffsets[GridGraph.hexagonNeighbourIndices[i]];
							if (x1 == nx && z1 == nz) {
								// If 'O' is gridNode2, then gridNode is one of the nodes marked with an 'x'
								//    x x
								//  x O x
								//  x x
								wasClose = true;
								break;
							}
						}
						break;
					default:
						// Should not happen unless NumNeighbours is modified in the future
						throw new System.Exception("Unhandled NumNeighbours");
					}

					if (wasClose) {
						// We now need to find all nodes marked with an x to be able to mark them as targets
						AddEndpointsForSurroundingGridNodes(ref ctx, gridNode2, originalEndPoint, targetIndex);

						// hTargetNode is used for heuristic optimizations
						// (also known as euclidean embedding).
						// Even though the endNode is not walkable
						// we can use it for better heuristics since
						// there is a workaround added (EuclideanEmbedding.ApplyGridGraphEndpointSpecialCase)
						// which is there to support this case.
						// TODO
						return true;
					}
				}
			}

			return false;
		}

		/// <summary>Helper method to add endpoints around a specific unwalkable grid node</summary>
		void AddEndpointsForSurroundingGridNodes (ref SearchContext ctx, GridNode gridNode, Vector3 desiredPoint, int targetIndex) {
			// Loop through all adjacent grid nodes
			var gridGraph = GridNode.GetGridGraph(gridNode.GraphIndex);

			// Number of neighbours as an int
			int mxnum = gridGraph.neighbours == NumNeighbours.Four ? 4 : (gridGraph.neighbours == NumNeighbours.Eight ? 8 : 6);

			// Calculate the coordinates of the node
			var x = gridNode.NodeInGridIndex % gridGraph.width;
			var z = gridNode.NodeInGridIndex / gridGraph.width;

			for (int i = 0; i < mxnum; i++) {
				int nx, nz;
				if (gridGraph.neighbours == NumNeighbours.Six) {
					// Hexagon graph
					nx = x + GridGraph.neighbourXOffsets[GridGraph.hexagonNeighbourIndices[i]];
					nz = z + GridGraph.neighbourZOffsets[GridGraph.hexagonNeighbourIndices[i]];
				} else {
					nx = x + GridGraph.neighbourXOffsets[i];
					nz = z + GridGraph.neighbourZOffsets[i];
				}

				var adjacentNode = gridGraph.GetNode(nx, nz);
				// Check if the position is still inside the grid
				if (adjacentNode != null && traversalConstraint.CanTraverse(adjacentNode)) {
					ctx.pathHandler.AddTemporaryNode(new TemporaryNode {
						type = TemporaryNodeType.End,
						position = (Int3)adjacentNode.ClosestPointOnNode(desiredPoint),
						associatedNode = adjacentNode.NodeIndex,
						targetIndex = targetIndex,
					});
				}
			}
		}
#endif

		/// <summary>Prepares the path. Searches for start and end nodes and does some simple checking if a path is at all possible</summary>
		protected override void Prepare (ref SearchContext ctx) {
			var constraint = this.nearestNodeConstraint;
			FindStartAndEndNodes(ref ctx, ref constraint);
		}

		protected void FindStartAndEndNodes (ref SearchContext ctx, ref NearestNodeConstraint constraint) {
			var startNNInfo = AstarPath.active.GetNearest(startPoint, constraint);

			startPoint = startNNInfo.position;

			if (startNNInfo.node == null) {
				FailWithError("Couldn't find a node close to the start point");
				return;
			}

			UnityEngine.Assertions.Assert.IsTrue(traversalConstraint.CanTraverse(startNNInfo.node));

			ctx.pathHandler.AddTemporaryNode(new TemporaryNode {
				associatedNode = startNNInfo.node.NodeIndex,
				position = (Int3)startNNInfo.position,
				type = TemporaryNodeType.Start,
			});

			// If it is declared that this path type has an end point
			// Some path types might want to use most of the ABPath code, but will not have an explicit end point at this stage
			uint endNodeIndex = 0;
			if (hasEndPoint) {
				// Find the closest node that we can reach from the start node
				constraint.area = (int)startNNInfo.node.Area;
				var endNNInfo = AstarPath.active.GetNearest(originalEndPoint, constraint);
				endPoint = endNNInfo.position;

				if (endNNInfo.node == null) {
					FailWithError("Couldn't find a node close to the end point");
					return;
				}

				UnityEngine.Assertions.Assert.IsTrue(traversalConstraint.CanTraverse(endNNInfo.node));
				UnityEngine.Assertions.Assert.AreEqual(startNNInfo.node.Area, endNNInfo.node.Area);

				endNodeIndex = endNNInfo.node.NodeIndex;

#if !ASTAR_NO_GRID_GRAPH
				// Potentially we want to special case grid graphs a bit
				// to better support some kinds of games
				if (!EndPointGridGraphSpecialCase(ref ctx, constraint, endNNInfo, originalEndPoint, 0))
#endif
				{
					ctx.pathHandler.AddTemporaryNode(new TemporaryNode {
						associatedNode = endNNInfo.node.NodeIndex,
						position = (Int3)endNNInfo.position,
						type = TemporaryNodeType.End,
					});
				}
			}

			ctx.TemporaryEndNodesBoundingBox(out var mnTarget, out var mxTarget);
			ctx.heuristicObjective = new HeuristicObjective(mnTarget, mxTarget, heuristic, heuristicScale, endNodeIndex, AstarPath.active.euclideanEmbedding);
			ctx.traversalConstraint = traversalConstraint;
			ctx.traversalCosts = traversalCosts;
			ctx.MarkNodesAdjacentToTemporaryEndNodes();
			ctx.AddStartNodesToHeap();
		}

		void CompletePartial (ref SearchContext ctx) {
			CompleteState = PathCompleteState.Partial;
			// TODO: Add unit test for this
			endPoint = ctx.pathHandler.GetNode(partialBestTargetPathNodeIndex).ClosestPointOnNode(originalEndPoint);
			cost = partialBestTargetGScore;
			Trace(ref ctx, partialBestTargetPathNodeIndex);
		}

		/// <summary>
		/// Refreshes the start and end points of the path.
		/// Useful if the agent has moved significantly since the path was requested.
		///
		/// If the path has not yet been processed by the pathfinding system, the start and end points will be updated immediately.
		/// If the path is being calculated or has been calculated, this function will do nothing.
		///
		/// A common way is to call this function once per frame until the path has been calculated,
		/// with the most up-to-date positions of the agent and the target.
		/// </summary>
		public virtual void RefreshPathEndpoints (Vector3 start, Vector3 end) {
			lock (this) {
				if (PipelineState == PathState.PathQueue || PipelineState == PathState.Created) {
					UpdateStartEnd(start, end);
				}
			}
		}

		protected override void OnHeapExhausted (ref SearchContext ctx) {
			if (calculatePartial && partialBestTargetPathNodeIndex != GraphNode.InvalidNodeIndex) {
				CompletePartial(ref ctx);
			} else {
				FailWithError("Searched all reachable nodes, but could not find target. This can happen if you have nodes with a different tag blocking the way to the goal. You can enable path.calculatePartial to handle that case as a workaround (though this comes with a performance cost).");
			}
		}

		protected override void OnFoundEndNode (ref SearchContext ctx, uint pathNode, uint hScore, uint gScore) {
			var pathHandler = ctx.pathHandler;
			if (pathHandler.IsTemporaryNode(pathNode)) {
				// Common case, a temporary node is used to represent the target.
				// However, it may not be clamped to the closest point on the node.
				// In particular the grid graph special case will not clamp it.
				// So we clamp it here instead.
				var tempNode = pathHandler.GetTemporaryNode(pathNode);
				var associatedNode = pathHandler.GetNode(tempNode.associatedNode);
				if (endingCondition != null && !endingCondition.TargetFound(associatedNode, partialBestTargetHScore, gScore)) {
					// The ending condition is not fulfilled, so we should not stop here.
					// This is a weird situation where the ending condition doesn't consider the closest node to the destination
					// as a valid end node. It can be useful in rare cases, though.
					return;
				}
				endPoint = (Vector3)tempNode.position;
				endPoint = associatedNode.ClosestPointOnNode(endPoint);
			} else {
				// The target node is a normal node. We use the center of the node as the end point.
				// This can happen when using a custom ending condition.
				var node = pathHandler.GetNode(pathNode);
				endPoint = (Vector3)node.position;
			}
			cost = gScore;
			CompleteState = PathCompleteState.Complete;
			Trace(ref ctx, pathNode);
		}

		public override void OnVisitNode (ref SearchContext ctx, uint pathNode, uint hScore, uint gScore) {
			// This method may be called multiple times without checking if the path is complete yet.
			if (CompleteState != PathCompleteState.NotCalculated) return;

			if (endingCondition != null) {
				var node = ctx.pathHandler.GetNode(pathNode);
				if (endingCondition.TargetFound(node, hScore, gScore)) {
					OnFoundEndNode(ref ctx, pathNode, hScore, gScore);
					if (CompleteState == PathCompleteState.Complete) {
						return;
					}
				}
			}

			if (hScore < partialBestTargetHScore) {
				partialBestTargetPathNodeIndex = pathNode;
				partialBestTargetHScore = hScore;
				partialBestTargetGScore = gScore;
			}
		}

		/// <summary>Returns a debug string for this path.</summary>
		protected override void DebugString (System.Text.StringBuilder builder, PathLog logMode) {
			if (logMode == PathLog.None || (!error && logMode == PathLog.OnlyErrors)) {
				return;
			}

			DebugStringPrefix(logMode, builder);
			if (!error) {
				builder.Append(" Path Cost: ");
				builder.Append(cost);
			}

			if (!error && logMode == PathLog.Heavy) {
				if (hasEndPoint && endNode != null) {
					// builder.Append("\nEnd Node\n	G: ");
					// builder.Append(nodeR.G);
					// builder.Append("\n	H: ");
					// builder.Append(nodeR.H);
					// builder.Append("\n	F: ");
					// builder.Append(nodeR.F);
					builder.Append("\n	Point: ");
					builder.Append(((Vector3)endPoint).ToString());
					builder.Append("\n	Graph: ");
					builder.Append(endNode.GraphIndex);
				}

				builder.Append("\nStart Node");
				builder.Append("\n	Point: ");
				builder.Append(((Vector3)startPoint).ToString());
				builder.Append("\n	Graph: ");
				if (startNode != null) builder.Append(startNode.GraphIndex);
				else builder.Append("< null startNode >");
			}

			DebugStringSuffix(logMode, builder);
		}
	}
}
