using UnityEngine;

namespace Pathfinding {
	using Pathfinding.Util;
	using Pathfinding.Drawing;
	using Pathfinding.Pooling;

	/// <summary>
	/// Interface for handling off-mesh links.
	///
	/// If a component implements this interface, and is attached to the same GameObject as a NodeLink2 component,
	/// then the OnTraverseOffMeshLink method will be called when an agent traverses the off-mesh link.
	///
	/// This only works with the <see cref="FollowerEntity"/> component.
	///
	/// Note: <see cref="FollowerEntity.onTraverseOffMeshLink"/> overrides this callback, if set.
	///
	/// The <see cref="Interactable"/> component implements this interface, and allows a small state-machine to run when the agent traverses the link.
	///
	/// See: <see cref="FollowerEntity.onTraverseOffMeshLink"/>
	/// See: offmeshlinks (view in online documentation for working links)
	/// </summary>
	public interface IOffMeshLinkHandler {
		/// <summary>
		/// Name of the handler.
		/// This is used to identify the handler in the inspector.
		/// </summary>
		public string name => null;

#if MODULE_ENTITIES
		/// <summary>
		/// Called when an agent starts traversing an off-mesh link.
		///
		/// This can be used to perform any setup that is necessary for the traversal.
		///
		/// Returns: An object which will be used to control the agent for the full duration of the link traversal.
		///
		/// For simple cases, you can often implement <see cref="IOffMeshLinkHandler"/> and <see cref="IOffMeshLinkStateMachine"/> in the same class, and then
		/// just make this method return this.
		/// For more complex cases, you may want to keep track of the agent's identity, and thus want to return a new object for each agent that traverses the link.
		/// </summary>
		/// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
		/// This context is only valid for this method call. Do not store it and use it later.</param>
		public IOffMeshLinkStateMachine GetOffMeshLinkStateMachine(ECS.AgentOffMeshLinkTraversalContext context);
#endif
	}

	public interface IOffMeshLinkStateMachine {
#if MODULE_ENTITIES
		/// <summary>
		/// Called when an agent traverses an off-mesh link.
		/// This method should be a coroutine (i.e return an IEnumerable) which will be iterated over until it finishes, or the agent is destroyed.
		/// The coroutine should yield null every frame until the agent has finished traversing the link.
		///
		/// When the coroutine completes, the agent will be assumed to have reached the end of the link and control
		/// will be returned to the normal movement code.
		///
		/// The coroutine typically moves the agent to the end of the link over some time, and perform any other actions that are necessary.
		/// For example, it could play an animation, or move the agent in a specific way.
		/// </summary>
		/// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
		/// This context is only valid when this coroutine steps forward. Do not store it and use it elsewhere.</param>
		System.Collections.IEnumerable OnTraverseOffMeshLink(ECS.AgentOffMeshLinkTraversalContext context) => ECS.JobStartOffMeshLinkTransition.DefaultOnTraverseOffMeshLink(context);

		/// <summary>
		/// Called when an agent finishes traversing an off-mesh link.
		///
		/// This can be used to perform any cleanup that is necessary after the traversal.
		///
		/// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both.
		/// </summary>
		/// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
		/// This context is only valid for this method call. Do not store it and use it later.</param>
		void OnFinishTraversingOffMeshLink (ECS.AgentOffMeshLinkTraversalContext context) {}
#endif

		/// <summary>
		/// Called when an agent fails to finish traversing an off-mesh link.
		///
		/// This can be used to perform any cleanup that is necessary after the traversal.
		///
		/// An abort can happen if the agent was destroyed while it was traversing the link. It can also happen if the agent was teleported somewhere else while traversing the link.
		///
		/// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both.
		///
		/// Warning: When this is called, the agent may already be destroyed. The handler component itself could also be destroyed at this point.
		/// </summary>
		void OnAbortTraversingOffMeshLink () {}
	}

