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

namespace Pathfinding {
	/// <summary>
	/// A path which searches from one point to a number of different targets in one search or from a number of different start points to a single target.
	///
	/// This is faster than searching with an ABPath for each target if pathsForAll is true.
	/// This path type can be used for example when you want an agent to find the closest target of a few different options.
	///
	/// When pathsForAll is true, it will calculate a path to each target point, but it can share a lot of calculations for the different paths so
	/// it is faster than requesting them separately.
	///
	/// When pathsForAll is false, it will perform a search using the heuristic set to None and stop as soon as it finds the first target.
	/// This may be faster or slower than requesting each path separately.
	/// It will run a Dijkstra search where it searches all nodes around the start point until the closest target is found.
	/// Note that this is usually faster if some target points are very close to the start point and some are very far away, but
	/// it can be slower if all target points are relatively far away because then it will have to search a much larger
	/// region since it will not use any heuristics.
	///
	/// See: Seeker.StartMultiTargetPath
	/// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths"
	///
	/// Version: Since 3.7.1 the vectorPath and path fields are always set to the shortest path even when pathsForAll is true.
	/// </summary>
	public class MultiTargetPath : ABPath {
		/// <summary>Callbacks to call for each individual path</summary>
		public OnPathDelegate[] callbacks;

		/// <summary>Nearest nodes to the <see cref="targetPoints"/></summary>
		public GraphNode[] targetNodes;

		/// <summary>Number of target nodes left to find</summary>
		protected int targetNodeCount;

		/// <summary>Indicates if the target has been found. Also true if the target cannot be reached (is in another area)</summary>
		public bool[] targetsFound;

		/// <summary>The cost of the calculated path for each target. Will be 0 if a path was not found.</summary>
		public uint[] targetPathCosts;

		/// <summary>Target points specified when creating the path. These are snapped to the nearest nodes</summary>
		public Vector3[] targetPoints;

		/// <summary>Target points specified when creating the path. These are not snapped to the nearest nodes</summary>
		public Vector3[] originalTargetPoints;

		/// <summary>Stores all vector paths to the targets. Elements are null if no path was found</summary>
		public List<Vector3>[] vectorPaths;

		/// <summary>Stores all paths to the targets. Elements are null if no path was found</summary>
		public List<GraphNode>[] nodePaths;

		/// <summary>If true, a path to all targets will be returned, otherwise just the one to the closest one.</summary>
		public bool pathsForAll = true;

		/// <summary>The closest target index (if any target was found)</summary>
		public int chosenTarget = -1;

		/// <summary>False if the path goes from one point to multiple targets. True if it goes from multiple start points to one target point</summary>
		public bool inverted { get; protected set; }

		public override bool endPointKnownBeforeCalculation => false;

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

		public static MultiTargetPath Construct (Vector3[] startPoints, Vector3 target, OnPathDelegate[] callbackDelegates, OnPathDelegate callback = null) {
			MultiTargetPath p = Construct(target, startPoints, callbackDelegates, callback);

			p.inverted = true;
			return p;
		}

		public static MultiTargetPath Construct (Vector3 start, Vector3[] targets, OnPathDelegate[] callbackDelegates, OnPathDelegate callback = null) {
			var p = PathPool.GetPath<MultiTargetPath>();

			p.Setup(start, targets, callbackDelegates, callback);
			return p;
		}

		/// <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.
		///
		/// Note: For a multi target path, this function only supports updating the common start point or the common end point. Not all individual targets/start points. Depending on which constructor is used, either the start parameter or the end parameter will be used, and the other one ignored.
		/// </summary>
		public override void RefreshPathEndpoints (Vector3 start, Vector3 end) {
			lock (this) {
				if (PipelineState == PathState.PathQueue || PipelineState == PathState.Created) {
					// This function only supports updating the common point of all paths, for this path type.
					// So either the common start point or the common end point.
					originalStartPoint = startPoint = inverted ? end : start;
				}
			}
		}

