using UnityEngine;
using System.Collections.Generic;

namespace Pathfinding {
	using UnityEngine.Profiling;
	using Pathfinding.Util;
	using Pathfinding.Serialization;
	using Unity.Collections;
	using Unity.Jobs;
	using Pathfinding.Graphs.Navmesh.Jobs;
	using Pathfinding.Graphs.Navmesh;
	using Unity.Mathematics;

	/// <summary>
	/// Generates graphs based on navmeshes.
	/// [Open online documentation to see images]
	///
	/// Navmeshes are meshes in which each triangle defines a walkable area.
	/// These are great because the AI can get so much more information on how it can walk.
	/// Polygons instead of points mean that the <see cref="FunnelModifier"/> can produce really nice looking paths, and the graphs are also really fast to search
	/// and have a low memory footprint because fewer nodes are usually needed to describe the same area compared to grid graphs.
	///
	/// The navmesh graph requires that you create a navmesh manually. The package also has support for generating navmeshes automatically using the <see cref="RecastGraph"/>.
	///
	/// For a tutorial on how to configure a navmesh graph, take a look at getstarted2 (view in online documentation for working links).
	///
	/// [Open online documentation to see images]
	///
	/// \section navmeshgraph-inspector Inspector
	/// [Open online documentation to see images]
	///
	/// \inspectorField{sourceMesh; Source Mesh}
	/// \inspectorField{offset; Offset}
	/// \inspectorField{rotation; Rotation}
	/// \inspectorField{scale; Scale}
	/// \inspectorField{recalculateNormals; Recalculate Normals}
	/// \inspectorField{enableNavmeshCutting; Affected By Navmesh Cuts}
	/// \inspectorField{navmeshCuttingCharacterRadius; Agent Radius}
	/// \inspectorField{initialPenalty; Initial Penalty}
	///
	/// See: <see cref="RecastGraph"/>
	/// </summary>
	[JsonOptIn]
	[Pathfinding.Util.Preserve]
	public class NavMeshGraph : NavmeshBase, IUpdatableGraph {
		/// <summary>Mesh to construct navmesh from</summary>
		[JsonMember]
		public Mesh sourceMesh;

		/// <summary>Offset in world space</summary>
		[JsonMember]
		public Vector3 offset;

		/// <summary>Rotation in degrees</summary>
		[JsonMember]
		public Vector3 rotation;

		/// <summary>Scale of the graph</summary>
		[JsonMember]
		public float scale = 1;

		/// <summary>
		/// Determines how normals are calculated.
		/// Disable for spherical graphs or other complicated surfaces that allow the agents to e.g walk on walls or ceilings.
		///
		/// By default the normals of the mesh will be flipped so that they point as much as possible in the upwards direction.
		/// The normals are important when connecting adjacent nodes. Two adjacent nodes will only be connected if they are oriented the same way.
		/// This is particularly important if you have a navmesh on the walls or even on the ceiling of a room. Or if you are trying to make a spherical navmesh.
		/// If you do one of those things then you should set disable this setting and make sure the normals in your source mesh are properly set.
		///
		/// If you for example take a look at the image below. In the upper case then the nodes on the bottom half of the
		/// mesh haven't been connected with the nodes on the upper half because the normals on the lower half will have been
		/// modified to point inwards (as that is the direction that makes them face upwards the most) while the normals on
		/// the upper half point outwards. This causes the nodes to not connect properly along the seam. When this option
		/// is set to false instead the nodes are connected properly as in the original mesh all normals point outwards.
		/// [Open online documentation to see images]
		///
		/// The default value of this field is true to reduce the risk for errors in the common case. If a mesh is supplied that
		/// has all normals pointing downwards and this option is false, then some methods like <see cref="PointOnNavmesh"/> will not work correctly
		/// as they assume that the normals point upwards. For a more complicated surface like a spherical graph those methods make no sense anyway
		/// as there is no clear definition of what it means to be "inside" a triangle when there is no clear up direction.
		/// </summary>
		[JsonMember]
		public bool recalculateNormals = true;

		/// <summary>
		/// Cached bounding box minimum of <see cref="sourceMesh"/>.
		/// This is important when the graph has been saved to a file and is later loaded again, but the original mesh does not exist anymore (or has been moved).
		/// In that case we still need to be able to find the bounding box since the <see cref="CalculateTransform"/> method uses it.
		/// </summary>
		[JsonMember]
		Vector3 cachedSourceMeshBoundsMin;

