using UnityEngine;
using Pathfinding.RVO.Sampled;

namespace Pathfinding.RVO {
	/// <summary>
	/// Quadtree for quick nearest neighbour search of rvo agents.
	/// See: Pathfinding.RVO.Simulator
	/// </summary>
	public class RVOQuadtree {
		const int LeafSize = 15;

		float maxRadius = 0;

		/// <summary>
		/// Node in a quadtree for storing RVO agents.
		/// See: Pathfinding.GraphNode for the node class that is used for pathfinding data.
		/// </summary>
		struct Node {
			public int child00;
			public Agent linkedList;
			public byte count;

			/// <summary>Maximum speed of all agents inside this node</summary>
			public float maxSpeed;

			public void Add (Agent agent) {
				agent.next = linkedList;
				linkedList = agent;
			}

			/// <summary>
			/// Distribute the agents in this node among the children.
			/// Used after subdividing the node.
			/// </summary>
			public void Distribute (Node[] nodes, Rect r) {
				Vector2 c = r.center;

				while (linkedList != null) {
					Agent nx = linkedList.next;
					var index = child00 + (linkedList.position.x > c.x ? 2 : 0) + (linkedList.position.y > c.y ? 1 : 0);
					nodes[index].Add(linkedList);
					linkedList = nx;
				}
				count = 0;
			}

			public float CalculateMaxSpeed (Node[] nodes, int index) {
				if (child00 == index) {
					// Leaf node
					for (var agent = linkedList; agent != null; agent = agent.next) {
						maxSpeed = System.Math.Max(maxSpeed, agent.CalculatedSpeed);
					}
				} else {
					maxSpeed = System.Math.Max(nodes[child00].CalculateMaxSpeed(nodes, child00), nodes[child00+1].CalculateMaxSpeed(nodes, child00+1));
					maxSpeed = System.Math.Max(maxSpeed, nodes[child00+2].CalculateMaxSpeed(nodes, child00+2));
					maxSpeed = System.Math.Max(maxSpeed, nodes[child00+3].CalculateMaxSpeed(nodes, child00+3));
				}
				return maxSpeed;
			}
		}

		Node[] nodes = new Node[16];
		int filledNodes = 1;

		Rect bounds;

		/// <summary>Removes all agents from the tree</summary>
		public void Clear () {
			nodes[0] = new Node();
			filledNodes = 1;
			maxRadius = 0;
		}

		public void SetBounds (Rect r) {
			bounds = r;
		}

		int GetNodeIndex () {
			if (filledNodes + 4 >= nodes.Length) {
				var nds = new Node[nodes.Length*2];
				for (int i = 0; i < nodes.Length; i++) nds[i] = nodes[i];
				nodes = nds;
			}
			nodes[filledNodes] = new Node();
			nodes[filledNodes].child00 = filledNodes;
			filledNodes++;
			nodes[filledNodes] = new Node();
			nodes[filledNodes].child00 = filledNodes;
			filledNodes++;
			nodes[filledNodes] = new Node();
			nodes[filledNodes].child00 = filledNodes;
			filledNodes++;
			nodes[filledNodes] = new Node();
			nodes[filledNodes].child00 = filledNodes;
			filledNodes++;
			return filledNodes-4;
		}

		/// <summary>
		/// Add a new agent to the tree.
		/// Warning: Agents must not be added multiple times to the same tree
		/// </summary>
		public void Insert (Agent agent) {
			int i = 0;
			Rect r = bounds;
			Vector2 p = new Vector2(agent.position.x, agent.position.y);

			agent.next = null;

			maxRadius = System.Math.Max(agent.radius, maxRadius);

			int depth = 0;

			while (true) {
				depth++;

				if (nodes[i].child00 == i) {
					// Leaf node. Break at depth 10 in case lots of agents ( > LeafSize ) are in the same spot
					if (nodes[i].count < LeafSize || depth > 10) {
						nodes[i].Add(agent);
						nodes[i].count++;
						break;
					} else {
						// Split
						nodes[i].child00 = GetNodeIndex();
						nodes[i].Distribute(nodes, r);
					}
				}
				// Note, no else
				if (nodes[i].child00 != i) {
					// Not a leaf node
					Vector2 c = r.center;
					if (p.x > c.x) {
						if (p.y > c.y) {
							i = nodes[i].child00+3;
							r = Rect.MinMaxRect(c.x, c.y, r.xMax, r.yMax);
						} else {
							i = nodes[i].child00+2;
							r = Rect.MinMaxRect(c.x, r.yMin, r.xMax, c.y);
						}
					} else {
						if (p.y > c.y) {
							i = nodes[i].child00+1;
							r = Rect.MinMaxRect(r.xMin, c.y, c.x, r.yMax);
						} else {
							i = nodes[i].child00;
							r = Rect.MinMaxRect(r.xMin, r.yMin, c.x, c.y);
						}
					}
				}
			}
		}