		protected void Setup (Vector3 start, Vector3[] targets, OnPathDelegate[] callbackDelegates, OnPathDelegate callback) {
			inverted = false;
			this.callback = callback;
			callbacks = callbackDelegates;
			if (callbacks != null && callbacks.Length != targets.Length) throw new System.ArgumentException("The targets array must have the same length as the callbackDelegates array");
			targetPoints = targets;

			originalStartPoint = start;

			startPoint = start;

			if (targets.Length == 0) {
				FailWithError("No targets were assigned to the MultiTargetPath");
				return;
			}

			endPoint = targets[0];

			originalTargetPoints = new Vector3[targetPoints.Length];
			for (int i = 0; i < targetPoints.Length; i++) {
				originalTargetPoints[i] = targetPoints[i];
			}
		}

		protected override void Reset () {
			base.Reset();
			pathsForAll = true;
			chosenTarget = -1;
			inverted = true;
		}

		protected override void OnEnterPool () {
			if (vectorPaths != null)
				for (int i = 0; i < vectorPaths.Length; i++)
					if (vectorPaths[i] != null) ListPool<Vector3>.Release(vectorPaths[i]);

			vectorPaths = null;
			vectorPath = null;

			if (nodePaths != null)
				for (int i = 0; i < nodePaths.Length; i++)
					if (nodePaths[i] != null) ListPool<GraphNode>.Release(nodePaths[i]);

			nodePaths = null;
			path = null;
			callbacks = null;
			targetNodes = null;
			targetsFound = null;
			targetPathCosts = null;
			targetPoints = null;
			originalTargetPoints = null;

			base.OnEnterPool();
		}

		/// <summary>Set chosenTarget to the index of the shortest path</summary>
		void ChooseShortestPath () {
			// When pathsForAll is false there will be at most one non-null path
			chosenTarget = -1;
			if (nodePaths != null) {
				uint bestG = uint.MaxValue;
				for (int i = 0; i < nodePaths.Length; i++) {
					if (nodePaths[i] != null) {
						var g = targetPathCosts[i];
						if (g < bestG) {
							chosenTarget = i;
							bestG = g;
						}
					}
				}
			}
		}

		void SetPathParametersForReturn (int target) {
			path = nodePaths[target];
			vectorPath = vectorPaths[target];

			if (inverted) {
				startPoint = targetPoints[target];
				originalStartPoint = originalTargetPoints[target];
			} else {
				endPoint = targetPoints[target];
				originalEndPoint = originalTargetPoints[target];
			}
			cost = path != null ? targetPathCosts[target] : 0;
		}

		protected override void ReturnPath () {
			if (error) {
				// Call all callbacks
				if (callbacks != null) {
					for (int i = 0; i < callbacks.Length; i++)
						if (callbacks[i] != null) callbacks[i] (this);
				}

				if (callback != null) callback(this);

				return;
			}

			bool anySucceded = false;

			// Set the end point to the start point
			// since the path is reversed
			// (the start point will be set individually for each path)
			if (inverted) {
				endPoint = startPoint;
				originalEndPoint = originalStartPoint;
			}

			for (int i = 0; i < nodePaths.Length; i++) {
				if (nodePaths[i] != null) {
					// Note that we use the lowercase 'completeState' here.
					// The property (CompleteState) will ensure that the complete state is never
					// changed away from the error state but in this case we don't want that behaviour.
					completeState = PathCompleteState.Complete;
					anySucceded = true;
				} else {
					completeState = PathCompleteState.Error;
				}

				if (callbacks != null && callbacks[i] != null) {
					SetPathParametersForReturn(i);
					callbacks[i] (this);

					// In case a modifier changed the vectorPath, update the array of all vectorPaths
					vectorPaths[i] = vectorPath;
				}
			}

			if (anySucceded) {
				completeState = PathCompleteState.Complete;
				SetPathParametersForReturn(chosenTarget);
			} else {
				completeState = PathCompleteState.Error;
			}

			if (callback != null) {
				callback(this);
			}
		}