		/// <summary>
		/// Radius to use when expanding navmesh cuts.
		///
		/// See: <see cref="NavmeshCut.radiusExpansionMode"/>
		/// </summary>
		[JsonMember]
		public float navmeshCuttingCharacterRadius = 0.5f;

		public override float NavmeshCuttingCharacterRadius => navmeshCuttingCharacterRadius;

		public override bool RecalculateNormals => recalculateNormals;

		public override float TileWorldSizeX => forcedBoundsSize.x;

		public override float TileWorldSizeZ => forcedBoundsSize.z;

		// Tiles are not supported, so this is irrelevant
		public override float MaxTileConnectionEdgeDistance => 0f;

		/// <summary>
		/// True if the point is inside the bounding box of this graph.
		///
		/// Warning: If your input mesh is entirely flat, the bounding box will also end up entirely flat (with a height of zero), this will make this function return false for almost all points, unless they are at exactly the right y-coordinate.
		///
		/// Note: For an unscanned graph, this will always return false.
		/// </summary>
		public override bool IsInsideBounds (Vector3 point) {
			if (this.tiles == null || this.tiles.Length == 0 || sourceMesh == null) return false;

			var local = transform.InverseTransform(point);
			var size = sourceMesh.bounds.size*scale;

			// Allow a small margin
			const float EPS = 0.0001f;

			return local.x >= -EPS && local.y >= -EPS && local.z >= -EPS && local.x <= size.x + EPS && local.y <= size.y + EPS && local.z <= size.z + EPS;
		}

		/// <summary>
		/// World bounding box for the graph.
		///
		/// This always contains the whole graph.
		///
		/// Note: Since this is an axis-aligned bounding box, it may not be particularly tight if the graph is significantly rotated.
		///
		/// If no mesh has been assigned, this will return a zero sized bounding box at the origin.
		///
		/// [Open online documentation to see images]
		/// </summary>
		public override Bounds bounds {
			get {
				if (sourceMesh == null) return default;
				var m = (float4x4)CalculateTransform().matrix;
				var b = new ToWorldMatrix(new float3x3(m.c0.xyz, m.c1.xyz, m.c2.xyz)).ToWorld(new Bounds(Vector3.zero, sourceMesh.bounds.size * scale));
				return b;
			}
		}

		public override GraphTransform CalculateTransform () {
			return new GraphTransform(Matrix4x4.TRS(offset, Quaternion.Euler(rotation), Vector3.one) * Matrix4x4.TRS(sourceMesh != null ? sourceMesh.bounds.min * scale : cachedSourceMeshBoundsMin * scale, Quaternion.identity, Vector3.one));
		}

		class NavMeshGraphUpdatePromise : IGraphUpdatePromise {
			public NavMeshGraph graph;
			public List<GraphUpdateObject> graphUpdates;

			public void Apply (IGraphUpdateContext ctx) {
				for (int i = 0; i < graphUpdates.Count; i++) {
					var graphUpdate = graphUpdates[i];
					UpdateArea(graphUpdate, graph);
					// TODO: Not strictly accurate, since the update may affect node that have a surface that extends
					// outside of the bounds.
					ctx.DirtyBounds(graphUpdate.bounds);
				}
			}
		}

		IGraphUpdatePromise IUpdatableGraph.ScheduleGraphUpdates (List<GraphUpdateObject> graphUpdates) => new NavMeshGraphUpdatePromise { graph = this, graphUpdates = graphUpdates };

