using UnityEngine;
using System.Collections.Generic;
using Pathfinding.Util;
using Pathfinding.Serialization;
using UnityEngine.Profiling;

namespace Pathfinding {
	/// <summary>
	/// Helper for navmesh cut objects.
	/// Responsible for keeping track of which navmesh cuts have moved and coordinating graph updates to account for those changes.
	///
	/// See: navmeshcutting (view in online documentation for working links)
	/// See: <see cref="AstarPath.navmeshUpdates"/>
	/// See: <see cref="Pathfinding.NavmeshBase.enableNavmeshCutting"/>
	/// </summary>
	[System.Serializable]
	public class NavmeshUpdates {
		/// <summary>
		/// How often to check if an update needs to be done (real seconds between checks).
		/// For worlds with a very large number of NavmeshCut objects, it might be bad for performance to do this check every frame.
		/// If you think this is a performance penalty, increase this number to check less often.
		///
		/// For almost all games, this can be kept at 0.
		///
		/// If negative, no updates will be done. They must be manually triggered using <see cref="ForceUpdate"/>.
		///
		/// <code>
		/// // Check every frame (the default)
		/// AstarPath.active.navmeshUpdates.updateInterval = 0;
		///
		/// // Check every 0.1 seconds
		/// AstarPath.active.navmeshUpdates.updateInterval = 0.1f;
		///
		/// // Never check for changes
		/// AstarPath.active.navmeshUpdates.updateInterval = -1;
		/// // You will have to schedule updates manually using
		/// AstarPath.active.navmeshUpdates.ForceUpdate();
		/// </code>
		///
		/// You can also find this in the AstarPath inspector under Settings.
		/// [Open online documentation to see images]
		/// </summary>
		public float updateInterval;

		/// <summary>Last time navmesh cuts were applied</summary>
		float lastUpdateTime = float.NegativeInfinity;

		/// <summary>Stores navmesh cutting related data for a single graph</summary>
		internal class NavmeshUpdateSettings {
			public TileHandler handler;
			public readonly List<IntRect> forcedReloadRects = new List<IntRect>();
			readonly NavmeshBase graph;

			public NavmeshUpdateSettings(NavmeshBase graph) {
				this.graph = graph;
			}

			public void Refresh (bool forceCreate = false) {
				if (!graph.enableNavmeshCutting) {
					if (handler != null) {
						handler.cuts.Clear();
						handler.ReloadInBounds(new IntRect(int.MinValue, int.MinValue, int.MaxValue, int.MaxValue));
						// Make sure the updates are applied immediately.
						// This is important because if navmesh cutting is enabled immediately after this
						// then it will call CreateTileTypesFromGraph, and we need to ensure that it is not
						// calling that when the graph still has cuts in it as they will then be baked in.
						AstarPath.active.FlushGraphUpdates();
						AstarPath.active.FlushWorkItems();

						forcedReloadRects.ClearFast();
						handler = null;
					}
				} else if ((handler == null && (forceCreate || NavmeshClipper.allEnabled.Count > 0)) || (handler != null && !handler.isValid)) {
					// Note: Only create a handler if there are any navmesh cuts in the scene.
					// We don't want to waste a lot of memory if navmesh cutting isn't actually used for anything
					// and even more important: we don't want to do any sporadic updates to the graph which
					// may clear the graph's tags or change it's structure (e.g from the delaunay optimization in the TileHandler).

					// The tile handler is invalid (or doesn't exist), so re-create it
					handler = new TileHandler(graph);
					for (int i = 0; i < NavmeshClipper.allEnabled.Count; i++) AddClipper(NavmeshClipper.allEnabled[i]);
					handler.CreateTileTypesFromGraph();

					// Reload in huge bounds. This will cause all tiles to be updated.
					forcedReloadRects.Add(new IntRect(int.MinValue, int.MinValue, int.MaxValue, int.MaxValue));
				}
			}