		public void CalculateSpeeds () {
			nodes[0].CalculateMaxSpeed(nodes, 0);
		}

		public void Query (Vector2 p, float speed, float timeHorizon, float agentRadius, Agent agent) {
			new QuadtreeQuery {
				p = p, speed = speed, timeHorizon = timeHorizon, maxRadius = float.PositiveInfinity,
				agentRadius = agentRadius, agent = agent, nodes = nodes
			}.QueryRec(0, bounds);
		}

		struct QuadtreeQuery {
			public Vector2 p;
			public float speed, timeHorizon, agentRadius, maxRadius;
			public Agent agent;
			public Node[] nodes;

			public void QueryRec (int i, Rect r) {
				// Determine the radius that we need to search to take all agents into account
				// Note: the second agentRadius usage should actually be the radius of the other agents, not this agent
				// but for performance reasons and for simplicity we assume that agents have approximately the same radius.
				// Thus an agent with a very small radius may in some cases detect an agent with a very large radius too late
				// however this effect should be minor.
				var radius = System.Math.Min(System.Math.Max((nodes[i].maxSpeed + speed)*timeHorizon, agentRadius) + agentRadius, maxRadius);

				if (nodes[i].child00 == i) {
					// Leaf node
					for (Agent a = nodes[i].linkedList; a != null; a = a.next) {
						float v = agent.InsertAgentNeighbour(a, radius*radius);
						// Limit the search if the agent has hit the max number of nearby agents threshold
						if (v < maxRadius*maxRadius) {
							maxRadius = Mathf.Sqrt(v);
						}
					}
				} else {
					// Not a leaf node
					Vector2 c = r.center;
					if (p.x-radius < c.x) {
						if (p.y-radius < c.y) {
							QueryRec(nodes[i].child00, Rect.MinMaxRect(r.xMin, r.yMin, c.x, c.y));
							radius = System.Math.Min(radius, maxRadius);
						}
						if (p.y+radius > c.y) {
							QueryRec(nodes[i].child00+1, Rect.MinMaxRect(r.xMin, c.y, c.x, r.yMax));
							radius = System.Math.Min(radius, maxRadius);
						}
					}

					if (p.x+radius > c.x) {
						if (p.y-radius < c.y) {
							QueryRec(nodes[i].child00+2, Rect.MinMaxRect(c.x, r.yMin, r.xMax, c.y));
							radius = System.Math.Min(radius, maxRadius);
						}
						if (p.y+radius > c.y) {
							QueryRec(nodes[i].child00+3, Rect.MinMaxRect(c.x, c.y, r.xMax, r.yMax));
						}
					}
				}
			}
		}

		public void DebugDraw () {
			DebugDrawRec(0, bounds);
		}

		void DebugDrawRec (int i, Rect r) {
			Debug.DrawLine(new Vector3(r.xMin, 0, r.yMin), new Vector3(r.xMax, 0, r.yMin), Color.white);
			Debug.DrawLine(new Vector3(r.xMax, 0, r.yMin), new Vector3(r.xMax, 0, r.yMax), Color.white);
			Debug.DrawLine(new Vector3(r.xMax, 0, r.yMax), new Vector3(r.xMin, 0, r.yMax), Color.white);
			Debug.DrawLine(new Vector3(r.xMin, 0, r.yMax), new Vector3(r.xMin, 0, r.yMin), Color.white);

			if (nodes[i].child00 != i) {
				// Not a leaf node
				Vector2 c = r.center;
				DebugDrawRec(nodes[i].child00+3, Rect.MinMaxRect(c.x, c.y, r.xMax, r.yMax));
				DebugDrawRec(nodes[i].child00+2, Rect.MinMaxRect(c.x, r.yMin, r.xMax, c.y));
				DebugDrawRec(nodes[i].child00+1, Rect.MinMaxRect(r.xMin, c.y, c.x, r.yMax));
				DebugDrawRec(nodes[i].child00+0, Rect.MinMaxRect(r.xMin, r.yMin, c.x, c.y));
			}

			for (Agent a = nodes[i].linkedList; a != null; a = a.next) {
				var p = nodes[i].linkedList.position;
				Debug.DrawLine(new Vector3(p.x, 0, p.y)+Vector3.up, new Vector3(a.position.x, 0, a.position.y)+Vector3.up, new Color(1, 1, 0, 0.5f));
			}
		}
	}
}