#region Copyright & License Information
/*
 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
 * 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.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Traits
{
	[Desc("Unit is able to move.")]
	public class MobileInfo : PausableConditionalTraitInfo, IMoveInfo, IPositionableInfo, IFacingInfo, IActorPreviewInitInfo,
		IEditorActorOptions
	{
		[LocomotorReference]
		[FieldLoader.Require]
		[Desc("Which Locomotor does this trait use. Must be defined on the World actor.")]
		public readonly string Locomotor = null;

		public readonly int InitialFacing = 0;

		[Desc("Speed at which the actor turns.")]
		public readonly int TurnSpeed = 255;

		public readonly int Speed = 1;

		public readonly string Cursor = "move";
		public readonly string BlockedCursor = "move-blocked";

		[VoiceReference]
		public readonly string Voice = "Action";

		[Desc("Facing to use for actor previews (map editor, color picker, etc)")]
		public readonly int PreviewFacing = 96;

		[Desc("Display order for the facing slider in the map editor")]
		public readonly int EditorFacingDisplayOrder = 3;

		[ConsumedConditionReference]
		[Desc("Boolean expression defining the condition under which the regular (non-force) move cursor is disabled.")]
		public readonly BooleanExpression RequireForceMoveCondition = null;

		[ConsumedConditionReference]
		[Desc("Boolean expression defining the condition under which this actor cannot be nudged by other actors.")]
		public readonly BooleanExpression ImmovableCondition = null;

		IEnumerable<object> IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type)
		{
			yield return new FacingInit(PreviewFacing);
		}

		public override object Create(ActorInitializer init) { return new Mobile(init, this); }

		public LocomotorInfo LocomotorInfo { get; private set; }

		public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
		{
			var locomotorInfos = rules.Actors["world"].TraitInfos<LocomotorInfo>();
			LocomotorInfo = locomotorInfos.FirstOrDefault(li => li.Name == Locomotor);
			if (LocomotorInfo == null)
				throw new YamlException("A locomotor named '{0}' doesn't exist.".F(Locomotor));
			else if (locomotorInfos.Count(li => li.Name == Locomotor) > 1)
				throw new YamlException("There is more than one locomotor named '{0}'.".F(Locomotor));

			// We need to reset the reference to the locomotor between each worlds, otherwise we are reference the previous state.
			locomotor = null;

			base.RulesetLoaded(rules, ai);
		}

		public int GetInitialFacing() { return InitialFacing; }

		// initialized and used by CanEnterCell
		Locomotor locomotor;

		/// <summary>
		/// Note: If the target <paramref name="cell"/> has any free subcell, the value of <paramref name="subCell"/> is ignored.
		/// </summary>
		public bool CanEnterCell(World world, Actor self, CPos cell, SubCell subCell = SubCell.FullCell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
		{
			// PERF: Avoid repeated trait queries on the hot path
			if (locomotor == null)
				locomotor = world.WorldActor.TraitsImplementing<Locomotor>()
				   .SingleOrDefault(l => l.Info.Name == Locomotor);

			if (locomotor.MovementCostForCell(cell) == short.MaxValue)
				return false;

			return locomotor.CanMoveFreelyInto(self, cell, subCell, check, ignoreActor);
		}

		public bool CanStayInCell(World world, CPos cell)
		{
			// PERF: Avoid repeated trait queries on the hot path
			if (locomotor == null)
				locomotor = world.WorldActor.TraitsImplementing<Locomotor>()
				   .SingleOrDefault(l => l.Info.Name == Locomotor);

			if (cell.Layer == CustomMovementLayerType.Tunnel)
				return false;

			return locomotor.CanStayInCell(cell);
		}

		public IReadOnlyDictionary<CPos, SubCell> OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any)
		{
			return new ReadOnlyDictionary<CPos, SubCell>(new Dictionary<CPos, SubCell>() { { location, subCell } });
		}

		bool IOccupySpaceInfo.SharesCell { get { return LocomotorInfo.SharesCell; } }

		IEnumerable<EditorActorOption> IEditorActorOptions.ActorOptions(ActorInfo ai, World world)
		{
			yield return new EditorActorSlider("Facing", EditorFacingDisplayOrder, 0, 255, 8,
				actor =>
				{
					var init = actor.Init<FacingInit>();
					return init != null ? init.Value(world) : InitialFacing;
				},
				(actor, value) =>
				{
					// TODO: This can all go away once turrets are properly defined as a relative facing
					var turretInit = actor.Init<TurretFacingInit>();
					var turretsInit = actor.Init<TurretFacingsInit>();
					var facingInit = actor.Init<FacingInit>();

					var oldFacing = facingInit != null ? facingInit.Value(world) : InitialFacing;
					var newFacing = (int)value;

					if (turretInit != null)
					{
						var newTurretFacing = (turretInit.Value(world) + newFacing - oldFacing + 255) % 255;
						actor.ReplaceInit(new TurretFacingInit(newTurretFacing));
					}

					if (turretsInit != null)
					{
						var newTurretFacings = turretsInit.Value(world)
							.ToDictionary(kv => kv.Key, kv => (kv.Value + newFacing - oldFacing + 255) % 255);
						actor.ReplaceInit(new TurretFacingsInit(newTurretFacings));
					}

					actor.ReplaceInit(new FacingInit(newFacing));
				});
		}
	}

	public class Mobile : PausableConditionalTrait<MobileInfo>, IIssueOrder, IResolveOrder, IOrderVoice, IPositionable, IMove, ITick, ICreationActivity,
		IFacing, IDeathActorInitModifier, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyBlockingMove, IActorPreviewInitModifier, INotifyBecomingIdle
	{
		readonly Actor self;
		readonly Lazy<IEnumerable<int>> speedModifiers;

		readonly bool returnToCellOnCreation;
		readonly bool returnToCellOnCreationRecalculateSubCell = true;
		readonly int creationActivityDelay;

		#region IMove CurrentMovementTypes
		MovementType movementTypes;
		public MovementType CurrentMovementTypes
		{
			get
			{
				return movementTypes;
			}

			set
			{
				var oldValue = movementTypes;
				movementTypes = value;
				if (value != oldValue)
				{
					self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
					foreach (var n in notifyMoving)
						n.MovementTypeChanged(self, value);
				}
			}
		}
		#endregion

		int oldFacing, facing;
		WPos oldPos;
		CPos fromCell, toCell;
		public SubCell FromSubCell, ToSubCell;
		INotifyCustomLayerChanged[] notifyCustomLayerChanged;
		INotifyVisualPositionChanged[] notifyVisualPositionChanged;
		INotifyMoving[] notifyMoving;
		INotifyFinishedMoving[] notifyFinishedMoving;
		IWrapMove[] moveWrappers;
		bool requireForceMove;

		public bool IsImmovable { get; private set; }
		public bool TurnToMove;
		public bool IsBlocking { get; private set; }

		public bool IsMovingBetweenCells
		{
			get { return FromCell != ToCell; }
		}

		#region IFacing
		[Sync]
		public int Facing
		{
			get { return facing; }
			set { facing = value; }
		}

		public int TurnSpeed { get { return Info.TurnSpeed; } }
		#endregion

		[Sync]
		public CPos FromCell { get { return fromCell; } }

		[Sync]
		public CPos ToCell { get { return toCell; } }

		[Sync]
		public int PathHash;	// written by Move.EvalPath, to temporarily debug this crap.

		public Locomotor Locomotor { get; private set; }

		public IPathFinder Pathfinder { get; private set; }

		#region IOccupySpace

		[Sync]
		public WPos CenterPosition { get; private set; }

		public CPos TopLeft { get { return ToCell; } }

		public Pair<CPos, SubCell>[] OccupiedCells()
		{
			if (FromCell == ToCell)
				return new[] { Pair.New(FromCell, FromSubCell) };

			// HACK: Should be fixed properly, see https://github.com/OpenRA/OpenRA/pull/17292 for an explanation
			if (Info.LocomotorInfo.SharesCell)
				return new[] { Pair.New(ToCell, ToSubCell) };

			return new[] { Pair.New(FromCell, FromSubCell), Pair.New(ToCell, ToSubCell) };
		}
		#endregion

		public Mobile(ActorInitializer init, MobileInfo info)
			: base(info)
		{
			self = init.Self;

			speedModifiers = Exts.Lazy(() => self.TraitsImplementing<ISpeedModifier>().ToArray().Select(x => x.GetSpeedModifier()));

			ToSubCell = FromSubCell = info.LocomotorInfo.SharesCell ? init.World.Map.Grid.DefaultSubCell : SubCell.FullCell;
			if (init.Contains<SubCellInit>())
			{
				FromSubCell = ToSubCell = init.Get<SubCellInit, SubCell>();
				returnToCellOnCreationRecalculateSubCell = false;
			}

			if (init.Contains<LocationInit>())
			{
				fromCell = toCell = init.Get<LocationInit, CPos>();
				SetVisualPosition(self, init.World.Map.CenterOfSubCell(FromCell, FromSubCell));
			}

			Facing = oldFacing = init.Contains<FacingInit>() ? init.Get<FacingInit, int>() : info.InitialFacing;

			// Sets the initial visual position
			// Unit will move into the cell grid (defined by LocationInit) as its initial activity
			if (init.Contains<CenterPositionInit>())
			{
				oldPos = init.Get<CenterPositionInit, WPos>();
				SetVisualPosition(self, oldPos);
				returnToCellOnCreation = true;
			}

			if (init.Contains<CreationActivityDelayInit>())
				creationActivityDelay = init.Get<CreationActivityDelayInit, int>();
		}

		protected override void Created(Actor self)
		{
			notifyCustomLayerChanged = self.TraitsImplementing<INotifyCustomLayerChanged>().ToArray();
			notifyVisualPositionChanged = self.TraitsImplementing<INotifyVisualPositionChanged>().ToArray();
			notifyMoving = self.TraitsImplementing<INotifyMoving>().ToArray();
			notifyFinishedMoving = self.TraitsImplementing<INotifyFinishedMoving>().ToArray();
			moveWrappers = self.TraitsImplementing<IWrapMove>().ToArray();
			Pathfinder = self.World.WorldActor.Trait<IPathFinder>();
			Locomotor = self.World.WorldActor.TraitsImplementing<Locomotor>()
				.Single(l => l.Info.Name == Info.Locomotor);

			base.Created(self);
		}

		void ITick.Tick(Actor self)
		{
			UpdateMovement(self);
		}

		public void UpdateMovement(Actor self)
		{
			var newMovementTypes = MovementType.None;
			if ((oldPos - CenterPosition).HorizontalLengthSquared != 0)
				newMovementTypes |= MovementType.Horizontal;

			if (oldPos.Z != CenterPosition.Z)
				newMovementTypes |= MovementType.Vertical;

			if (oldFacing != Facing)
				newMovementTypes |= MovementType.Turn;

			CurrentMovementTypes = newMovementTypes;

			oldPos = CenterPosition;
			oldFacing = Facing;
		}

		void INotifyAddedToWorld.AddedToWorld(Actor self)
		{
			self.World.AddToMaps(self, this);
		}

		void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
		{
			self.World.RemoveFromMaps(self, this);
		}

		protected override void TraitEnabled(Actor self)
		{
			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
		}

		protected override void TraitDisabled(Actor self)
		{
			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
		}

		protected override void TraitResumed(Actor self)
		{
			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
		}

		protected override void TraitPaused(Actor self)
		{
			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
		}

		#region Local misc stuff

		public void Nudge(Actor nudger)
		{
			if (IsTraitDisabled || IsTraitPaused || IsImmovable)
				return;

			var cell = GetAdjacentCell(nudger.Location);
			if (cell != null)
				self.QueueActivity(false, MoveTo(cell.Value, 0));
		}

		public CPos? GetAdjacentCell(CPos nextCell)
		{
			var availCells = new List<CPos>();
			var notStupidCells = new List<CPos>();
			foreach (CVec direction in CVec.Directions)
			{
				var p = ToCell + direction;
				if (CanEnterCell(p) && CanStayInCell(p))
					availCells.Add(p);
				else if (p != nextCell && p != ToCell)
					notStupidCells.Add(p);
			}

			CPos? newCell = null;
			if (availCells.Count > 0)
				newCell = availCells.Random(self.World.SharedRandom);
			else
			{
				var cellInfo = notStupidCells
					.SelectMany(c => self.World.ActorMap.GetActorsAt(c).Where(IsMovable),
						(c, a) => new { Cell = c, Actor = a })
					.RandomOrDefault(self.World.SharedRandom);
				if (cellInfo != null)
					newCell = cellInfo.Cell;
			}

			return newCell;
		}

		static bool IsMovable(Actor otherActor)
		{
			if (!otherActor.IsIdle)
				return false;

			var mobile = otherActor.TraitOrDefault<Mobile>();
			if (mobile == null || mobile.IsTraitDisabled || mobile.IsTraitPaused || mobile.IsImmovable)
				return false;

			return true;
		}

		public bool IsLeaving()
		{
			if (CurrentMovementTypes.HasFlag(MovementType.Horizontal))
				return true;

			if (CurrentMovementTypes.HasFlag(MovementType.Turn))
				return TurnToMove;

			return false;
		}

		public bool CanInteractWithGroundLayer(Actor self)
		{
			// TODO: Think about extending this to support arbitrary layer-layer checks
			// in a way that is compatible with the other IMove types.
			// This would then allow us to e.g. have units attack other units inside tunnels.
			if (ToCell.Layer == 0)
				return true;

			ICustomMovementLayer layer;
			if (self.World.GetCustomMovementLayers().TryGetValue(ToCell.Layer, out layer))
				return layer.InteractsWithDefaultLayer;

			return true;
		}

		#endregion

		#region IPositionable

		// Returns a valid sub-cell
		public SubCell GetValidSubCell(SubCell preferred = SubCell.Any)
		{
			// Try same sub-cell
			if (preferred == SubCell.Any)
				preferred = FromSubCell;

			// Fix sub-cell assignment
			if (Info.LocomotorInfo.SharesCell)
			{
				if (preferred <= SubCell.FullCell)
					return self.World.Map.Grid.DefaultSubCell;
			}
			else
			{
				if (preferred != SubCell.FullCell)
					return SubCell.FullCell;
			}

			return preferred;
		}

		// Sets the location (fromCell, toCell, FromSubCell, ToSubCell) and visual position (CenterPosition)
		public void SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any)
		{
			subCell = GetValidSubCell(subCell);
			SetLocation(cell, subCell, cell, subCell);

			var position = cell.Layer == 0 ? self.World.Map.CenterOfCell(cell) :
				self.World.GetCustomMovementLayers()[cell.Layer].CenterOfCell(cell);

			var subcellOffset = self.World.Map.Grid.OffsetOfSubCell(subCell);
			SetVisualPosition(self, position + subcellOffset);
			FinishedMoving(self);
		}

		// Sets the location (fromCell, toCell, FromSubCell, ToSubCell) and visual position (CenterPosition)
		public void SetPosition(Actor self, WPos pos)
		{
			var cell = self.World.Map.CellContaining(pos);
			SetLocation(cell, FromSubCell, cell, FromSubCell);
			SetVisualPosition(self, self.World.Map.CenterOfSubCell(cell, FromSubCell) + new WVec(0, 0, self.World.Map.DistanceAboveTerrain(pos).Length));
			FinishedMoving(self);
		}

		// Sets only the visual position (CenterPosition)
		public void SetVisualPosition(Actor self, WPos pos)
		{
			CenterPosition = pos;
			self.World.UpdateMaps(self, this);

			// The first time SetVisualPosition is called is in the constructor before creation, so we need a null check here as well
			if (notifyVisualPositionChanged == null)
				return;

			foreach (var n in notifyVisualPositionChanged)
				n.VisualPositionChanged(self, fromCell.Layer, toCell.Layer);
		}

		public bool IsLeavingCell(CPos location, SubCell subCell = SubCell.Any)
		{
			return ToCell != location && fromCell == location
				&& (subCell == SubCell.Any || FromSubCell == subCell || subCell == SubCell.FullCell || FromSubCell == SubCell.FullCell);
		}

		public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
		{
			return Locomotor.GetAvailableSubCell(self, a, check, preferredSubCell, ignoreActor);
		}

		public bool CanExistInCell(CPos cell)
		{
			return Locomotor.MovementCostForCell(cell) != short.MaxValue;
		}

		public bool CanEnterCell(CPos cell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
		{
			return Info.CanEnterCell(self.World, self, cell, ToSubCell, ignoreActor, check);
		}

		public bool CanStayInCell(CPos cell)
		{
			return Info.CanStayInCell(self.World, cell);
		}

		#endregion

		#region Local IPositionable-related

		// Sets only the location (fromCell, toCell, FromSubCell, ToSubCell)
		public void SetLocation(CPos from, SubCell fromSub, CPos to, SubCell toSub)
		{
			if (FromCell == from && ToCell == to && FromSubCell == fromSub && ToSubCell == toSub)
				return;

			RemoveInfluence();
			fromCell = from;
			toCell = to;
			FromSubCell = fromSub;
			ToSubCell = toSub;
			AddInfluence();
			IsBlocking = false;

			// Most custom layer conditions are added/removed when starting the transition between layers.
			if (toCell.Layer != fromCell.Layer)
				foreach (var n in notifyCustomLayerChanged)
					n.CustomLayerChanged(self, fromCell.Layer, toCell.Layer);
		}

		public void FinishedMoving(Actor self)
		{
			// Need to check both fromCell and toCell because FinishedMoving is called multiple times during the move
			if (fromCell.Layer == toCell.Layer)
				foreach (var n in notifyFinishedMoving)
					n.FinishedMoving(self, fromCell.Layer, toCell.Layer);

			// Only make actor crush if it is on the ground
			if (!self.IsAtGroundLevel())
				return;

			var actors = self.World.ActorMap.GetActorsAt(ToCell, ToSubCell).Where(a => a != self).ToList();
			if (!AnyCrushables(actors))
				return;

			var notifiers = actors.SelectMany(a => a.TraitsImplementing<INotifyCrushed>().Select(t => new TraitPair<INotifyCrushed>(a, t)));
			foreach (var notifyCrushed in notifiers)
				notifyCrushed.Trait.OnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes);
		}

		bool AnyCrushables(List<Actor> actors)
		{
			var crushables = actors.SelectMany(a => a.TraitsImplementing<ICrushable>().Select(t => new TraitPair<ICrushable>(a, t))).ToList();
			if (crushables.Count == 0)
				return false;

			foreach (var crushes in crushables)
				if (crushes.Trait.CrushableBy(crushes.Actor, self, Info.LocomotorInfo.Crushes))
					return true;

			return false;
		}

		public void AddInfluence()
		{
			if (self.IsInWorld)
				self.World.ActorMap.AddInfluence(self, this);
		}

		public void RemoveInfluence()
		{
			if (self.IsInWorld)
				self.World.ActorMap.RemoveInfluence(self, this);
		}

		#endregion

		#region IMove

		Activity WrapMove(Activity inner)
		{
			var moveWrapper = moveWrappers.FirstOrDefault(Exts.IsTraitEnabled);
			if (moveWrapper != null)
				return moveWrapper.WrapMove(inner);

			return inner;
		}

		public Activity MoveTo(CPos cell, int nearEnough = 0, Actor ignoreActor = null,
			bool evaluateNearestMovableCell = false, Color? targetLineColor = null)
		{
			return WrapMove(new Move(self, cell, WDist.FromCells(nearEnough), ignoreActor, evaluateNearestMovableCell, targetLineColor));
		}

		public Activity MoveWithinRange(Target target, WDist range,
			WPos? initialTargetPosition = null, Color? targetLineColor = null)
		{
			return WrapMove(new MoveWithinRange(self, target, WDist.Zero, range, initialTargetPosition, targetLineColor));
		}

		public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange,
			WPos? initialTargetPosition = null, Color? targetLineColor = null)
		{
			return WrapMove(new MoveWithinRange(self, target, minRange, maxRange, initialTargetPosition, targetLineColor));
		}

		public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange,
			WPos? initialTargetPosition = null, Color? targetLineColor = null)
		{
			return WrapMove(new Follow(self, target, minRange, maxRange, initialTargetPosition, targetLineColor));
		}

		public Activity ReturnToCell(Actor self)
		{
			return new ReturnToCellActivity(self);
		}

		public class ReturnToCellActivity : Activity
		{
			readonly Mobile mobile;
			readonly bool recalculateSubCell;

			CPos cell;
			SubCell subCell;
			WPos pos;
			int delay;

			public ReturnToCellActivity(Actor self, int delay = 0, bool recalculateSubCell = false)
			{
				mobile = self.Trait<Mobile>();
				IsInterruptible = false;
				this.delay = delay;
				this.recalculateSubCell = recalculateSubCell;
			}

			protected override void OnFirstRun(Actor self)
			{
				pos = self.CenterPosition;
				if (self.World.Map.DistanceAboveTerrain(pos) > WDist.Zero && self.TraitOrDefault<Parachutable>() != null)
					QueueChild(new Parachute(self));
			}

			public override bool Tick(Actor self)
			{
				pos = self.CenterPosition;
				cell = mobile.ToCell;
				subCell = mobile.ToSubCell;

				if (recalculateSubCell)
					subCell = mobile.Info.LocomotorInfo.SharesCell ? self.World.ActorMap.FreeSubCell(cell, subCell) : SubCell.FullCell;

				// TODO: solve/reduce cell is full problem
				if (subCell == SubCell.Invalid)
					subCell = self.World.Map.Grid.DefaultSubCell;

				// Reserve the exit cell
				mobile.SetPosition(self, cell, subCell);
				mobile.SetVisualPosition(self, pos);

				if (delay > 0)
					QueueChild(new Wait(delay));

				QueueChild(mobile.VisualMove(self, pos, self.World.Map.CenterOfSubCell(cell, subCell)));
				return true;
			}

			public override void Cancel(Actor self, bool keepQueue = false)
			{
				// If we are forbidden from stopping in this cell, use evaluateNearestMovableCell
				// to nudge us to the nearest cell that we can stop in.
				if (!mobile.CanStayInCell(cell))
					QueueChild(new Move(self, cell, WDist.Zero, null, true));

				base.Cancel(self, keepQueue);
			}
		}

		public Activity MoveToTarget(Actor self, Target target,
			WPos? initialTargetPosition = null, Color? targetLineColor = null)
		{
			if (target.Type == TargetType.Invalid)
				return null;

			return WrapMove(new MoveAdjacentTo(self, target, initialTargetPosition, targetLineColor));
		}

		public Activity MoveIntoTarget(Actor self, Target target)
		{
			if (target.Type == TargetType.Invalid)
				return null;

			// Activity cancels if the target moves by more than half a cell
			// to avoid problems with the cell grid
			return WrapMove(new VisualMoveIntoTarget(self, target, new WDist(512)));
		}

		public Activity VisualMove(Actor self, WPos fromPos, WPos toPos)
		{
			return WrapMove(VisualMove(self, fromPos, toPos, self.Location));
		}

		public int EstimatedMoveDuration(Actor self, WPos fromPos, WPos toPos)
		{
			var speed = MovementSpeedForCell(self, self.Location);
			return speed > 0 ? (toPos - fromPos).Length / speed : 0;
		}

		public CPos NearestMoveableCell(CPos target)
		{
			// Limit search to a radius of 10 tiles
			return NearestMoveableCell(target, 1, 10);
		}

		public bool CanEnterTargetNow(Actor self, Target target)
		{
			if (target.Type == TargetType.FrozenActor && !target.FrozenActor.IsValid)
				return false;

			return self.Location == self.World.Map.CellContaining(target.CenterPosition) || Util.AdjacentCells(self.World, target).Any(c => c == self.Location);
		}

		#endregion

		#region Local IMove-related

		public int MovementSpeedForCell(Actor self, CPos cell)
		{
			var index = cell.Layer == 0 ? self.World.Map.GetTerrainIndex(cell) :
				self.World.GetCustomMovementLayers()[cell.Layer].GetTerrainIndex(cell);

			if (index == byte.MaxValue)
				return 0;

			var terrainSpeed = Info.LocomotorInfo.TilesetTerrainInfo[self.World.Map.Rules.TileSet][index].Speed;
			if (terrainSpeed == 0)
				return 0;

			var modifiers = speedModifiers.Value.Append(terrainSpeed);

			return Util.ApplyPercentageModifiers(Info.Speed, modifiers);
		}

		public CPos NearestMoveableCell(CPos target, int minRange, int maxRange)
		{
			// HACK: This entire method is a hack, and needs to be replaced with
			// a proper path search that can account for movement layer transitions.
			// HACK: Work around code that blindly tries to move to cells in invalid movement layers.
			// This will need to change (by removing this method completely as above) before we can
			// properly support user-issued orders on to elevated bridges or other interactable custom layers
			if (target.Layer != 0)
				target = new CPos(target.X, target.Y);

			if (target == self.Location && CanStayInCell(target))
				return target;

			if (CanEnterCell(target, check: BlockedByActor.Immovable) && CanStayInCell(target))
				return target;

			foreach (var tile in self.World.Map.FindTilesInAnnulus(target, minRange, maxRange))
				if (CanEnterCell(tile, check: BlockedByActor.Immovable) && CanStayInCell(tile))
					return tile;

			// Couldn't find a cell
			return target;
		}

		public CPos NearestCell(CPos target, Func<CPos, bool> check, int minRange, int maxRange)
		{
			if (check(target))
				return target;

			foreach (var tile in self.World.Map.FindTilesInAnnulus(target, minRange, maxRange))
				if (check(tile))
					return tile;

			// Couldn't find a cell
			return target;
		}

		public void EnteringCell(Actor self)
		{
			// Only make actor crush if it is on the ground
			if (!self.IsAtGroundLevel())
				return;

			var actors = self.World.ActorMap.GetActorsAt(ToCell).Where(a => a != self).ToList();
			if (!AnyCrushables(actors))
				return;

			var notifiers = actors.SelectMany(a => a.TraitsImplementing<INotifyCrushed>().Select(t => new TraitPair<INotifyCrushed>(a, t)));
			foreach (var notifyCrushed in notifiers)
				notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes);
		}

		public Activity ScriptedMove(CPos cell) { return new Move(self, cell); }
		public Activity MoveTo(Func<BlockedByActor, List<CPos>> pathFunc) { return new Move(self, pathFunc); }

		Activity VisualMove(Actor self, WPos fromPos, WPos toPos, CPos cell)
		{
			var speed = MovementSpeedForCell(self, cell);
			var length = speed > 0 ? (toPos - fromPos).Length / speed : 0;

			var delta = toPos - fromPos;
			var facing = delta.HorizontalLengthSquared != 0 ? delta.Yaw.Facing : Facing;

			return new Drag(self, fromPos, toPos, length, facing);
		}

		CPos? ClosestGroundCell()
		{
			var above = new CPos(TopLeft.X, TopLeft.Y);
			if (CanEnterCell(above))
				return above;

			var pathFinder = self.World.WorldActor.Trait<IPathFinder>();
			List<CPos> path;
			using (var search = PathSearch.Search(self.World, Locomotor, self, BlockedByActor.All,
					loc => loc.Layer == 0 && CanEnterCell(loc))
				.FromPoint(self.Location))
				path = pathFinder.FindPath(search);

			if (path.Count > 0)
				return path[0];

			return null;
		}

		#endregion

		void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits)
		{
			if (!inits.Contains<DynamicFacingInit>() && !inits.Contains<FacingInit>())
				inits.Add(new DynamicFacingInit(() => facing));
		}

		void IDeathActorInitModifier.ModifyDeathActorInit(Actor self, TypeDictionary init)
		{
			init.Add(new FacingInit(facing));

			// Allows the husk to drag to its final position
			if (CanEnterCell(self.Location, self, BlockedByActor.Stationary))
				init.Add(new HuskSpeedInit(MovementSpeedForCell(self, self.Location)));
		}

		void INotifyBecomingIdle.OnBecomingIdle(Actor self)
		{
			if (self.Location.Layer == 0)
			{
				// Make sure that units aren't left idling in a transit-only cell
				// HACK: activities should be making sure that this can't happen in the first place!
				if (!Locomotor.CanStayInCell(self.Location))
					self.QueueActivity(MoveTo(self.Location, evaluateNearestMovableCell: true));
				return;
			}

			var cml = self.World.WorldActor.TraitsImplementing<ICustomMovementLayer>()
				.First(l => l.Index == self.Location.Layer);

			if (!cml.ReturnToGroundLayerOnIdle)
				return;

			var moveTo = ClosestGroundCell();
			if (moveTo != null)
				self.QueueActivity(MoveTo(moveTo.Value, 0));
		}

		void INotifyBlockingMove.OnNotifyBlockingMove(Actor self, Actor blocking)
		{
			if (!self.AppearsFriendlyTo(blocking))
				return;

			if (self.IsIdle)
			{
				Nudge(blocking);
				return;
			}

			IsBlocking = true;
		}

		public override IEnumerable<VariableObserver> GetVariableObservers()
		{
			foreach (var observer in base.GetVariableObservers())
				yield return observer;

			if (Info.RequireForceMoveCondition != null)
				yield return new VariableObserver(RequireForceMoveConditionChanged, Info.RequireForceMoveCondition.Variables);

			if (Info.ImmovableCondition != null)
				yield return new VariableObserver(ImmovableConditionChanged, Info.ImmovableCondition.Variables);
		}

		void RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
		{
			requireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions);
		}

		void ImmovableConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
		{
			var wasImmovable = IsImmovable;
			IsImmovable = Info.ImmovableCondition.Evaluate(conditions);
			if (wasImmovable != IsImmovable)
				self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
		}

		IEnumerable<IOrderTargeter> IIssueOrder.Orders
		{
			get
			{
				if (!IsTraitDisabled)
					yield return new MoveOrderTargeter(self, this);
			}
		}

		// Note: Returns a valid order even if the unit can't move to the target
		Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)
		{
			if (order is MoveOrderTargeter)
				return new Order("Move", self, target, queued);

			return null;
		}

		void IResolveOrder.ResolveOrder(Actor self, Order order)
		{
			if (IsTraitDisabled)
				return;

			if (order.OrderString == "Move")
			{
				var cell = self.World.Map.Clamp(this.self.World.Map.CellContaining(order.Target.CenterPosition));
				if (!Info.LocomotorInfo.MoveIntoShroud && !self.Owner.Shroud.IsExplored(cell))
					return;

				self.QueueActivity(order.Queued, WrapMove(new Move(self, cell, WDist.FromCells(8), null, true, Color.Green)));
				self.ShowTargetLines();
			}

			// TODO: This should only cancel activities queued by this trait
			else if (order.OrderString == "Stop")
				self.CancelActivity();
			else if (order.OrderString == "Scatter")
				Nudge(self);
		}

		string IOrderVoice.VoicePhraseForOrder(Actor self, Order order)
		{
			if (IsTraitDisabled)
				return null;

			switch (order.OrderString)
			{
				case "Move":
					if (!Info.LocomotorInfo.MoveIntoShroud && order.Target.Type != TargetType.Invalid)
					{
						var cell = self.World.Map.CellContaining(order.Target.CenterPosition);
						if (!self.Owner.Shroud.IsExplored(cell))
							return null;
					}

					return Info.Voice;
				case "Scatter":
				case "Stop":
					return Info.Voice;
				default:
					return null;
			}
		}

		Activity ICreationActivity.GetCreationActivity()
		{
			return returnToCellOnCreation ? new ReturnToCellActivity(self, creationActivityDelay, returnToCellOnCreationRecalculateSubCell) : null;
		}

		class MoveOrderTargeter : IOrderTargeter
		{
			readonly Mobile mobile;
			readonly LocomotorInfo locomotorInfo;
			readonly bool rejectMove;
			public bool TargetOverridesSelection(Actor self, Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers)
			{
				// Always prioritise orders over selecting other peoples actors or own actors that are already selected
				if (target.Type == TargetType.Actor && (target.Actor.Owner != self.Owner || self.World.Selection.Contains(target.Actor)))
					return true;

				return modifiers.HasModifier(TargetModifiers.ForceMove);
			}

			public MoveOrderTargeter(Actor self, Mobile unit)
			{
				mobile = unit;
				locomotorInfo = mobile.Info.LocomotorInfo;
				rejectMove = !self.AcceptsOrder("Move");
			}

			public string OrderID { get { return "Move"; } }
			public int OrderPriority { get { return 4; } }
			public bool IsQueued { get; protected set; }

			public bool CanTarget(Actor self, Target target, List<Actor> othersAtTarget, ref TargetModifiers modifiers, ref string cursor)
			{
				if (rejectMove || target.Type != TargetType.Terrain || (mobile.requireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove)))
					return false;

				var location = self.World.Map.CellContaining(target.CenterPosition);
				IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);

				var explored = self.Owner.Shroud.IsExplored(location);
				cursor = self.World.Map.Contains(location) ?
					(self.World.Map.GetTerrainInfo(location).CustomCursor ?? mobile.Info.Cursor) : mobile.Info.BlockedCursor;

				if (mobile.IsTraitPaused
					|| (!explored && !locomotorInfo.MoveIntoShroud)
					|| (explored && mobile.Locomotor.MovementCostForCell(location) == short.MaxValue))
					cursor = mobile.Info.BlockedCursor;

				return true;
			}
		}
	}
}