			/// <summary>Called when some tiles in a recast graph have been completely recalculated (e.g from scanning the graph)</summary>
			public void OnRecalculatedTiles (NavmeshTile[] tiles) {
				Refresh();
				if (handler != null) handler.OnRecalculatedTiles(tiles);
			}

			/// <summary>Called when a NavmeshCut or NavmeshAdd is enabled</summary>
			public void AddClipper (NavmeshClipper obj) {
				if (!obj.graphMask.Contains((int)graph.graphIndex)) return;

				// Without the forceCreate parameter set to true then no handler will be created
				// because there are no clippers in the scene yet. However one is being added right now.
				Refresh(true);
				if (handler == null) return;
				var graphSpaceBounds = obj.GetBounds(handler.graph.transform);
				var touchingTiles = handler.graph.GetTouchingTilesInGraphSpace(graphSpaceBounds);
				handler.cuts.Add(obj, touchingTiles);
			}

			/// <summary>Called when a NavmeshCut or NavmeshAdd is disabled</summary>
			public void RemoveClipper (NavmeshClipper obj) {
				Refresh();
				if (handler == null) return;
				var root = handler.cuts.GetRoot(obj);

				if (root != null) {
					forcedReloadRects.Add(root.previousBounds);
					handler.cuts.Remove(obj);
				}
			}
		}

		internal void OnEnable () {
			NavmeshClipper.AddEnableCallback(HandleOnEnableCallback, HandleOnDisableCallback);
		}

		internal void OnDisable () {
			NavmeshClipper.RemoveEnableCallback(HandleOnEnableCallback, HandleOnDisableCallback);
		}

		/// <summary>Discards all pending updates caused by moved or modified navmesh cuts</summary>
		public void DiscardPending () {
			for (int i = 0; i < NavmeshClipper.allEnabled.Count; i++) {
				NavmeshClipper.allEnabled[i].NotifyUpdated();
			}

			var graphs = AstarPath.active.graphs;
			for (int i = 0; i < graphs.Length; i++) {
				var navmeshBase = graphs[i] as NavmeshBase;
				if (navmeshBase != null) navmeshBase.navmeshUpdateData.forcedReloadRects.Clear();
			}
		}

		/// <summary>Called when a NavmeshCut or NavmeshAdd is enabled</summary>
		void HandleOnEnableCallback (NavmeshClipper obj) {
			var graphs = AstarPath.active.graphs;

			for (int i = 0; i < graphs.Length; i++) {
				var navmeshBase = graphs[i] as NavmeshBase;
				if (navmeshBase != null) navmeshBase.navmeshUpdateData.AddClipper(obj);
			}
			obj.ForceUpdate();
		}

		/// <summary>Called when a NavmeshCut or NavmeshAdd is disabled</summary>
		void HandleOnDisableCallback (NavmeshClipper obj) {
			var graphs = AstarPath.active.graphs;

			for (int i = 0; i < graphs.Length; i++) {
				var navmeshBase = graphs[i] as NavmeshBase;
				if (navmeshBase != null) navmeshBase.navmeshUpdateData.RemoveClipper(obj);
			}
			lastUpdateTime = float.NegativeInfinity;
		}

		/// <summary>Update is called once per frame</summary>
		internal void Update () {
			if (AstarPath.active.isScanning) return;
			Profiler.BeginSample("Navmesh cutting");
			bool anyInvalidHandlers = false;
			var graphs = AstarPath.active.graphs;
			for (int i = 0; i < graphs.Length; i++) {
				var navmeshBase = graphs[i] as NavmeshBase;
				if (navmeshBase != null) {
					navmeshBase.navmeshUpdateData.Refresh();
					anyInvalidHandlers = navmeshBase.navmeshUpdateData.forcedReloadRects.Count > 0;
				}
			}

			if ((updateInterval >= 0 && Time.realtimeSinceStartup - lastUpdateTime > updateInterval) || anyInvalidHandlers) {
				ForceUpdate();
			}
			Profiler.EndSample();
		}

