#region Copyright & License Information
/*
 * Copyright (c) The OpenRA Developers and Contributors
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Pathfinder
{
	/// <summary>
	/// Provides pathfinding abilities for actors that use a specific <see cref="Locomotor"/>.
	/// Maintains a hierarchy of abstract graphs that provide a more accurate heuristic function during
	/// A* pathfinding than the one available from <see cref="PathSearch.DefaultCostEstimator(Locomotor)"/>.
	/// This allows for faster pathfinding.
	/// </summary>
	/// <remarks>
	/// <para>The goal of this pathfinder is to increase performance of path searches. <see cref="PathSearch"/> is used
	/// to perform a path search as usual, but a different heuristic function is provided that is more accurate. This
	/// means fewer nodes have to be explored during the search, resulting in a performance increase.</para>
	///
	/// <para>When an A* path search is performed, the search expands outwards from the source location until the
	/// target is found. The heuristic controls how this expansion occurs. When the heuristic of h(n) = 0 is given, we
	/// get Dijkstra's algorithm. The search grows outwards from the source node in an expanding circle with no sense
	/// of direction. This will find the shortest path by brute force. It will explore many nodes during the search,
	/// including lots of nodes in the opposite direction to the target.</para>
	///
	/// <para><see cref="PathSearch.DefaultCostEstimator(Locomotor)"/> provides heuristic for searching a 2D grid. It
	/// estimates the cost as the straight-line distance between the source and target nodes. The search grows in a
	/// straight line towards the target node. This is a vast improvement over Dijkstra's algorithm as we now
	/// prioritize exploring nodes that lie closer to the target, rather than exploring nodes that take us away from
	/// the target.</para>
	///
	/// <para>This default straight-line heuristic still has drawbacks - it is unaware of the obstacles on the grid. If
	/// the route to be found requires steering around obstacles then this heuristic can perform badly. Imagine a path
	/// that must steer around a lake, or move back on itself to get out of a dead end. In these cases the straight-line
	/// heuristic moves blindly towards the target, when actually the path requires that we move sidewards or even
	/// backwards to find a route. When this occurs then the straight-line heuristic ends up exploring nodes that
	/// aren't useful - they lead us into dead ends or directly into an obstacle that we need to go around instead.
	/// </para>
	///
	/// <para>The <see cref="HierarchicalPathFinder"/> improves the heuristic by making it aware of unreachable map
	/// terrain. A "low-resolution" version of the map is maintained, and used to provide an initial route. When the
	/// search is conducted it explores along this initial route. This allows the search to "know" it needs to go
	/// sideways around the lake or backwards out of the dead-end, meaning we can explore even fewer nodes.</para>
	///
	/// <para>The "low-resolution" version of the map is referred to as the abstract graph. The abstract graph is
	/// created by dividing the map up into a series of grids, of say 10x10 nodes. Within each grid, we determine the
	/// connected regions of nodes within that grid. If all the nodes within the grid connect to each other, we have
	/// one such region. If they are split up by impassable terrain then we may have two or more regions within the
	/// grid. Every region will be represented by one node in the abstract graph (an abstract node, for short).</para>
	///
	/// <para>When a path search is to be performed, we first perform a A* search on the abstract graph with the
	/// <see cref="PathSearch.DefaultCostEstimator(Locomotor)"/>. This graph is much smaller than the full map, so
	/// this search is quick. The resulting path gives us the initial route between each abstract node. We can then use
	/// this to create the improved heuristic for use on the path search on the full resolution map. When determining
	/// the cost for the node, we can use the straight-line distance towards the next abstract node as our estimate.
	/// Our search is therefore guided along the initial route.</para>
	///
	/// <para>This implementation only maintains one level of abstract graph, but a hierarchy of such graphs is
	/// possible. This allows the top-level and lowest resolution graph to be as small as possible - important because
	/// it will be searched using the dumbest heuristic. Each level underneath is higher-resolution and contains more
	/// nodes, but uses a heuristic informed from the previous level to guide the search in the right direction.</para>
	///
	/// <para>This implementation is aware of movement costs over terrain given by
	/// <see cref="Locomotor.MovementCostToEnterCell(Actor, CPos, CPos, BlockedByActor, Actor, bool)"/>. It is aware of
	/// changes to the costs in terrain and able to update the abstract graph when this occurs. It is able to search
	/// the abstract graph as if <see cref="BlockedByActor.None"/> had been specified. If
	/// <see cref="BlockedByActor.Immovable"/> is given in the constructor, the abstract graph will additionally
	/// account for a subset of immovable actors using the same rules as
	/// <see cref="Locomotor.CanMoveFreelyInto(Actor, CPos, SubCell, BlockedByActor, Actor, bool)"/>. It will be aware
	/// of changes to actors on the map and update the abstract graph when this occurs. Other types of blocking actors
	/// will not be accounted for in the heuristic.</para>
	///
	/// <para>If the obstacle on the map is from terrain (e.g. a cliff or lake) the heuristic will work well. If the
	/// obstacle is from the subset of immovable actors (e.g. trees, walls, buildings) and
	/// <see cref="BlockedByActor.Immovable"/> was given, the heuristic will work well. If the obstacle is from other
	/// actors (e.g. units) then the heuristic is unaware of these. Therefore the same problem where the search goes in
	/// the wrong direction is possible, e.g. through a choke-point that has units blocking it. In this scenario the
	/// performance benefit will be lost, as the search will have to explore more nodes until it can get around the
	/// obstacle.</para>
	///
	/// <para>In summary, the <see cref="HierarchicalPathFinder"/> reduces the performance impact of path searches that
	/// must go around terrain, and some types of actor, but does not improve performance of searches that must go
	/// around the remaining types of actor.</para>
	/// </remarks>
	public sealed class HierarchicalPathFinder
	{
		// This value determined via empiric testing as the best performance trade-off.
		const int GridSize = 10;

		readonly World world;
		readonly Locomotor locomotor;
		readonly IActorMap actorMap;
		readonly Func<CPos, CPos, int> costEstimator;
		readonly HashSet<int> dirtyGridIndexes = new();
		readonly HashSet<CPos> cellsWithBlockingActor;
		Grid mapBounds;
		int gridXs;
		int gridYs;

		/// <summary>
		/// Index by a <see cref="GridIndex"/>.
		/// </summary>
		GridInfo[] gridInfos;

		/// <summary>
		/// The abstract graph is represented here.
		/// An abstract node is the key, and costs to other abstract nodes are then available.
		/// Abstract nodes with no connections are NOT present in the graph.
		/// A lookup will fail, rather than return an empty list.
		/// </summary>
		Dictionary<CPos, List<GraphConnection>> abstractGraph;

		/// <summary>
		/// The abstract domains are represented here.
		/// An abstract node is the key, and a domain index is given.
		/// If the domain index of two nodes is equal, a path exists between them (ignoring all blocking actors).
		/// If unequal, no path is possible.
		/// </summary>
		readonly Dictionary<CPos, uint> abstractDomains;

		/// <summary>
		/// Knows about the abstract nodes within a grid. Can map a local cell to its abstract node.
		/// </summary>
		readonly struct GridInfo
		{
			readonly CPos?[] singleAbstractCellForLayer;
			readonly Dictionary<CPos, CPos> localCellToAbstractCell;

			public GridInfo(CPos?[] singleAbstractCellForLayer, Dictionary<CPos, CPos> localCellToAbstractCell)
			{
				this.singleAbstractCellForLayer = singleAbstractCellForLayer;
				this.localCellToAbstractCell = localCellToAbstractCell;
			}

			/// <summary>
			/// Maps a local cell to a abstract node in the graph.
			/// Returns null when the local cell is unreachable.
			/// Pass a null <paramref name="hpf"/> to skip cost checks if the caller already checked.
			/// </summary>
			public CPos? AbstractCellForLocalCell(CPos localCell, HierarchicalPathFinder hpf)
			{
				var abstractCell = singleAbstractCellForLayer[localCell.Layer];
				if (abstractCell != null)
				{
					// All reachable cells in the grid are joined together so only a single abstract cell was needed,
					// but there may be unreachable cells in the grid which we must exclude.
					if (hpf != null && !hpf.CellIsAccessible(localCell))
						return null;
					return abstractCell;
				}

				// Only reachable cells would be populated in the lookup, so no need to check their cost.
				if (localCellToAbstractCell.TryGetValue(localCell, out var abstractCellFromMap))
					return abstractCellFromMap;
				return null;
			}

			public void CopyAbstractCellsInto(HashSet<CPos> set)
			{
				foreach (var single in singleAbstractCellForLayer)
					if (single != null)
						set.Add(single.Value);
				foreach (var cell in localCellToAbstractCell.Values)
					set.Add(cell);
			}
		}

		/// <summary>
		/// Represents an abstract graph with some extra edges inserted.
		/// Instead of building a new dictionary with the edges added, we build a supplemental dictionary of changes.
		/// This is to avoid copying the entire abstract graph.
		/// </summary>
		sealed class AbstractGraphWithInsertedEdges
		{
			readonly Dictionary<CPos, List<GraphConnection>> abstractEdges;
			readonly Dictionary<CPos, IEnumerable<GraphConnection>> changedEdges;

			public AbstractGraphWithInsertedEdges(
				Dictionary<CPos, List<GraphConnection>> abstractEdges,
				IList<GraphEdge> sourceEdges,
				GraphEdge? targetEdge,
				Func<CPos, CPos, int> costEstimator)
			{
				this.abstractEdges = abstractEdges;
				changedEdges = new Dictionary<CPos, IEnumerable<GraphConnection>>(sourceEdges.Count * 9 + (targetEdge != null ? 9 : 0));
				foreach (var sourceEdge in sourceEdges)
					InsertEdgeAsBidirectional(sourceEdge, costEstimator);
				if (targetEdge != null)
					InsertEdgeAsBidirectional(targetEdge.Value, costEstimator);
			}

			void InsertEdgeAsBidirectional(GraphEdge edge, Func<CPos, CPos, int> costEstimator)
			{
				InsertConnections(edge.Source, edge.Destination, costEstimator);
			}

			void InsertConnections(CPos localCell, CPos abstractCell, Func<CPos, CPos, int> costEstimator)
			{
				if (!abstractEdges.TryGetValue(abstractCell, out var edges))
					edges = new List<GraphConnection>();
				changedEdges[localCell] = edges
					.Select(e => new GraphConnection(e.Destination, costEstimator(localCell, e.Destination)))
					.Append(new GraphConnection(abstractCell, costEstimator(localCell, abstractCell)));

				IEnumerable<GraphConnection> abstractChangedEdges = edges;
				if (changedEdges.TryGetValue(abstractCell, out var existingEdges))
					abstractChangedEdges = existingEdges;
				changedEdges[abstractCell] = abstractChangedEdges
					.Append(new GraphConnection(localCell, costEstimator(abstractCell, localCell)));

				foreach (var conn in edges)
				{
					IEnumerable<GraphConnection> connChangedEdges;
					if (changedEdges.TryGetValue(conn.Destination, out var existingConnEdges))
						connChangedEdges = existingConnEdges;
					else
						connChangedEdges = abstractEdges[conn.Destination];

					changedEdges[conn.Destination] = connChangedEdges
						.Append(new GraphConnection(localCell, costEstimator(conn.Destination, localCell)));
				}
			}

			public List<GraphConnection> GetConnections(CPos position)
			{
				if (changedEdges.TryGetValue(position, out var changedEdge))
					return changedEdge.ToList();
				if (abstractEdges.TryGetValue(position, out var abstractEdge))
					return abstractEdge;
				return new List<GraphConnection>();
			}
		}

		public HierarchicalPathFinder(World world, Locomotor locomotor, IActorMap actorMap, BlockedByActor check)
		{
			this.world = world;
			this.locomotor = locomotor;
			this.actorMap = actorMap;
			if (locomotor.Info.TerrainSpeeds.Count == 0)
				return;

			if (check == BlockedByActor.Immovable)
			{
				// When we account for immovable actors, it depends on the actors on the map.
				// When this changes, we must update the cost table.
				actorMap.CellUpdated += RequireBlockingRefreshInCell;

				// Determine immovable cells from actors already on the map.
				cellsWithBlockingActor = actorMap.AllActors()
					.Where(ActorIsBlocking)
					.SelectMany(a =>
						a.OccupiesSpace.OccupiedCells()
						.Select(oc => oc.Cell)
						.Where(c => ActorCellIsBlocking(a, c)))
					.ToHashSet();
			}
			else if (check != BlockedByActor.None)
				throw new System.ComponentModel.InvalidEnumArgumentException(
					$"{nameof(HierarchicalPathFinder)} supports {nameof(BlockedByActor.None)} " +
					$"and {nameof(BlockedByActor.Immovable)} only for {nameof(check)}");

			costEstimator = PathSearch.DefaultCostEstimator(locomotor);

			BuildGrids();
			BuildCostTable();
			abstractDomains = new Dictionary<CPos, uint>(gridXs * gridYs);
			RebuildDomains();

			// When we build the cost table, it depends on the movement costs of the cells at that time.
			// When this changes, we must update the cost table.
			locomotor.CellCostChanged += RequireCostRefreshInCell;

			// If the map projection changes, the result of Map.Contains(CPos) may change.
			// We need to rebuild grids to account for this possibility.
			this.world.Map.CellProjectionChanged += RequireProjectionRefreshInCell;
		}

		public (
			IReadOnlyDictionary<CPos, List<GraphConnection>> AbstractGraph,
			IReadOnlyDictionary<CPos, uint> AbstractDomains) GetOverlayData()
		{
			if (costEstimator == null)
				return default;

			// Ensure the abstract graph and domains are up to date when using the overlay.
			RebuildDirtyGrids();
			RebuildDomains();
			return (
				new ReadOnlyDictionary<CPos, List<GraphConnection>>(abstractGraph),
				new ReadOnlyDictionary<CPos, uint>(abstractDomains));
		}

		/// <summary>
		/// Divides the map area up into a series of grids.
		/// </summary>
		void BuildGrids()
		{
			static Grid GetCPosBounds(Map map)
			{
				if (map.Grid.Type == MapGridType.RectangularIsometric)
				{
					var bottomRight = map.AllCells.BottomRight;
					var bottomRightU = bottomRight.ToMPos(map).U;
					return new Grid(
						new CPos(0, -bottomRightU),
						new CPos(bottomRight.X + 1, bottomRight.Y + bottomRightU + 1),
						false);
				}

				return new Grid(CPos.Zero, (CPos)map.MapSize, false);
			}

			mapBounds = GetCPosBounds(world.Map);
			gridXs = Exts.IntegerDivisionRoundingAwayFromZero(mapBounds.Width, GridSize);
			gridYs = Exts.IntegerDivisionRoundingAwayFromZero(mapBounds.Height, GridSize);

			var customMovementLayers = world.GetCustomMovementLayers();
			gridInfos = new GridInfo[gridXs * gridYs];
			for (var gridX = mapBounds.TopLeft.X; gridX < mapBounds.BottomRight.X; gridX += GridSize)
				for (var gridY = mapBounds.TopLeft.Y; gridY < mapBounds.BottomRight.Y; gridY += GridSize)
					gridInfos[GridIndex(new CPos(gridX, gridY))] = BuildGrid(gridX, gridY, customMovementLayers);
		}

		/// <summary>
		/// Determines the abstract nodes within a single grid. One abstract node will be created for each set of cells
		/// that are reachable from each other within the grid area. A grid with open terrain will commonly have one
		/// abstract node. If impassable terrain such as cliffs or water divides the cells into 2 or more distinct
		/// regions, one abstract node is created for each region. We also remember which cells belong to which
		/// abstract node. Given a local cell, this allows us to determine which abstract node it belongs to.
		/// </summary>
		GridInfo BuildGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLayers)
		{
			var singleAbstractCellForLayer = new CPos?[customMovementLayers.Length];
			var localCellToAbstractCell = new Dictionary<CPos, CPos>();

			// When accounting for immovable actors, use a custom cost so those cells become invalid paths.
			var customCost = cellsWithBlockingActor == null
				? (Func<CPos, int>)null
				: c => cellsWithBlockingActor.Contains(c) ? PathGraph.PathCostForInvalidPath : 0;

			var accessibleCells = new HashSet<CPos>(GridSize * GridSize);
			for (byte gridLayer = 0; gridLayer < customMovementLayers.Length; gridLayer++)
			{
				if (gridLayer != 0 &&
					(customMovementLayers[gridLayer] == null ||
					!customMovementLayers[gridLayer].EnabledForLocomotor(locomotor.Info)))
					continue;

				var grid = GetGrid(new CPos(gridX, gridY, gridLayer), mapBounds);
				for (var y = gridY; y < grid.BottomRight.Y; y++)
				{
					for (var x = gridX; x < grid.BottomRight.X; x++)
					{
						var cell = new CPos(x, y, gridLayer);
						if (CellIsAccessible(cell))
							accessibleCells.Add(cell);
					}
				}

				static CPos AbstractCellForLocalCells(List<CPos> cells, byte layer)
				{
					var minX = int.MaxValue;
					var minY = int.MaxValue;
					var maxX = int.MinValue;
					var maxY = int.MinValue;
					foreach (var cell in cells)
					{
						minX = Math.Min(cell.X, minX);
						minY = Math.Min(cell.Y, minY);
						maxX = Math.Max(cell.X, maxX);
						maxY = Math.Max(cell.Y, maxY);
					}

					var regionWidth = maxX - minX;
					var regionHeight = maxY - minY;
					var desired = new CPos(minX + regionWidth / 2, minY + regionHeight / 2, layer);

					// Make sure the abstract cell is one of the available local cells.
					// This ensures each abstract cell we generate is unique.
					// We'll choose an abstract node as close to the middle of the region as possible.
					var abstractCell = desired;
					var distance = int.MaxValue;
					foreach (var cell in cells)
					{
						var newDistance = (cell - desired).LengthSquared;
						if (distance > newDistance)
						{
							distance = newDistance;
							abstractCell = cell;
						}
					}

					return abstractCell;
				}

				// Flood fill the search area from one of the accessible cells.
				// We can use this to determine the connected regions within the grid.
				// Each region we discover will be represented by an abstract node.
				// Repeat this process until all disjoint regions are discovered.
				var hasPopulatedAbstractCellForLayer = false;
				while (accessibleCells.Count > 0)
				{
					var src = accessibleCells.First();
					using (var search = GetLocalPathSearch(
						null, new[] { src }, src, customCost, null, BlockedByActor.None, false, grid, 100, null, false, null))
					{
						var localCellsInRegion = search.ExpandAll();
						var abstractCell = AbstractCellForLocalCells(localCellsInRegion, gridLayer);
						accessibleCells.ExceptWith(localCellsInRegion);

						// PERF: If there is only one distinct region of cells,
						// we can represent this grid with one abstract cell.
						// We don't need to remember how to map back from a local cell to an abstract cell.
						if (!hasPopulatedAbstractCellForLayer && accessibleCells.Count == 0)
							singleAbstractCellForLayer[gridLayer] = abstractCell;
						else
						{
							// When there is more than one region within the grid
							// (imagine a wall or stream separating the grid into disjoint areas)
							// then we will remember a mapping from local cells to each of their abstract cells.
							hasPopulatedAbstractCellForLayer = true;
							foreach (var localCell in localCellsInRegion)
								localCellToAbstractCell.Add(localCell, abstractCell);
						}
					}
				}
			}

			return new GridInfo(singleAbstractCellForLayer, localCellToAbstractCell);
		}

		/// <summary>
		/// Builds the abstract graph in entirety. The abstract graph contains edges between all the abstract nodes
		/// that represent the costs to move between them.
		/// </summary>
		void BuildCostTable()
		{
			abstractGraph = new Dictionary<CPos, List<GraphConnection>>(gridXs * gridYs);
			var customMovementLayers = world.GetCustomMovementLayers();
			for (var gridX = mapBounds.TopLeft.X; gridX < mapBounds.BottomRight.X; gridX += GridSize)
				for (var gridY = mapBounds.TopLeft.Y; gridY < mapBounds.BottomRight.Y; gridY += GridSize)
					foreach (var edges in GetAbstractEdgesForGrid(gridX, gridY, customMovementLayers))
						abstractGraph.Add(edges.Key, edges.Value);
		}

		/// <summary>
		/// For a given grid, determines the edges between the abstract nodes within the grid and the abstract nodes
		/// within adjacent grids on the same layer. Also determines any edges available to grids on other layers via
		/// custom movement layers.
		/// </summary>
		IEnumerable<KeyValuePair<CPos, List<GraphConnection>>> GetAbstractEdgesForGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLayers)
		{
			var abstractEdges = new HashSet<(CPos Src, CPos Dst)>();
			for (byte gridLayer = 0; gridLayer < customMovementLayers.Length; gridLayer++)
			{
				if (gridLayer != 0 &&
					(customMovementLayers[gridLayer] == null ||
					!customMovementLayers[gridLayer].EnabledForLocomotor(locomotor.Info)))
					continue;

				void AddEdgesIfMovementAllowedBetweenCells(CPos cell, CPos candidateCell)
				{
					if (!MovementAllowedBetweenCells(cell, candidateCell))
						return;

					var abstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell);
					if (abstractCell == null)
						return;

					var abstractCellAdjacent = AbstractCellForLocalCellNoAccessibleCheck(candidateCell);
					if (abstractCellAdjacent == null)
						return;

					abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value));
				}

				// Searches along edges of all grids within a layer.
				// Checks for the local edge cell if we can traverse to any of the three adjacent cells in the next grid.
				// Builds connections in the abstract graph when any local cells have connections.
				void AddAbstractEdges(int xIncrement, int yIncrement, CVec adjacentVec, int2 offset)
				{
					var startY = gridY + offset.Y;
					var startX = gridX + offset.X;
					for (var y = startY; y < startY + GridSize; y += yIncrement)
					{
						for (var x = startX; x < startX + GridSize; x += xIncrement)
						{
							var cell = new CPos(x, y, gridLayer);
							if (!CellIsAccessible(cell))
								continue;

							var adjacentCell = cell + adjacentVec;
							for (var i = -1; i <= 1; i++)
							{
								var candidateCell = adjacentCell + i * new CVec(adjacentVec.Y, adjacentVec.X);
								AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell);
							}
						}
					}
				}

				// Searches all cells within a layer.
				// Checks for the local cell if we can traverse from/to a custom movement layer.
				// Builds connections in the abstract graph when any local cells have connections.
				void AddAbstractCustomLayerEdges()
				{
					var gridCml = customMovementLayers[gridLayer];
					for (byte candidateLayer = 0; candidateLayer < customMovementLayers.Length; candidateLayer++)
					{
						if (gridLayer == candidateLayer)
							continue;

						var candidateCml = customMovementLayers[candidateLayer];
						if (candidateLayer != 0 && (candidateCml == null || !candidateCml.EnabledForLocomotor(locomotor.Info)))
							continue;

						for (var y = gridY; y < gridY + GridSize; y++)
						{
							for (var x = gridX; x < gridX + GridSize; x++)
							{
								var cell = new CPos(x, y, gridLayer);
								if (!CellIsAccessible(cell))
									continue;

								CPos candidateCell;
								if (gridLayer == 0)
								{
									candidateCell = new CPos(cell.X, cell.Y, candidateLayer);
									if (candidateCml.EntryMovementCost(locomotor.Info, candidateCell) == PathGraph.MovementCostForUnreachableCell)
										continue;
								}
								else
								{
									candidateCell = new CPos(cell.X, cell.Y, 0);
									if (gridCml.ExitMovementCost(locomotor.Info, candidateCell) == PathGraph.MovementCostForUnreachableCell)
										continue;
								}

								AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell);
							}
						}
					}
				}

				// Top, Left, Bottom, Right
				AddAbstractEdges(1, GridSize, new CVec(0, -1), new int2(0, 0));
				AddAbstractEdges(GridSize, 1, new CVec(-1, 0), new int2(0, 0));
				AddAbstractEdges(1, GridSize, new CVec(0, 1), new int2(0, GridSize - 1));
				AddAbstractEdges(GridSize, 1, new CVec(1, 0), new int2(GridSize - 1, 0));

				AddAbstractCustomLayerEdges();
			}

			return abstractEdges
				.GroupBy(edge => edge.Src)
				.Select(group => KeyValuePair.Create(
					group.Key,
					group.Select(edge => new GraphConnection(edge.Dst, costEstimator(edge.Src, edge.Dst))).ToList()));
		}

		/// <summary>
		/// When reachability changes for a cell, marks the grid it belongs to as out of date.
		/// </summary>
		void RequireCostRefreshInCell(CPos cell, short oldCost, short newCost)
		{
			// We don't care about the specific cost of the cell, just whether it is reachable or not.
			// This is good because updating the table is expensive, so only having to update it when
			// the reachability changes rather than for all costs changes saves us a lot of time.
			if (oldCost == PathGraph.MovementCostForUnreachableCell ^ newCost == PathGraph.MovementCostForUnreachableCell)
				dirtyGridIndexes.Add(GridIndex(cell));
		}

		bool CellIsAccessible(CPos cell)
		{
			return locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell &&
				(cellsWithBlockingActor == null || !cellsWithBlockingActor.Contains(cell));
		}

		bool MovementAllowedBetweenCells(CPos accessibleSrcCell, CPos destCell)
		{
			return locomotor.MovementCostToEnterCell(
				null, accessibleSrcCell, destCell, BlockedByActor.None, null) != PathGraph.MovementCostForUnreachableCell &&
				(cellsWithBlockingActor == null || !cellsWithBlockingActor.Contains(destCell));
		}

		/// <summary>
		/// When actors change for a cell, marks the grid it belongs to as out of date.
		/// </summary>
		void RequireBlockingRefreshInCell(CPos cell)
		{
			var cellHasBlockingActor = false;
			var actors = actorMap.GetActorsAt(cell);
			foreach (var actor in actors)
			{
				if (ActorIsBlocking(actor) && ActorCellIsBlocking(actor, cell))
				{
					cellHasBlockingActor = true;
					break;
				}
			}

			if (cellHasBlockingActor)
			{
				if (cellsWithBlockingActor.Add(cell))
					dirtyGridIndexes.Add(GridIndex(cell));
			}
			else
			{
				if (cellsWithBlockingActor.Remove(cell))
					dirtyGridIndexes.Add(GridIndex(cell));
			}
		}

		/// <summary>
		/// When map projection changes for a cell, marks the grid it belongs to as out of date.
		/// </summary>
		void RequireProjectionRefreshInCell(CPos cell)
		{
			dirtyGridIndexes.Add(GridIndex(cell));
		}

		/// <summary>
		/// <para>
		/// <see cref="BlockedByActor.Immovable"/> defines immovability based on the mobile trait. The blocking rules
		/// in <see cref="Locomotor.CanMoveFreelyInto(Actor, CPos, SubCell, BlockedByActor, Actor, bool)"/> allow units
		/// to pass these immovable actors if they are temporary blockers (e.g. gates) or crushable by the locomotor.
		/// Since our abstract graph must work for any actor, we have to be conservative and can only consider a subset
		/// of the immovable actors in the graph - ones we know cannot be passed by some actors due to these rules.
		/// Both this and <see cref="ActorCellIsBlocking"/> must be true for a cell to be blocked.
		/// </para>
		/// <para>
		/// This method is dependant on the logic in
		/// <see cref="Locomotor.CanMoveFreelyInto(Actor, CPos, SubCell, BlockedByActor, Actor, bool)"/> and
		/// <see cref="Locomotor.UpdateCellBlocking"/>. This method must be kept in sync with changes in the locomotor
		/// rules.
		/// </para>
		/// </summary>
		bool ActorIsBlocking(Actor actor)
		{
			var isMovable = actor.OccupiesSpace is Mobile mobile && !mobile.IsTraitDisabled && !mobile.IsTraitPaused && !mobile.IsImmovable;
			if (isMovable)
				return false;

			var isTemporaryBlocker = world.RulesContainTemporaryBlocker && actor.TraitOrDefault<ITemporaryBlocker>() != null;
			if (isTemporaryBlocker)
				return false;

			foreach (var crushable in actor.Crushables)
				if (world.NoPlayersMask != crushable.CrushableBy(actor, locomotor.Info.Crushes))
					return false;

			return true;
		}

		/// <summary>
		/// The blocking rules additionally allow some cells to be considered passable even if the actor is blocking.
		/// A cell is passable if the locomotor can share the cell and a subcell is available. It is also passable if
		/// it is a transit only cell of a <see cref="Building"/>. We cannot consider these cells to be blocked.
		/// Both this and <see cref="ActorIsBlocking"/> must be true for a cell to be blocked.
		/// </summary>
		bool ActorCellIsBlocking(Actor actor, CPos cell)
		{
			var canShareCell = locomotor.Info.SharesCell && actorMap.HasFreeSubCell(cell);
			if (canShareCell)
				return false;

			var isTransitOnly = actor.OccupiesSpace is Building building && building.TransitOnlyCells().Contains(cell);
			if (isTransitOnly)
				return false;

			return true;
		}

		int GridIndex(CPos cellInGrid)
		{
			return
				(cellInGrid.Y - mapBounds.TopLeft.Y) / GridSize * gridXs +
				(cellInGrid.X - mapBounds.TopLeft.X) / GridSize;
		}

		CPos GetGridTopLeft(int gridIndex, byte layer)
		{
			return new CPos(
				gridIndex % gridXs * GridSize + mapBounds.TopLeft.X,
				gridIndex / gridXs * GridSize + mapBounds.TopLeft.Y,
				layer);
		}

		static CPos GetGridTopLeft(CPos cellInGrid, Grid mapBounds)
		{
			return new CPos(
				(cellInGrid.X - mapBounds.TopLeft.X) / GridSize * GridSize + mapBounds.TopLeft.X,
				(cellInGrid.Y - mapBounds.TopLeft.Y) / GridSize * GridSize + mapBounds.TopLeft.Y,
				cellInGrid.Layer);
		}

		static Grid GetGrid(CPos cellInGrid, Grid mapBounds)
		{
			var gridTopLeft = GetGridTopLeft(cellInGrid, mapBounds);
			var width = Math.Min(mapBounds.BottomRight.X - gridTopLeft.X, GridSize);
			var height = Math.Min(mapBounds.BottomRight.Y - gridTopLeft.Y, GridSize);

			return new Grid(
				gridTopLeft,
				gridTopLeft + new CVec(width, height),
				true);
		}

		/// <summary>
		/// Calculates a path for the actor from multiple possible sources to target, using a unidirectional search.
		/// Returned path is *reversed* and given target to source.
		/// The actor must use the same <see cref="Locomotor"/> as this <see cref="HierarchicalPathFinder"/>.
		/// </summary>
		public List<CPos> FindPath(Actor self, IReadOnlyCollection<CPos> sources, CPos target,
			BlockedByActor check, int heuristicWeightPercentage, Func<CPos, int> customCost,
			Actor ignoreActor, bool inReverse, bool laneBias, PathFinderOverlay pathFinderOverlay)
		{
			if (costEstimator == null)
				return PathFinder.NoPath;

			pathFinderOverlay?.NewRecording(self, sources, target);

			if (!world.Map.Contains(target))
				return PathFinder.NoPath;

			RebuildDirtyGrids();

			var targetAbstractCell = AbstractCellForLocalCell(target);
			if (targetAbstractCell == null)
				return PathFinder.NoPath;

			RebuildDomains();
			var targetDomain = abstractDomains[targetAbstractCell.Value];

			// Unlike the target cell, the source cell is allowed to be an unreachable location.
			// Instead, what matters is whether any cell adjacent to the source cell can be reached.
			var sourcesWithPathableNodes = new HashSet<CPos>(sources.Count);
			var sourceEdges = new List<GraphEdge>(sources.Count);
			List<CPos> unpathableNodes = null;
			foreach (var source in sources)
			{
				if (!world.Map.Contains(source))
					continue;

				// The source cell is reachable, we can add an edge from there and have no need to check adjacent cells.
				var sourceAbstractCell = AbstractCellForLocalCell(source);
				if (sourceAbstractCell != null)
				{
					// If the source and target belong to different domains, there is no path.
					var sourceDomain = abstractDomains[sourceAbstractCell.Value];
					if (sourceDomain != targetDomain)
						continue;

					sourcesWithPathableNodes.Add(source);
					var sourceEdge = EdgeFromLocalToAbstract(source, sourceAbstractCell.Value);
					if (sourceEdge != null)
						sourceEdges.Add(sourceEdge.Value);
					continue;
				}

				// If the source cell is unreachable, we must add edges from any adjacent cells that are reachable instead.
				foreach (var dir in CVec.Directions)
				{
					var adjacentSource = source + dir;
					if (!MovementAllowedBetweenCells(source, adjacentSource))
						continue;

					var adjacentSourceAbstractCell = AbstractCellForLocalCell(adjacentSource);
					if (adjacentSourceAbstractCell == null)
						continue;

					// If the source and target belong to different domains, there is no path.
					var adjacentSourceDomain = abstractDomains[adjacentSourceAbstractCell.Value];
					if (adjacentSourceDomain != targetDomain)
					{
						unpathableNodes ??= new List<CPos>();
						unpathableNodes.Add(adjacentSource);
						continue;
					}

					sourcesWithPathableNodes.Add(source);
					var sourceEdge = EdgeFromLocalToAbstract(adjacentSource, adjacentSourceAbstractCell.Value);
					if (sourceEdge != null)
						sourceEdges.Add(sourceEdge.Value);
				}
			}

			if (sourcesWithPathableNodes.Count == 0)
				return PathFinder.NoPath;

			var targetEdge = EdgeFromLocalToAbstract(target, targetAbstractCell.Value);

			// The new edges will be treated as bi-directional.
			var fullGraph = new AbstractGraphWithInsertedEdges(abstractGraph, sourceEdges, targetEdge, costEstimator);

			// Determine an abstract path to all sources, for use in a unidirectional search.
			var estimatedSearchSize = (abstractGraph.Count + 2) / 8;
			using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph(
				fullGraph.GetConnections, locomotor, target, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
			{
				using (var fromSrc = GetLocalPathSearch(
					self, sourcesWithPathableNodes, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
					Heuristic(reverseAbstractSearch, estimatedSearchSize, sourcesWithPathableNodes, unpathableNodes),
					inReverse,
					pathFinderOverlay?.RecordLocalEdges(self)))
					return fromSrc.FindPath();
			}
		}

		/// <summary>
		/// Calculates a path for the actor from source to target, using a bidirectional search.
		/// Returned path is *reversed* and given target to source.
		/// The actor must use the same <see cref="Locomotor"/> as this <see cref="HierarchicalPathFinder"/>.
		/// </summary>
		public List<CPos> FindPath(Actor self, CPos source, CPos target,
			BlockedByActor check, int heuristicWeightPercentage, Func<CPos, int> customCost,
			Actor ignoreActor, bool inReverse, bool laneBias, PathFinderOverlay pathFinderOverlay)
		{
			if (costEstimator == null)
				return PathFinder.NoPath;

			// If the source and target are close, see if they can be reached locally.
			// This avoids the cost of an abstract search unless we need one.
			const int CloseGridDistance = 2;
			if ((target - source).LengthSquared < GridSize * GridSize * CloseGridDistance * CloseGridDistance && source.Layer == target.Layer)
			{
				var gridToSearch = new Grid(
					new CPos(
						Math.Min(source.X, target.X) - GridSize / 2,
						Math.Min(source.Y, target.Y) - GridSize / 2,
						source.Layer),
					new CPos(
						Math.Max(source.X, target.X) + GridSize / 2,
						Math.Max(source.Y, target.Y) + GridSize / 2,
						source.Layer),
					false);

				pathFinderOverlay?.NewRecording(self, new[] { source }, target);

				List<CPos> localPath;
				using (var search = GetLocalPathSearch(
					self, new[] { source }, target, customCost, ignoreActor, check, laneBias, gridToSearch, heuristicWeightPercentage,
					null,
					inReverse,
					pathFinderOverlay?.RecordLocalEdges(self)))
					localPath = search.FindPath();

				if (localPath.Count > 0)
					return localPath;
			}

			pathFinderOverlay?.NewRecording(self, new[] { source }, target);

			RebuildDirtyGrids();

			// If the target cell is unreachable, there is no path.
			var targetAbstractCell = AbstractCellForLocalCell(target);
			if (targetAbstractCell == null)
				return PathFinder.NoPath;

			// If the source cell is unreachable, there may still be a path.
			// As long as one of the cells adjacent to the source is reachable, the path can be made.
			// Call the other overload which can handle this scenario.
			var sourceAbstractCell = AbstractCellForLocalCell(source);
			if (sourceAbstractCell == null)
				return FindPath(self, new[] { source }, target, check, heuristicWeightPercentage, customCost, ignoreActor, inReverse, laneBias, pathFinderOverlay);

			// If the source and target belong to different domains, there is no path.
			RebuildDomains();
			var targetDomain = abstractDomains[targetAbstractCell.Value];
			var sourceDomain = abstractDomains[sourceAbstractCell.Value];
			if (sourceDomain != targetDomain)
				return PathFinder.NoPath;

			var targetEdge = EdgeFromLocalToAbstract(target, targetAbstractCell.Value);
			var sourceEdge = EdgeFromLocalToAbstract(source, sourceAbstractCell.Value);

			// The new edges will be treated as bi-directional.
			var fullGraph = new AbstractGraphWithInsertedEdges(
				abstractGraph, sourceEdge != null ? new[] { sourceEdge.Value } : Array.Empty<GraphEdge>(), targetEdge, costEstimator);

			// Determine an abstract path in both directions, for use in a bidirectional search.
			var estimatedSearchSize = (abstractGraph.Count + 2) / 8;
			using (var forwardAbstractSearch = PathSearch.ToTargetCellOverGraph(
				fullGraph.GetConnections, locomotor, source, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
			{
				if (!forwardAbstractSearch.ExpandToTarget())
					return PathFinder.NoPath;

				using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph(
					fullGraph.GetConnections, locomotor, target, source, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
				{
					reverseAbstractSearch.ExpandToTarget();

					using (var fromSrc = GetLocalPathSearch(
						self, new[] { source }, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
						Heuristic(reverseAbstractSearch, estimatedSearchSize, null, null),
						inReverse,
						pathFinderOverlay?.RecordLocalEdges(self)))
					using (var fromDest = GetLocalPathSearch(
						self, new[] { target }, source, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
						Heuristic(forwardAbstractSearch, estimatedSearchSize, null, null),
						!inReverse,
						pathFinderOverlay?.RecordLocalEdges(self)))
						return PathSearch.FindBidiPath(fromDest, fromSrc);
				}
			}
		}

		/// <summary>
		/// Determines if a path exists between source and target.
		/// When <see cref="BlockedByActor.None"/> was given, only terrain is taken into account,
		/// i.e. as if <see cref="BlockedByActor.None"/> was used when finding a path.
		/// When <see cref="BlockedByActor.Immovable"/> was given, a subset of immovable actors are also taken into
		/// account. If the method returns false, there is definitely no path. If it returns true there could be a
		/// path, but it is possible that there is no path because of an immovable actor that does not belong to the
		/// subset of actors that can be accounted for. So be careful.
		/// This would apply for any actor using the same <see cref="Locomotor"/> as this <see cref="HierarchicalPathFinder"/>.
		/// </summary>
		public bool PathExists(CPos source, CPos target)
		{
			if (costEstimator == null)
				return false;

			if (!world.Map.Contains(source) || !world.Map.Contains(target))
				return false;

			RebuildDomains();

			var abstractTarget = AbstractCellForLocalCell(target);
			if (abstractTarget == null)
				return false;
			var targetDomain = abstractDomains[abstractTarget.Value];

			// The source cell is reachable, we can compare the domains directly.
			var abstractSource = AbstractCellForLocalCell(source);
			if (abstractSource != null)
			{
				var sourceDomain = abstractDomains[abstractSource.Value];
				return sourceDomain == targetDomain;
			}

			// Unlike the target cell, the source cell is allowed to be an unreachable location.
			// Instead, what matters is whether any cell adjacent to the source cell can be reached.
			// So we need to compare the domains of reachable cells adjacent to the source location.
			foreach (var dir in CVec.Directions)
			{
				var adjacentSource = source + dir;
				if (!MovementAllowedBetweenCells(source, adjacentSource))
					continue;

				var abstractAdjacentSource = AbstractCellForLocalCell(adjacentSource);
				if (abstractAdjacentSource == null)
					continue;

				var adjacentSourceDomain = abstractDomains[abstractAdjacentSource.Value];
				if (adjacentSourceDomain == targetDomain)
					return true;
			}

			return false;
		}

		/// <summary>
		/// The abstract graph can become out of date when reachability costs for terrain change.
		/// When this occurs, we must rebuild any affected parts of the abstract graph so it remains correct.
		/// </summary>
		void RebuildDirtyGrids()
		{
			if (dirtyGridIndexes.Count == 0)
				return;

			// An empty domain indicates it is out of date and will require rebuilding when next accessed.
			abstractDomains.Clear();

			var customMovementLayers = world.GetCustomMovementLayers();
			foreach (var gridIndex in dirtyGridIndexes)
			{
				var oldGrid = gridInfos[gridIndex];
				var gridTopLeft = GetGridTopLeft(gridIndex, 0);
				gridInfos[gridIndex] = BuildGrid(gridTopLeft.X, gridTopLeft.Y, customMovementLayers);
				RebuildCostTable(gridTopLeft.X, gridTopLeft.Y, oldGrid, customMovementLayers);
			}

			dirtyGridIndexes.Clear();
		}

		/// <summary>
		/// Updates the abstract graph to account for changes in a specific grid. Any nodes and edges related to that
		/// grid will be removed, new nodes and edges will be determined and then inserted into the graph.
		/// </summary>
		void RebuildCostTable(int gridX, int gridY, GridInfo oldGrid, ICustomMovementLayer[] customMovementLayers)
		{
			// For this grid, it is possible the abstract nodes have changed.
			// Remove the old abstract nodes for this grid.
			// This is important as GetAbstractEdgesForGrid will look at the *current* grids.
			// So it won't be aware of any nodes that disappeared before we updated the grid.
			var abstractNodes = new HashSet<CPos>();
			oldGrid.CopyAbstractCellsInto(abstractNodes);
			foreach (var oldAbstractNode in abstractNodes)
				abstractGraph.Remove(oldAbstractNode);
			abstractNodes.Clear();

			// Add new abstract edges for this grid, since we cleared out the old nodes everything should be new.
			foreach (var edges in GetAbstractEdgesForGrid(gridX, gridY, customMovementLayers))
				abstractGraph.Add(edges.Key, edges.Value);

			foreach (var direction in CVec.Directions)
			{
				var adjacentGrid = new CPos(gridX, gridY) + GridSize * direction;
				if (!mapBounds.Contains(adjacentGrid))
					continue;

				// For all adjacent grids, their abstract nodes will not have changed, but the connections may have done.
				// Update the connections, and keep track of which nodes we have updated.
				gridInfos[GridIndex(adjacentGrid)].CopyAbstractCellsInto(abstractNodes);
				foreach (var edges in GetAbstractEdgesForGrid(adjacentGrid.X, adjacentGrid.Y, customMovementLayers))
				{
					abstractGraph[edges.Key] = edges.Value;
					abstractNodes.Remove(edges.Key);
				}

				// If any nodes were left over they have no connections now, so we can remove them from the graph.
				foreach (var unconnectedNode in abstractNodes)
					abstractGraph.Remove(unconnectedNode);
				abstractNodes.Clear();
			}
		}

		/// <summary>
		/// The abstract domains can become out of date when the abstract graph changes.
		/// When this occurs, we must rebuild the domain cache.
		/// </summary>
		void RebuildDomains()
		{
			// First, rebuild the abstract graph if it is out of date.
			RebuildDirtyGrids();

			// Check if our domain cache is empty, if so this indicates it is out-of-date and needs rebuilding.
			if (abstractDomains.Count != 0)
				return;

			List<GraphConnection> AbstractEdge(CPos abstractCell)
			{
				if (abstractGraph.TryGetValue(abstractCell, out var abstractEdge))
					return abstractEdge;
				return null;
			}

			// As in BuildGrid, flood fill the search graph until all disjoint domains are discovered.
			var domain = 0u;
			var abstractCells = new HashSet<CPos>(abstractGraph.Count);
			foreach (var grid in gridInfos)
				grid.CopyAbstractCellsInto(abstractCells);
			while (abstractCells.Count > 0)
			{
				var searchCell = abstractCells.First();
				var search = PathSearch.ToTargetCellOverGraph(
					AbstractEdge,
					locomotor,
					searchCell,
					searchCell,
					abstractGraph.Count / 8);
				var searched = search.ExpandAll();
				foreach (var abstractCell in searched)
					abstractDomains.Add(abstractCell, domain);
				abstractCells.ExceptWith(searched);
				domain++;
			}
		}

		/// <summary>
		/// Maps a local cell to a abstract node in the graph. Returns null when the local cell is unreachable.
		/// The cell must have been checked to be on the map with <see cref="Map.Contains(CPos)"/>.
		/// </summary>
		CPos? AbstractCellForLocalCell(CPos localCell)
		{
			return gridInfos[GridIndex(localCell)].AbstractCellForLocalCell(localCell, this);
		}

		/// <summary>
		/// Maps a local cell to a abstract node in the graph. Returns null when the local cell is unreachable.
		/// Skips the <see cref="CellIsAccessible"/> check, if it has already been performed.
		/// If an accessible check has not been performed, call <see cref="AbstractCellForLocalCell"/> instead.
		/// </summary>
		CPos? AbstractCellForLocalCellNoAccessibleCheck(CPos localCell)
		{
			return gridInfos[GridIndex(localCell)].AbstractCellForLocalCell(localCell, null);
		}

		/// <summary>
		/// Creates a <see cref="GraphEdge"/> from the <paramref name="localCell"/> to the <paramref name="abstractCell"/>.
		/// Return null when no edge is required, because the cells match.
		/// </summary>
		GraphEdge? EdgeFromLocalToAbstract(CPos localCell, CPos abstractCell)
		{
			if (localCell == abstractCell)
				return null;

			return new GraphEdge(localCell, abstractCell, costEstimator(localCell, abstractCell));
		}

		/// <summary>
		/// Uses the provided abstract search to provide an estimate of the distance remaining to the target
		/// (the heuristic) for a local path search. The abstract search must run in the opposite direction to the
		/// local search. So when searching from source to target, the abstract search must be from target to source.
		/// </summary>
		Func<CPos, bool, int> Heuristic(PathSearch abstractSearch, int estimatedSearchSize,
			HashSet<CPos> sources, List<CPos> unpathableNodes)
		{
			var nodeForCostLookup = new Dictionary<CPos, CPos>(estimatedSearchSize);
			var graph = (SparsePathGraph)abstractSearch.Graph;
			return (cell, knownAccessible) =>
			{
				// When dealing with an unreachable source cell, the path search will check adjacent locations.
				// These cells may be reachable, but may represent jumping into an area cut off from the target.
				// Searching on the abstract graph would fail to provide a route in this scenario, so bail early.
				if (unpathableNodes != null && unpathableNodes.Contains(cell))
					return PathGraph.PathCostForInvalidPath;

				// During a search, all other cells searched by the heuristic are guaranteed to be reachable.
				// So we don't need to handle an abstract cell lookup failing, or the search failing to expand.
				// Cells added as initial starting points for the search might not be reachable,
				// so for those we need to perform accessibility checks.
				// If the exceptions here do fire, they indicate a bug. The abstract graph is considering a cell to be
				// unreachable, but the local pathfinder thinks it is reachable. We must fix the abstract graph to also
				// consider the cell to be reachable.
				CPos? maybeAbstractCell;
				if (knownAccessible)
					maybeAbstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell);
				else
					maybeAbstractCell = AbstractCellForLocalCell(cell);

				if (maybeAbstractCell == null)
				{
					// If the source cell is unreachable, use one of the adjacent reachable cells instead.
					if (sources != null && sources.Contains(cell))
					{
						foreach (var dir in CVec.Directions)
						{
							var adjacentCell = cell + dir;
							if (!MovementAllowedBetweenCells(cell, adjacentCell) ||
								(unpathableNodes != null && unpathableNodes.Contains(adjacentCell)))
								continue;

							// Ideally we'd choose the cheapest cell rather than just any one of them,
							// but we're lazy and this is an edge case.
							maybeAbstractCell = AbstractCellForLocalCell(adjacentCell);
							if (maybeAbstractCell != null)
								break;
						}
					}

					if (maybeAbstractCell == null)
						throw new Exception(
							"The abstract path should never be searched for an unreachable point. " +
							$"Cell {cell} failed lookup for an abstract cell.");
				}

				var abstractCell = maybeAbstractCell.Value;
				var info = graph[abstractCell];

				// Expand the abstract search only if we have yet to get a route to the abstract cell.
				if (info.Status != CellStatus.Closed)
				{
					abstractSearch.TargetPredicate = c => c == abstractCell;
					if (!abstractSearch.ExpandToTarget())
						throw new Exception(
							"The abstract path should never be searched for an unreachable point. " +
							$"Abstract cell {abstractCell} failed to route to abstract cell.");
					info = graph[abstractCell];
				}

				var abstractNode = info.PreviousNode;

				// When transitioning between layers, the XY will be the same and only the layer changes.
				// If we have transitioned layers then we need the next node
				// along otherwise we're measuring from our current location.
				if (abstractCell.Layer != abstractNode.Layer)
					abstractNode = graph[abstractNode].PreviousNode;

				// Now we have an abstract node to target, determine if there is one further along the path we can use.
				// This will provide a better estimate. Cache these results as they are expensive to calculate.
				if (!nodeForCostLookup.TryGetValue(abstractNode, out var abstractNodeForCost))
				{
					abstractNodeForCost = AbstractNodeForCost(graph, abstractCell, abstractNode);
					nodeForCostLookup.Add(abstractNode, abstractNodeForCost);
				}

				var cost = graph[abstractNodeForCost].CostSoFar + costEstimator(cell, abstractNodeForCost);
				return cost;
			};
		}

		/// <summary>
		/// Determines an abstract node further along the path which can be reached directly without deviating from the
		/// abstract path from the abstract cell of the source location.
		/// As this node can be reached directly we can target it instead
		/// of the original node to provide a better cost estimate.
		/// </summary>
		CPos AbstractNodeForCost(SparsePathGraph graph, CPos abstractCell, CPos abstractNode)
		{
			// We currently have the next abstract node along our path.
			// This means we can create a distance estimate for our current
			// cell to this next abstract node, plus then the cost provided by the abstract path.
			// | AC --- AN ---------------------------- D |
			// This means the lowest cost estimate follows the abstract path.
			// The abstract path becomes a sort of highway, units want to
			// move to it as soon as possible and follow it.
			// This causes issues when a unit can move in a straight line to the destination.
			// Instead of moving in a straight line, they move to join the highway, travel the highway,
			// then leave the highway at the other end.
			// We can combat this by delaying them from joining the highway until the abstract paths turns.
			// | AC ----------------- AN -------------- D |
			// This means the unit will move in a direct path whilst it can, and only "join the highway"
			// when the highway is going to navigate it around an obstacle.
			// When the heuristic weight is >100%, this greatly improves the resulting path.
			// As when the weight is higher we may never get a chance to check for the shorter direct path.
			var abstractNodesAlongPath = new List<CPos>();
			while (true)
			{
				var previousAbstractNode = graph[abstractNode].PreviousNode;

				// The whole abstract path has been travelled, can't go further.
				if (previousAbstractNode == abstractNode)
					break;

				// Check if we can move directly to the new node whilst staying
				// within the boundary of the abstract path so far.
				var intersectsAllNodes = true;
				abstractNodesAlongPath.Add(abstractNode);
				foreach (var node in abstractNodesAlongPath)
				{
					if (!GetGrid(node, mapBounds).IntersectsLine(abstractCell, previousAbstractNode))
					{
						intersectsAllNodes = false;
						break;
					}
				}

				if (!intersectsAllNodes)
					break;

				abstractNode = previousAbstractNode;
			}

			return abstractNode;
		}

		PathSearch GetLocalPathSearch(
			Actor self, IEnumerable<CPos> srcs, CPos dst, Func<CPos, int> customCost,
			Actor ignoreActor, BlockedByActor check, bool laneBias, Grid? grid, int heuristicWeightPercentage,
			Func<CPos, bool, int> heuristic,
			bool inReverse,
			PathSearch.IRecorder recorder)
		{
			return PathSearch.ToTargetCell(
				world, locomotor, self, srcs, dst, check, heuristicWeightPercentage,
				customCost, ignoreActor, laneBias, inReverse, heuristic, grid, recorder);
		}
	}
}