		protected void RebuildOpenList (ref SearchContext ctx) {
			var pathHandler = ctx.pathHandler;
			BinaryHeap heap = pathHandler.heap;
			if (heap.tieBreaking != BinaryHeap.TieBreaking.HScore) return;

			for (int i = 0; i < heap.numberOfItems; i++) {
				var pathNodeIndex = heap.GetPathNodeIndex(i);
				Int3 pos;
				if (pathHandler.IsTemporaryNode(pathNodeIndex)) {
					pos = pathHandler.GetTemporaryNode(pathNodeIndex).position;
				} else {
					pos = pathHandler.GetNode(pathNodeIndex).DecodeVariantPosition(pathNodeIndex, pathHandler.pathNodes[pathNodeIndex].fractionAlongEdge);
				}
				// Note: node index can be 0 here because the multi target path never uses the euclidean embedding
				var hScore = (uint)ctx.heuristicObjective.Calculate((int3)pos, 0);
				heap.SetH(i, hScore);
			}

			pathHandler.heap.Rebuild(pathHandler.pathNodes);
		}

		protected override void Prepare (ref SearchContext ctx) {
			var constraint = nearestNodeConstraint;
			var startNNInfo = AstarPath.active.GetNearest(startPoint, constraint);
			var startNode = startNNInfo.node;

			if (endingCondition != null) {
				FailWithError("Multi target paths cannot use custom ending conditions");
				return;
			}

			if (startNode == null) {
				FailWithError("Could not find start node for multi target path");
				return;
			}

			UnityEngine.Assertions.Assert.IsTrue(traversalConstraint.CanTraverse(startNode));

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

			// Find the closest nodes that we can reach from the start node
			constraint.area = (int)startNode.Area;

			vectorPaths = new List<Vector3>[targetPoints.Length];
			nodePaths = new List<GraphNode>[targetPoints.Length];
			targetNodes = new GraphNode[targetPoints.Length];
			targetsFound = new bool[targetPoints.Length];
			targetPathCosts = new uint[targetPoints.Length];
			targetNodeCount = 0;

			bool anyReachable = false;

			for (int i = 0; i < targetPoints.Length; i++) {
				var originalTarget = targetPoints[i];
				var endNNInfo = AstarPath.active.GetNearest(originalTarget, constraint);

				targetNodes[i] = endNNInfo.node;

				targetPoints[i] = endNNInfo.position;

				if (endNNInfo.node != null) {
					anyReachable = true;
					UnityEngine.Assertions.Assert.IsTrue(traversalConstraint.CanTraverse(endNNInfo.node));
					UnityEngine.Assertions.Assert.IsTrue(endNNInfo.node.Area == startNode.Area);

					targetNodeCount++;
#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, originalTarget, i))
#endif
					{
						ctx.pathHandler.AddTemporaryNode(new TemporaryNode {
							associatedNode = endNNInfo.node.NodeIndex,
							position = (Int3)endNNInfo.position,
							targetIndex = i,
							type = TemporaryNodeType.End,
						});
					}
				} else {
					// Signal that the pathfinder should not look for this target because we cannot find a valid target node
					targetsFound[i] = true;
				}
			}

			startPoint = startNNInfo.position;

			if (!anyReachable) {
				FailWithError("Couldn't find a valid node close to the any of the end points");
				return;
			}

			ctx.traversalConstraint = traversalConstraint;
			ctx.traversalCosts = traversalCosts;
			ctx.MarkNodesAdjacentToTemporaryEndNodes();
			ctx.AddStartNodesToHeap();
			RecalculateHTarget(ref ctx);
		}

		void RecalculateHTarget (ref SearchContext ctx) {
			if (pathsForAll) {
				// Sequentially go through all targets.
				// First we will find the path to the first target (or at least aim for it, we might find another one along the way),
				// then we will change the heuristic objective to the second target and so on.
				// This does not guarantee that we find the targets in order of closest to furthest,
				// but that is not required since we want to find all paths anyway.
				var target = ctx.FirstTemporaryEndNode();
				ctx.heuristicObjective = new HeuristicObjective(target, target, heuristic, heuristicScale, 0, null);
			} else {
				// Create a bounding box that contains all the end points,
				// and use that to calculate the heuristic.
				// This will ensure we find the closest target first.
				ctx.TemporaryEndNodesBoundingBox(out var mnTarget, out var mxTarget);
				ctx.heuristicObjective = new HeuristicObjective(mnTarget, mxTarget, heuristic, heuristicScale, 0, null);
			}

			// Rebuild the open list since all the H scores have changed
			RebuildOpenList(ref ctx);
		}