		/// <summary>
		/// Checks all NavmeshCut instances and updates graphs if needed.
		/// Note: This schedules updates for all necessary tiles to happen as soon as possible.
		/// The pathfinding threads will continue to calculate the paths that they were calculating when this function
		/// was called and then they will be paused and the graph updates will be carried out (this may be several frames into the
		/// future and the graph updates themselves may take several frames to complete).
		/// If you want to force all navmesh cutting to be completed in a single frame call this method
		/// and immediately after call AstarPath.FlushWorkItems.
		///
		/// <code>
		/// // Schedule pending updates to be done as soon as the pathfinding threads
		/// // are done with what they are currently doing.
		/// AstarPath.active.navmeshUpdates.ForceUpdate();
		/// // Block until the updates have finished
		/// AstarPath.active.FlushGraphUpdates();
		/// </code>
		/// </summary>
		public void ForceUpdate () {
			lastUpdateTime = Time.realtimeSinceStartup;

			List<NavmeshClipper> hasBeenUpdated = null;

			var graphs = AstarPath.active.graphs;
			for (int graphIndex = 0; graphIndex < graphs.Length; graphIndex++) {
				var navmeshBase = graphs[graphIndex] as NavmeshBase;
				if (navmeshBase == null) continue;

				// Done in Update as well, but users may call ForceUpdate directly
				navmeshBase.navmeshUpdateData.Refresh();

				var handler = navmeshBase.navmeshUpdateData.handler;

				if (handler == null) continue;

				var forcedReloadRects = navmeshBase.navmeshUpdateData.forcedReloadRects;

				// Get all navmesh cuts in the scene
				var allCuts = handler.cuts.AllItems;

				if (forcedReloadRects.Count == 0) {
					bool any = false;

					// Check if any navmesh cuts need updating
					for (var cut = allCuts; cut != null; cut = cut.next) {
						if (cut.obj.RequiresUpdate()) {
							any = true;
							break;
						}
					}

					// Nothing needs to be done for now
					if (!any) continue;
				}

				// Start batching tile updates which is good for performance
				// if we are updating a lot of them
				handler.StartBatchLoad();

				for (int i = 0; i < forcedReloadRects.Count; i++) {
					handler.ReloadInBounds(forcedReloadRects[i]);
				}
				forcedReloadRects.ClearFast();

				if (hasBeenUpdated == null) hasBeenUpdated = ListPool<NavmeshClipper>.Claim();

				// Reload all bounds touching the previous bounds and current bounds
				// of navmesh cuts that have moved or changed in some other way
				for (var cut = allCuts; cut != null; cut = cut.next) {
					if (cut.obj.RequiresUpdate()) {
						// Make sure the tile where it was is updated
						handler.ReloadInBounds(cut.previousBounds);

						var newGraphSpaceBounds = cut.obj.GetBounds(handler.graph.transform);
						var newTouchingTiles = handler.graph.GetTouchingTilesInGraphSpace(newGraphSpaceBounds);
						handler.cuts.Move(cut.obj, newTouchingTiles);
						handler.ReloadInBounds(newTouchingTiles);

						hasBeenUpdated.Add(cut.obj);
					}
				}

				handler.EndBatchLoad();
			}

			if (hasBeenUpdated != null) {
				// Notify navmesh cuts that they have been updated
				// This will cause RequiresUpdate to return false
				// until it is changed again.
				// Note: This is not as efficient as it could be when multiple graphs are used
				// because every navmesh cut will be added to the list once for every graph.
				for (int i = 0; i < hasBeenUpdated.Count; i++) {
					hasBeenUpdated[i].NotifyUpdated();
				}

				ListPool<NavmeshClipper>.Release(ref hasBeenUpdated);
			}
		}
	}
}