	/// <summary>
	/// Connects two nodes using an off-mesh link.
	/// In contrast to the <see cref="NodeLink"/> component, this link type will not connect the nodes directly
	/// instead it will create two link nodes at the start and end position of this link and connect
	/// through those nodes.
	///
	/// If the closest node to this object is called A and the closest node to the end transform is called
	/// D, then it will create one link node at this object's position (call it B) and one link node at
	/// the position of the end transform (call it C), it will then connect A to B, B to C and C to D.
	///
	/// This link type is possible to detect while following since it has these special link nodes in the middle.
	/// The link corresponding to one of those intermediate nodes can be retrieved using the <see cref="GetNodeLink"/> method
	/// which can be of great use if you want to, for example, play a link specific animation when reaching the link.
	///
	/// \inspectorField{NodeLink2.end; End}
	/// \inspectorField{NodeLink2.costFactor; Cost Factor}
	/// \inspectorField{NodeLink2.oneWay; One Way}
	/// \inspectorField{NodeLink2.pathfindingTag; Pathfinding Tag}
	/// \inspectorField{NodeLink2.graphMask; Graph Mask}
	///
	/// See: offmeshlinks (view in online documentation for working links)
	/// See: The example scene RecastExample2 contains a few links which you can take a look at to see how they are used.
	///
	/// Note: If you make any modifications to the node link's settings after it has been created, you need to call the <see cref="Apply"/> method in order to apply the changes to the graph.
	/// </summary>
	[AddComponentMenu("Pathfinding/Link2")]
	[HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink2.html")]
	public class NodeLink2 : GraphModifier {
		/// <summary>End position of the link</summary>
		public Transform end;

		/// <summary>
		/// The connection will be this times harder/slower to traverse.
		///
		/// A cost factor of 1 means that the link is equally expensive as moving the same distance on the normal navmesh. But a cost factor greater than 1 means that it is proportionally more expensive.
		///
		/// You should not use a cost factor less than 1 unless you also change the <see cref="AstarPath.heuristicScale"/> field (A* Inspector -> Settings -> Pathfinding) to at most the minimum cost factor that you use anywhere in the scene (or disable the heuristic altogether).
		/// This is because the pathfinding algorithm assumes that paths are at least as costly as walking just the straight line distance to the target, and if you use a cost factor less than 1, that assumption is no longer true.
		/// What then happens is that the pathfinding search may ignore some links because it doesn't even think to search in that direction, even if they would have lead to a lower path cost.
		///
		/// Warning: Reducing the heuristic scale or disabling the heuristic can significantly increase the cpu cost for pathfinding, especially for large graphs.
		///
		/// Read more about this at https://en.wikipedia.org/wiki/Admissible_heuristic.
		/// </summary>
		public float costFactor = 1.0f;

		/// <summary>Make a one-way connection</summary>
		public bool oneWay = false;

		/// <summary>
		/// The tag to apply to the link.
		///
		/// This can be used to exclude certain agents from using the link, or make it more expensive to use.
		///
		/// See: tags (view in online documentation for working links)
		/// </summary>
		public PathfindingTag pathfindingTag = 0;

		/// <summary>
		/// Which graphs this link is allowed to connect.
		///
		/// The link will always connect the nodes closest to the start and end points on the graphs that it is allowed to connect.
		/// </summary>
		public GraphMask graphMask = GraphMask.everything;

		public Transform StartTransform => transform;

		public Transform EndTransform => end;

		protected OffMeshLinks.OffMeshLinkSource linkSource;

		/// <summary>
		/// Returns the link component associated with the specified node.
		/// Returns: The link associated with the node or null if the node is not a link node, or it is not associated with a <see cref="NodeLink2"/> component.
		/// </summary>
		/// <param name="node">The node to get the link for.</param>
		public static NodeLink2 GetNodeLink(GraphNode node) => node is LinkNode linkNode ? linkNode.linkSource.component as NodeLink2 : null;

		/// <summary>
		/// True if the link is connected to the graph.
		///
		/// This will be true if the link has been successfully connected to the graph, and false if it either has failed, or if the component/gameobject is disabled.
		///
		/// When the component is enabled, the link will be scheduled to be added to the graph, it will not take effect immediately.
		/// This means that this property will return false until the next time graph updates are processed (usually later this frame, or next frame).
		/// To ensure the link is refreshed immediately, you can call <see cref="AstarPath.active.FlushWorkItems"/>.
		/// </summary>
		internal bool isActive => linkSource != null && (linkSource.status & OffMeshLinks.OffMeshLinkStatus.Active) != 0;

		IOffMeshLinkHandler onTraverseOffMeshLinkHandler;

		/// <summary>
		/// Callback to be called when an agent starts traversing an off-mesh link.
		///
		/// The handler will be called when the agent starts traversing an off-mesh link.
		/// It allows you to to control the agent for the full duration of the link traversal.
		///
		/// Use the passed context struct to get information about the link and to control the agent.
		///
		/// <code>
		/// using UnityEngine;
		/// using Pathfinding;
		/// using System.Collections;
		/// using Pathfinding.ECS;
		///
		/// namespace Pathfinding.Examples {
		///     public class FollowerJumpLink : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine {
		///         // Register this class as the handler for off-mesh links when the component is enabled
		///         void OnEnable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = this;
		///         void OnDisable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = null;
		///
		///         IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this;
		///
		///         void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink (AgentOffMeshLinkTraversalContext context) {
		///             Debug.Log("An agent finished traversing an off-mesh link");
		///         }
		///
		///         void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink () {
		///             Debug.Log("An agent aborted traversing an off-mesh link");
		///         }
		///
		///         IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext ctx) {
		///             var start = (Vector3)ctx.link.relativeStart;
		///             var end = (Vector3)ctx.link.relativeEnd;
		///             var dir = end - start;
		///
		///             // Disable local avoidance while traversing the off-mesh link.
		///             // If it was enabled, it will be automatically re-enabled when the agent finishes traversing the link.
		///             ctx.DisableLocalAvoidance();
		///
		///             // Move and rotate the agent to face the other side of the link.
		///             // When reaching the off-mesh link, the agent may be facing the wrong direction.
		///             while (!ctx.MoveTowards(
		///                 position: start,
		///                 rotation: Quaternion.LookRotation(dir, ctx.movementPlane.up),
		///                 gravity: true,
		///                 slowdown: true).reached) {
		///                 yield return null;
		///             }
		///
		///             var bezierP0 = start;
		///             var bezierP1 = start + Vector3.up*5;
		///             var bezierP2 = end + Vector3.up*5;
		///             var bezierP3 = end;
		///             var jumpDuration = 1.0f;
		///
		///             // Animate the AI to jump from the start to the end of the link
		///             for (float t = 0; t < jumpDuration; t += ctx.deltaTime) {
		///                 ctx.transform.Position = AstarSplines.CubicBezier(bezierP0, bezierP1, bezierP2, bezierP3, Mathf.SmoothStep(0, 1, t / jumpDuration));
		///                 yield return null;
		///             }
		///         }
		///     }
		/// }
		/// </code>
		///
		/// Warning: Off-mesh links can be destroyed or disabled at any moment. The built-in code will attempt to make the agent continue following the link even if it is destroyed,
		/// but if you write your own traversal code, you should be aware of this.
		///
		/// You can alternatively set the corresponding property property on the agent (<see cref="FollowerEntity.onTraverseOffMeshLink"/>) to specify a callback for a all off-mesh links.
		///
		/// Note: The agent's off-mesh link handler takes precedence over the link's off-mesh link handler, if both are set.
		///
		/// Warning: This property only works with the <see cref="FollowerEntity"/> component. Use <see cref="RichAI.onTraverseOffMeshLink"/> if you are using the <see cref="RichAI"/> movement script.
		///
		/// See: offmeshlinks (view in online documentation for working links) for more details and example code
		/// </summary>
		public IOffMeshLinkHandler onTraverseOffMeshLink {
			get => onTraverseOffMeshLinkHandler;
			set {
				onTraverseOffMeshLinkHandler = value;
				if (linkSource != null) linkSource.handler = value;
			}
		}

		public override void OnPostScan () {
			TryAddLink();
		}

		protected override void OnEnable () {
			base.OnEnable();
			if (Application.isPlaying && !BatchedEvents.Has(this)) BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdate);
			TryAddLink();
		}

		static void OnUpdate (NodeLink2[] components, int count) {
			// Only check for moved links every N frames, for performance
			if ((Time.frameCount % 16) != 0) return;

			for (int i = 0; i < count; i++) {
				var comp = components[i];
				var start = comp.StartTransform;
				var end = comp.EndTransform;
				var added = comp.linkSource != null;
				if ((start != null && end != null) != added || (added && (start.hasChanged || end.hasChanged))) {
					if (start != null) start.hasChanged = false;
					if (end != null) end.hasChanged = false;
					comp.RemoveLink();
					comp.TryAddLink();
				}
			}
		}

		void TryAddLink () {
			// In case the AstarPath component has been destroyed (destroying the link).
			// But do not clear it if the link is inactive because it failed to become enabled
			if (linkSource != null && (linkSource.status == OffMeshLinks.OffMeshLinkStatus.Inactive || (linkSource.status & OffMeshLinks.OffMeshLinkStatus.PendingRemoval) != 0)) linkSource = null;

			if (linkSource == null && AstarPath.active != null && EndTransform != null) {
				StartTransform.hasChanged = false;
				EndTransform.hasChanged = false;

				linkSource = new OffMeshLinks.OffMeshLinkSource {
					start = new OffMeshLinks.Anchor {
						center = StartTransform.position,
						rotation = StartTransform.rotation,
						width = 0f,
					},
					end = new OffMeshLinks.Anchor {
						center = EndTransform.position,
						rotation = EndTransform.rotation,
						width = 0f,
					},
					directionality = oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay,
					tag = pathfindingTag,
					costFactor = costFactor,
					graphMask = graphMask,
					maxSnappingDistance = 1, // TODO
					component = this,
					handler = onTraverseOffMeshLink,
				};
				AstarPath.active.offMeshLinks.Add(linkSource);
			}
		}

		void RemoveLink () {
			if (AstarPath.active != null && linkSource != null) AstarPath.active.offMeshLinks.Remove(linkSource);
			linkSource = null;
		}

		protected override void OnDisable () {
			base.OnDisable();
			BatchedEvents.Remove(this);
			RemoveLink();
		}

		[ContextMenu("Recalculate neighbours")]
		void ContextApplyForce () {
			Apply();
		}

		/// <summary>
		/// Disconnects and then reconnects the link to the graph.
		///
		/// If you have moved the link or otherwise modified it you need to call this method to apply those changes.
		/// </summary>
		public virtual void Apply () {
			RemoveLink();
			TryAddLink();
		}

		private readonly static Color GizmosColor = new Color(206.0f/255.0f, 136.0f/255.0f, 48.0f/255.0f, 0.5f);
		private readonly static Color GizmosColorSelected = new Color(235.0f/255.0f, 123.0f/255.0f, 32.0f/255.0f, 1.0f);

		public override void DrawGizmos () {
			if (StartTransform == null || EndTransform == null) return;

			var startPos = StartTransform.position;
			var endPos = EndTransform.position;
			if (linkSource != null && (Time.renderedFrameCount % 16) == 0 && Application.isEditor) {
				// Check if the link has moved
				// During runtime, this will be done by the OnUpdate method instead
				if (linkSource.start.center != startPos || linkSource.end.center != endPos || linkSource.directionality != (oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay) || linkSource.costFactor != costFactor || linkSource.graphMask != graphMask || linkSource.tag != pathfindingTag) {
					Apply();
				}
			}

			bool selected = GizmoContext.InActiveSelection(this);
			var graphs = linkSource != null && AstarPath.active != null? AstarPath.active.offMeshLinks.ConnectedGraphs(linkSource) : null;
			var up = Vector3.up;

			// Find the natural up direction of the connected graphs, so that we can orient the gizmos appropriately
			if (graphs != null) {
				for (int i = 0; i < graphs.Count; i++) {
					var graph = graphs[i];
					if (graph != null) {
						if (graph is NavmeshBase navmesh) {
							up = navmesh.transform.WorldUpAtGraphPosition(Vector3.zero);
							break;
						} else if (graph is GridGraph grid) {
							up = grid.transform.WorldUpAtGraphPosition(Vector3.zero);
							break;
						}
					}
				}
				ListPool<NavGraph>.Release(ref graphs);
			}

			var active = linkSource != null && linkSource.status == OffMeshLinks.OffMeshLinkStatus.Active;
			Color color = selected ? GizmosColorSelected : GizmosColor;
			if (active) color = Color.green;

			Draw.Circle(startPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectStart) ? Color.red : color);
			Draw.Circle(endPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectEnd) ? Color.red : color);

			NodeLink.DrawArch(startPos, endPos, up, color);
			if (selected) {
				Vector3 cross = Vector3.Cross(up, endPos-startPos).normalized;
				using (Draw.WithLineWidth(2)) {
					NodeLink.DrawArch(startPos+cross*0.0f, endPos+cross*0.0f, up, color);
				}
				// NodeLink.DrawArch(startPos-cross*0.1f, endPos-cross*0.1f, color);
			}
		}
	}
}
