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

namespace Pathfinding {
	/// <summary>
	/// Calculates paths from everywhere to a single point.
	/// This path is a bit special, because it does not do anything useful by itself. What it does is that it calculates paths to all nodes it can reach, it floods the graph.
	/// This data will remain stored in the path. Then you can calculate a <see cref="FloodPathTracer"/> path. That path will trace the path from its starting point all the way to where this path started.
	/// A FloodPathTracer search is extremely fast to calculate compared to a normal path request.
	///
	/// It is very useful in for example tower defence games, where all your AIs will walk to the same point but from different places, and you do not update the graph or change the target point very often.
	///
	/// Usage:
	/// - At start, you calculate ONE FloodPath and save the reference (it will be needed later).
	/// - Then when a unit is spawned or needs its path recalculated, start a FloodPathTracer path from the unit's position.
	/// It will then find the shortest path to the point specified when you calculated the FloodPath extremely quickly.
	/// - If you update the graph (for example place a tower in a TD game) or need to change the target point, you calculate a new FloodPath and make all AIs calculate new FloodPathTracer paths.
	///
	/// Note: Since a FloodPathTracer path only uses precalculated information, it will always use the same penalties/tags as the FloodPath it references.
	/// If you want to use different penalties/tags, you will have to calculate a new FloodPath.
	///
	/// Here follows some example code of the above list of steps:
	/// <code>
	/// public static FloodPath fpath;
	///
	/// public void Start () {
	///     fpath = FloodPath.Construct (someTargetPosition, null);
	///     AstarPath.StartPath (fpath);
	/// }
	/// </code>
	///
	/// When searching for a new path to someTargetPosition from let's say transform.position, you do
	/// <code>
	/// FloodPathTracer fpathTrace = FloodPathTracer.Construct (transform.position,fpath,null);
	/// seeker.StartPath (fpathTrace,OnPathComplete);
	/// </code>
	/// Where OnPathComplete is your callback function.
	///
	/// \section flood-path-builtin-movement Integration with the built-in movement scripts
	/// The built-in movement scripts cannot calculate a FloodPathTracer path themselves, but you can use the SetPath method to assign such a path to them:
	/// <code>
	/// var ai = GetComponent<IAstarAI>();
	/// // Disable the agent's own path recalculation code
	/// ai.canSearch = false;
	/// ai.SetPath(FloodPathTracer.Construct(ai.position, floodPath));
	/// </code>
	///
	/// [Open online documentation to see images]
	/// </summary>
	public class FloodPath : Path {
		public Vector3 originalStartPoint;
		public Vector3 startPoint;
		public GraphNode startNode;

		/// <summary>
		/// If false, will not save any information.
		/// Used by some internal parts of the system which doesn't need it.
		/// </summary>
		public bool saveParents = true;

		protected Dictionary<uint, uint> parents;
		uint validationHash;

		public bool HasPathTo (GraphNode node) {
			if (parents != null) {
				for (uint k = 0; k < node.PathNodeVariants; k++) {
					if (parents.ContainsKey(node.NodeIndex + k)) return true;
				}
			}
			return false;
		}

		/// <summary>
		/// Checks if the flood path data is still valid.
		///
		/// This check is quite strict, not allowing any nodes to have been destroyed since the path was calculated.
		/// The reason for this strictness is that if a node has been destroyed, and then a node has been created, they
		/// may end up sharing the same node index. This could cause the path generated by a FloodPathTracer to be
		/// completely messed up if it would have passed through the destroyed node.
		/// </summary>
		internal bool IsValid (GlobalNodeStorage nodeStorage) {
			return nodeStorage.destroyedNodesVersion == validationHash;
		}

		public uint GetParent (uint node) {
			return parents.TryGetValue(node, out var v) ? v : 0;
		}

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

		public static FloodPath Construct (Vector3 start, OnPathDelegate callback = null) {
			var p = PathPool.GetPath<FloodPath>();

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

		public static FloodPath Construct (GraphNode start, OnPathDelegate callback = null) {
			if (start == null) throw new ArgumentNullException("start");

			var p = PathPool.GetPath<FloodPath>();
			p.Setup(start, callback);
			return p;
		}

		protected void Setup (Vector3 start, OnPathDelegate callback) {
			this.callback = callback;
			originalStartPoint = start;
			startPoint = start;
			heuristic = Heuristic.None;
		}

		protected void Setup (GraphNode start, OnPathDelegate callback) {
			this.callback = callback;
			originalStartPoint = (Vector3)start.position;
			startNode = start;
			startPoint = (Vector3)start.position;
			heuristic = Heuristic.None;
		}

		protected override void Reset () {
			base.Reset();
			originalStartPoint = Vector3.zero;
			startPoint = Vector3.zero;
			startNode = null;
			/// <summary>TODO: Avoid this allocation</summary>
			parents = new Dictionary<uint, uint>();
			saveParents = true;
			validationHash = 0;
		}

		protected override void Prepare (ref SearchContext ctx) {
			if (startNode == null) {
				var startNNInfo = AstarPath.active.GetNearest(originalStartPoint, nearestNodeConstraint);

				startPoint = startNNInfo.position;
				startNode = startNNInfo.node;
			} else if (startNode.Destroyed) {
				FailWithError("Start node has been destroyed");
				return;
			} else {
				startPoint = (Vector3)startNode.position;
			}

#if ASTARDEBUG
			Debug.DrawLine((Vector3)startNode.position, startPoint, Color.blue);
#endif

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

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

			ctx.pathHandler.AddTemporaryNode(new TemporaryNode {
				type = TemporaryNodeType.Start,
				position = (Int3)startPoint,
				associatedNode = startNode.NodeIndex,
			});
			ctx.heuristicObjective = new HeuristicObjective(int3.zero, Heuristic.None, 0.0f);
			ctx.traversalConstraint = traversalConstraint;
			ctx.traversalCosts = traversalCosts;
			ctx.AddStartNodesToHeap();
			validationHash = ctx.pathHandler.nodeStorage.destroyedNodesVersion;
		}

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

		protected override void OnFoundEndNode (ref SearchContext ctx, uint pathNode, uint hScore, uint gScore) {
			throw new System.InvalidOperationException("FloodPaths do not have any end nodes");
		}

		public const uint TemporaryNodeBit = 1u << 31;

		public override void OnVisitNode (ref SearchContext ctx, uint pathNode, uint hScore, uint gScore) {
			// Insert into internal search tree
			if (saveParents) {
				var parentIndex = ctx.pathHandler.pathNodes[pathNode].parentIndex;
				parents[pathNode] = parentIndex | (ctx.pathHandler.IsTemporaryNode(parentIndex) ? TemporaryNodeBit : 0); // != 0 ? (pathHandler.IsTemporaryNode(parentIndex) ? pathHandler.GetTemporaryNode(parentIndex).associatedNode : parentIndex) : 0;
			}
		}
	}
}