		public static void UpdateArea (GraphUpdateObject o, INavmeshHolder graph) {
			Bounds bounds = graph.transform.InverseTransform(o.bounds);

			// Bounding rectangle with integer coordinates
			var irect = new IntRect(
				Mathf.FloorToInt(bounds.min.x*Int3.Precision),
				Mathf.FloorToInt(bounds.min.z*Int3.Precision),
				Mathf.CeilToInt(bounds.max.x*Int3.Precision),
				Mathf.CeilToInt(bounds.max.z*Int3.Precision)
				);

			// Corners of the bounding rectangle
			var a = new Int3(irect.xmin, 0, irect.ymin);
			var b = new Int3(irect.xmin, 0, irect.ymax);
			var c = new Int3(irect.xmax, 0, irect.ymin);
			var d = new Int3(irect.xmax, 0, irect.ymax);

			var ymin = ((Int3)bounds.min).y;
			var ymax = ((Int3)bounds.max).y;

			// Loop through all nodes and check if they intersect the bounding box
			graph.GetNodes(_node => {
				var node = _node as TriangleMeshNode;

				bool inside = false;

				int allLeft = 0;
				int allRight = 0;
				int allTop = 0;
				int allBottom = 0;

				// Check bounding box rect in XZ plane
				for (int v = 0; v < 3; v++) {
					Int3 p = node.GetVertexInGraphSpace(v);

					if (irect.Contains(p.x, p.z)) {
						inside = true;
						break;
					}

					if (p.x < irect.xmin) allLeft++;
					if (p.x > irect.xmax) allRight++;
					if (p.z < irect.ymin) allTop++;
					if (p.z > irect.ymax) allBottom++;
				}

				if (!inside && (allLeft == 3 || allRight == 3 || allTop == 3 || allBottom == 3)) {
					return;
				}

				// Check if the polygon edges intersect the bounding rect
				for (int v = 0; v < 3; v++) {
					int v2 = v > 1 ? 0 : v+1;

					Int3 vert1 = node.GetVertexInGraphSpace(v);
					Int3 vert2 = node.GetVertexInGraphSpace(v2);

					if (VectorMath.SegmentsIntersectXZ(a, b, vert1, vert2)) { inside = true; break; }
					if (VectorMath.SegmentsIntersectXZ(a, c, vert1, vert2)) { inside = true; break; }
					if (VectorMath.SegmentsIntersectXZ(c, d, vert1, vert2)) { inside = true; break; }
					if (VectorMath.SegmentsIntersectXZ(d, b, vert1, vert2)) { inside = true; break; }
				}

				// Check if the node contains any corner of the bounding rect
				if (inside || node.ContainsPointInGraphSpace(a) || node.ContainsPointInGraphSpace(b) || node.ContainsPointInGraphSpace(c) || node.ContainsPointInGraphSpace(d)) {
					inside = true;
				}

				if (!inside) {
					return;
				}

				int allAbove = 0;
				int allBelow = 0;

				// Check y coordinate
				for (int v = 0; v < 3; v++) {
					Int3 p = node.GetVertexInGraphSpace(v);
					if (p.y < ymin) allBelow++;
					if (p.y > ymax) allAbove++;
				}

				// Polygon is either completely above the bounding box or completely below it
				if (allBelow == 3 || allAbove == 3) return;

				// Triangle is inside the bounding box!
				// Update it!
				o.WillUpdateNode(node);
				o.Apply(node);
			});
		}

		class NavMeshGraphScanPromise : IGraphUpdatePromise {
			public NavMeshGraph graph;
			bool emptyGraph;
			GraphTransform transform;
			NavmeshTile[] tiles;
			Vector3 forcedBoundsSize;
			IntRect tileRect;
			NavmeshUpdates.NavmeshUpdateSettings cutSettings;