		protected override void Cleanup (ref SearchContext ctx) {
			// Make sure that the shortest path is set
			// after the path has been calculated
			ChooseShortestPath();
			base.Cleanup(ref ctx);
		}

		protected override void OnHeapExhausted (ref SearchContext ctx) {
			CompleteState = PathCompleteState.Complete;
		}

		protected override void OnFoundEndNode (ref SearchContext ctx, uint pathNode, uint hScore, uint gScore) {
			if (!ctx.pathHandler.IsTemporaryNode(pathNode)) {
				FailWithError("Expected the end node to be a temporary node. Cannot determine which path it belongs to. This could happen if you are using a custom ending condition for the path.");
				return;
			}

			var targetIndex = ctx.pathHandler.GetTemporaryNode(pathNode).targetIndex;
			if (targetsFound[targetIndex]) throw new System.ArgumentException("This target has already been found");

			Trace(ref ctx, pathNode);
			vectorPaths[targetIndex] = vectorPath;
			nodePaths[targetIndex] = path;
			vectorPath = ListPool<Vector3>.Claim();
			path = ListPool<GraphNode>.Claim();

			targetsFound[targetIndex] = true;
			targetPathCosts[targetIndex] = gScore;

			targetNodeCount--;

			// Mark all end nodes for this target as ignored to avoid including them
			// in the H-score calculation and to avoid calling OnFoundEndNode for this
			// target index again.
			for (uint i = 0; i < ctx.pathHandler.numTemporaryNodes; i++) {
				var nodeIndex = ctx.pathHandler.temporaryNodeStartIndex + i;
				ref var node = ref ctx.pathHandler.GetTemporaryNode(nodeIndex);
				if (node.type == TemporaryNodeType.End && node.targetIndex == targetIndex) {
					node.type = TemporaryNodeType.Ignore;
				}
			}

			// When we find the first target, we can stop because it will be the closest one.
			if (!pathsForAll) {
				CompleteState = PathCompleteState.Complete;
				targetNodeCount = 0;
				return;
			}


			// If there are no more targets to find, return here and avoid calculating a new hTarget
			if (targetNodeCount <= 0) {
				CompleteState = PathCompleteState.Complete;
				return;
			}

			RecalculateHTarget(ref ctx);
		}

		protected override void Trace (ref SearchContext ctx, uint pathNodeIndex) {
			base.Trace(ref ctx, pathNodeIndex, !inverted);
		}

		protected override void DebugString (System.Text.StringBuilder text, PathLog logMode) {
			if (logMode == PathLog.None || (!error && logMode == PathLog.OnlyErrors)) {
				return;
			}

			DebugStringPrefix(logMode, text);

			if (!error) {
				text.Append("\nShortest path was ");
				text.Append(chosenTarget == -1 ? "undefined" : nodePaths[chosenTarget].Count.ToString());
				text.Append(" nodes long");

				if (logMode == PathLog.Heavy) {
					text.Append("\nPaths (").Append(targetsFound.Length).Append("):");
					for (int i = 0; i < targetsFound.Length; i++) {
						text.Append("\n\n	Path ").Append(i).Append(" Found: ").Append(targetsFound[i]);

						if (nodePaths[i] != null) {
							text.Append("\n		Length: ");
							text.Append(nodePaths[i].Count);
						}
					}

					text.Append("\nStart Node");
					text.Append("\n	Point: ");
					text.Append(((Vector3)endPoint).ToString());
					text.Append("\n	Graph: ");
					text.Append(startNode.GraphIndex);
				}
			}

			DebugStringSuffix(logMode, text);
		}
	}
}