			public IEnumerator<JobHandle> Prepare () {
				var sourceMesh = graph.sourceMesh;
				graph.cachedSourceMeshBoundsMin = sourceMesh != null ? sourceMesh.bounds.min : Vector3.zero;
				transform = graph.CalculateTransform();

				if (sourceMesh == null) {
					emptyGraph = true;
					yield break;
				}

				if (!sourceMesh.isReadable) {
					Debug.LogError("The source mesh " + sourceMesh.name + " is not readable. Enable Read/Write in the mesh's import settings.", sourceMesh);
					emptyGraph = true;
					yield break;
				}

				Profiler.BeginSample("GetMeshData");
				var meshDatas = Mesh.AcquireReadOnlyMeshData(sourceMesh);
				MeshUtility.GetMeshData(meshDatas, 0, out var vertices, out var indices);
				meshDatas.Dispose();
				Profiler.EndSample();

				// Convert the vertices to graph space
				// so that the minimum of the bounding box of the mesh is at the origin
				// (the vertices will later be transformed to world space)
				var scale = graph.scale;
				var meshToGraphSpace = Matrix4x4.TRS(-sourceMesh.bounds.min * scale, Quaternion.identity, Vector3.one * scale);

				var promise = JobBuildTileMeshFromVertices.Schedule(vertices, indices, meshToGraphSpace, graph.RecalculateNormals);
				forcedBoundsSize = sourceMesh.bounds.size * scale;
				tileRect = new IntRect(0, 0, 0, 0);
				tiles = new NavmeshTile[tileRect.Area];
				var tilesGCHandle = System.Runtime.InteropServices.GCHandle.Alloc(tiles);
				var tileLayout = new TileLayout(new Bounds(transform.Transform(forcedBoundsSize*0.5f), forcedBoundsSize), Quaternion.Euler(graph.rotation), 0.001f, 0, false);
				cutSettings = new NavmeshUpdates.NavmeshUpdateSettings(graph, tileLayout);

				var cutPromise = RecastBuilder.CutTiles(graph, cutSettings.clipperLookup, tileLayout).Schedule(promise);
				var tileNodeConnections = new NativeArray<JobCalculateTriangleConnections.TileNodeConnectionsUnsafe>(tiles.Length, Allocator.Persistent);
				var postCutInput = cutPromise.GetValue();
				var preCutInput = promise.GetValue();

				NativeArray<TileMesh.TileMeshUnsafe> finalTileMeshes;
				if (postCutInput.tileMeshes.tileMeshes.IsCreated) {
					UnityEngine.Assertions.Assert.AreEqual(postCutInput.tileMeshes.tileMeshes.Length, tileRect.Area);
					finalTileMeshes = postCutInput.tileMeshes.tileMeshes;
				} else {
					finalTileMeshes = preCutInput.tileMeshes.tileMeshes;
				}

				var calculateConnectionsJob = new JobCalculateTriangleConnections {
					tileMeshes = finalTileMeshes,
					nodeConnections = tileNodeConnections,
				}.Schedule(cutPromise.handle);

				var createTilesJob = new JobCreateTiles {
					// If any cutting is done, we need to save the pre-cut data to be able to re-cut tiles later
					preCutTileMeshes = postCutInput.tileMeshes.tileMeshes.IsCreated ? preCutInput.tileMeshes.tileMeshes : default,
					tileMeshes = finalTileMeshes,
					tiles = tilesGCHandle,
					tileRect = tileRect,
					graphTileCount = new Vector2Int(tileRect.Width, tileRect.Height),
					graphIndex = graph.graphIndex,
					initialPenalty = graph.initialPenalty,
					recalculateNormals = graph.recalculateNormals,
					graphToWorldSpace = transform.matrix,
					tileWorldSize = tileLayout.TileWorldSize,
				}.Schedule(cutPromise.handle);
				var applyConnectionsJob = new JobWriteNodeConnections {
					tiles = tilesGCHandle,
					nodeConnections = tileNodeConnections,
				}.Schedule(JobHandle.CombineDependencies(createTilesJob, calculateConnectionsJob));

				yield return applyConnectionsJob;

				// This has already been used in the createTilesJob
				promise.Complete().Dispose();
				cutPromise.Complete().Dispose();
				tileNodeConnections.Dispose();

				vertices.Dispose();
				indices.Dispose();

				tilesGCHandle.Free();
			}

			public void Apply (IGraphUpdateContext ctx) {
				// Destroy all previous nodes (if any)
				graph.DestroyAllNodes();

				if (emptyGraph) {
					graph.forcedBoundsSize = Vector3.zero;
					graph.transform = transform;
					graph.tileZCount = graph.tileXCount = 1;
					TriangleMeshNode.SetNavmeshHolder(AstarPath.active.data.GetGraphIndex(graph), graph);
					graph.FillWithEmptyTiles();
					graph.navmeshUpdateData.Dispose();
					return;
				}

				// Initialize all nodes that were created in the jobs
				for (int j = 0; j < tiles.Length; j++) AstarPath.active.InitializeNodes(tiles[j].nodes);

				// Assign all data as one atomic operation (from the viewpoint of the main thread)
				graph.forcedBoundsSize = forcedBoundsSize;
				graph.transform = transform;
				graph.tileXCount = tileRect.Width;
				graph.tileZCount = tileRect.Height;
				graph.tiles = tiles;
				TriangleMeshNode.SetNavmeshHolder(graph.active.data.GetGraphIndex(graph), graph);
				cutSettings.AttachToGraph();

				if (graph.OnRecalculatedTiles != null) graph.OnRecalculatedTiles(tiles.Clone() as NavmeshTile[]);
			}
		}

		protected override IGraphUpdatePromise ScanInternal (bool async) => new NavMeshGraphScanPromise { graph = this };

		protected override void PostDeserialization (GraphSerializationContext ctx) {
			if (ctx.meta.version < AstarSerializer.V4_3_74) {
				this.navmeshCuttingCharacterRadius = 0;
			}
			base.PostDeserialization(ctx);
		}
	}
}
