#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.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Orders;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Traits
{
	public enum IdleBehaviorType
	{
		None,
		Land,
		ReturnToBase,
		LeaveMap,
		LeaveMapAtClosestEdge
	}

	public class AircraftInfo : PausableConditionalTraitInfo, IPositionableInfo, IFacingInfo, IMoveInfo, ICruiseAltitudeInfo,
		IActorPreviewInitInfo, IEditorActorOptions
	{
		[Desc("Behavior when aircraft becomes idle. Options are Land, ReturnToBase, LeaveMap, and None.",
			"'Land' will behave like 'None' (hover or circle) if a suitable landing site is not available.")]
		public readonly IdleBehaviorType IdleBehavior = IdleBehaviorType.None;

		public readonly WDist CruiseAltitude = new(1280);

		[Desc("Whether the aircraft can be repulsed.")]
		public readonly bool Repulsable = true;

		[Desc("The distance it tries to maintain from other aircraft if repulsable.")]
		public readonly WDist IdealSeparation = new(1706);

		[Desc("The speed at which the aircraft is repulsed from other aircraft. Specify -1 for normal movement speed.")]
		public readonly int RepulsionSpeed = -1;

		public readonly WAngle InitialFacing = WAngle.Zero;

		[Desc("Speed at which the actor turns.")]
		public readonly WAngle TurnSpeed = new(512);

		[Desc("Turn speed to apply when aircraft flies in circles while idle. Defaults to TurnSpeed if undefined.")]
		public readonly WAngle? IdleTurnSpeed = null;

		[Desc("When flying if the difference between current facing and desired facing is less than this value, don't turn. This prevents visual jitter.")]
		public readonly WAngle TurnDeadzone = new(2);

		[Desc("Maximum flight speed when cruising.")]
		public readonly int Speed = 1;

		[Desc("If non-negative, force the aircraft to move in circles at this speed when idle (a speed of 0 means don't move), ignoring CanHover.")]
		public readonly int IdleSpeed = -1;

		[Desc("Body pitch when flying forwards. Only relevant for voxel aircraft.")]
		public readonly WAngle Pitch = WAngle.Zero;

		[Desc("Pitch steps to apply each tick when starting/stopping.")]
		public readonly WAngle PitchSpeed = WAngle.Zero;

		[Desc("Body roll when turning. Only relevant for voxel aircraft.")]
		public readonly WAngle Roll = WAngle.Zero;

		[Desc("Body roll to apply when aircraft flies in circles while idle. Defaults to Roll if undefined. Only relevant for voxel aircraft.")]
		public readonly WAngle? IdleRoll = null;

		[Desc("Roll steps to apply each tick when turning.")]
		public readonly WAngle RollSpeed = WAngle.Zero;

		[Desc("Minimum altitude where this aircraft is considered airborne.")]
		public readonly int MinAirborneAltitude = 1;

		public readonly HashSet<string> LandableTerrainTypes = new();

		[Desc("Can the actor be ordered to move in to shroud?")]
		public readonly bool MoveIntoShroud = true;

		[Desc("e.g. crate, wall, infantry")]
		public readonly BitSet<CrushClass> Crushes = default;

		[Desc("Types of damage that are caused while crushing. Leave empty for no damage types.")]
		public readonly BitSet<DamageType> CrushDamageTypes = default;

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

		[Desc("Color to use for the target line for regular move orders.")]
		public readonly Color TargetLineColor = Color.Green;

		[GrantedConditionReference]
		[Desc("The condition to grant to self while airborne.")]
		public readonly string AirborneCondition = null;

		[GrantedConditionReference]
		[Desc("The condition to grant to self while at cruise altitude.")]
		public readonly string CruisingCondition = null;

		[Desc("Can the actor hover in place mid-air? If not, then the actor will have to remain in motion (circle around).")]
		public readonly bool CanHover = false;

		[Desc("Can the actor immediately change direction without turning first (doesn't need to fly in a curve)?")]
		public readonly bool CanSlide = false;

		[Desc("Does the actor land and take off vertically?")]
		public readonly bool VTOL = false;

		[Desc("Does this VTOL actor need to turn before landing (on terrain)?")]
		public readonly bool TurnToLand = false;

		[Desc("Does this actor automatically take off after resupplying?")]
		public readonly bool TakeOffOnResupply = false;

		[Desc("Does this actor automatically take off after creation?")]
		public readonly bool TakeOffOnCreation = true;

		[Desc("Can this actor be given an explicit land order using the force-move modifier?")]
		public readonly bool CanForceLand = true;

		[Desc("Altitude at which the aircraft considers itself landed.")]
		public readonly WDist LandAltitude = WDist.Zero;

		[Desc("Range to search for an alternative landing location if the ordered cell is blocked.")]
		public readonly WDist LandRange = WDist.FromCells(5);

		[Desc("How fast this actor ascends or descends during horizontal movement.")]
		public readonly WAngle MaximumPitch = WAngle.FromDegrees(10);

		[Desc("How fast this actor ascends or descends when moving vertically only (vertical take off/landing or hovering towards CruiseAltitude).")]
		public readonly WDist AltitudeVelocity = new(43);

		[Desc("Sounds to play when the actor is taking off.")]
		public readonly string[] TakeoffSounds = Array.Empty<string>();

		[Desc("Sounds to play when the actor is landing.")]
		public readonly string[] LandingSounds = Array.Empty<string>();

		[Desc("The distance of the resupply base that the aircraft will wait for its turn.")]
		public readonly WDist WaitDistanceFromResupplyBase = new(3072);

		[Desc("The number of ticks that a airplane will wait to make a new search for an available airport.")]
		public readonly int NumberOfTicksToVerifyAvailableAirport = 150;

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

		[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;

		[CursorReference]
		[Desc("Cursor to display when a move order can be issued at target location.")]
		public readonly string Cursor = "move";

		[CursorReference]
		[Desc("Cursor to display when a move order cannot be issued at target location.")]
		public readonly string BlockedCursor = "move-blocked";

		[CursorReference]
		[Desc("Cursor to display when able to land at target building.")]
		public readonly string EnterCursor = "enter";

		[CursorReference]
		[Desc("Cursor to display when unable to land at target building.")]
		public readonly string EnterBlockedCursor = "enter-blocked";

		public WAngle GetInitialFacing() { return InitialFacing; }
		public WDist GetCruiseAltitude() { return CruiseAltitude; }
		public Color GetTargetLineColor() { return TargetLineColor; }

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

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

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

		bool IOccupySpaceInfo.SharesCell => false;

		// Used to determine if an aircraft can spawn landed
		public bool CanEnterCell(World world, Actor self, CPos cell,
			SubCell subCell = SubCell.FullCell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
		{
			if (!world.Map.Contains(cell))
				return false;

			var type = world.Map.GetTerrainInfo(cell).Type;
			if (!LandableTerrainTypes.Contains(type))
				return false;

			if (check == BlockedByActor.None)
				return true;

			// Since aircraft don't share cells, we don't pass the subCell parameter
			return !world.ActorMap.GetActorsAt(cell).Any(x => x != ignoreActor);
		}

		IEnumerable<EditorActorOption> IEditorActorOptions.ActorOptions(ActorInfo ai, World world)
		{
			yield return new EditorActorSlider("Facing", EditorFacingDisplayOrder, 0, 1023, 8,
				actor =>
				{
					var init = actor.GetInitOrDefault<FacingInit>(this);
					return (init != null ? init.Value : InitialFacing).Angle;
				},
				(actor, value) => actor.ReplaceInit(new FacingInit(new WAngle((int)value))));
		}
	}

	public class Aircraft : PausableConditionalTrait<AircraftInfo>, ITick, ISync, IFacing, IPositionable, IMove,
		INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyActorDisposing, INotifyBecomingIdle, ICreationActivity,
		IActorPreviewInitModifier, IDeathActorInitModifier, IIssueDeployOrder, IIssueOrder, IResolveOrder, IOrderVoice
	{
		readonly Actor self;

		Repairable repairable;
		Rearmable rearmable;
		IAircraftCenterPositionOffset[] positionOffsets;
		IDisposable reservation;
		IEnumerable<int> speedModifiers;
		INotifyMoving[] notifyMoving;
		INotifyCenterPositionChanged[] notifyCenterPositionChanged;
		IOverrideAircraftLanding overrideAircraftLanding;

		[Sync]
		public WAngle Facing
		{
			get => Orientation.Yaw;
			set => Orientation = Orientation.WithYaw(value);
		}

		public WAngle Pitch
		{
			get => Orientation.Pitch;
			set => Orientation = Orientation.WithPitch(value);
		}

		public WAngle Roll
		{
			get => Orientation.Roll;
			set => Orientation = Orientation.WithRoll(value);
		}

		public WRot Orientation { get; private set; }

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

		public CPos TopLeft => self.World.Map.CellContaining(CenterPosition);
		public WAngle TurnSpeed => IsTraitDisabled || IsTraitPaused ? WAngle.Zero : Info.TurnSpeed;
		public WAngle? IdleTurnSpeed => IsTraitDisabled || IsTraitPaused ? null : Info.IdleTurnSpeed;

		public WAngle GetTurnSpeed(bool isIdleTurn)
		{
			// A MovementSpeed of zero indicates either a speed modifier of zero percent or that the trait is paused or disabled.
			// Bail early in that case.
			if ((isIdleTurn && IdleMovementSpeed == 0) || MovementSpeed == 0)
				return WAngle.Zero;

			var turnSpeed = isIdleTurn ? IdleTurnSpeed ?? TurnSpeed : TurnSpeed;

			return new WAngle(Util.ApplyPercentageModifiers(turnSpeed.Angle, speedModifiers).Clamp(1, 1024));
		}

		public Actor ReservedActor { get; private set; }
		public bool MayYieldReservation { get; private set; }
		public bool ForceLanding { get; private set; }

		(CPos, SubCell)[] landingCells = Array.Empty<(CPos, SubCell)>();
		public bool RequireForceMove;

		readonly int creationActivityDelay;
		readonly bool creationByMap;
		readonly CPos[] creationRallyPoint;

		bool notify = true;

		public static WPos GroundPosition(Actor self)
		{
			return self.CenterPosition - new WVec(WDist.Zero, WDist.Zero, self.World.Map.DistanceAboveTerrain(self.CenterPosition));
		}

		public bool AtLandAltitude => self.World.Map.DistanceAboveTerrain(GetPosition()) == LandAltitude;

		bool airborne;
		bool cruising;
		int airborneToken = Actor.InvalidConditionToken;
		int cruisingToken = Actor.InvalidConditionToken;

		MovementType movementTypes;
		WPos cachedPosition;
		WAngle cachedFacing;

		public Aircraft(ActorInitializer init, AircraftInfo info)
			: base(info)
		{
			self = init.Self;

			var locationInit = init.GetOrDefault<LocationInit>();
			var centerPositionInit = init.GetOrDefault<CenterPositionInit>();
			if (locationInit != null || centerPositionInit != null)
			{
				var pos = centerPositionInit?.Value ?? self.World.Map.CenterOfCell(locationInit.Value);
				creationByMap = init.Contains<SpawnedByMapInit>();
				SetPosition(self, pos);
			}

			Facing = init.GetValue<FacingInit, WAngle>(Info.InitialFacing);
			creationActivityDelay = init.GetValue<CreationActivityDelayInit, int>(0);
			creationRallyPoint = init.GetOrDefault<RallyPointInit>()?.Value;
		}

		public WDist LandAltitude
		{
			get
			{
				var alt = Info.LandAltitude;
				foreach (var offset in positionOffsets)
					alt -= new WDist(offset.PositionOffset.Z);

				return alt;
			}
		}

		public WPos GetPosition()
		{
			var pos = self.CenterPosition;
			foreach (var offset in positionOffsets)
				pos += offset.PositionOffset;

			return pos;
		}

		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);
		}

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

		protected override void Created(Actor self)
		{
			repairable = self.TraitOrDefault<Repairable>();
			rearmable = self.TraitOrDefault<Rearmable>();
			speedModifiers = self.TraitsImplementing<ISpeedModifier>().ToArray().Select(sm => sm.GetSpeedModifier());
			cachedPosition = self.CenterPosition;
			notifyMoving = self.TraitsImplementing<INotifyMoving>().ToArray();
			positionOffsets = self.TraitsImplementing<IAircraftCenterPositionOffset>().ToArray();
			overrideAircraftLanding = self.TraitOrDefault<IOverrideAircraftLanding>();
			notifyCenterPositionChanged = self.TraitsImplementing<INotifyCenterPositionChanged>().ToArray();
			base.Created(self);
		}

		void INotifyAddedToWorld.AddedToWorld(Actor self)
		{
			AddedToWorld(self);
		}

		protected virtual void AddedToWorld(Actor self)
		{
			self.World.AddToMaps(self, this);

			var altitude = self.World.Map.DistanceAboveTerrain(CenterPosition);
			if (altitude.Length >= Info.MinAirborneAltitude)
				OnAirborneAltitudeReached();
			if (altitude == Info.CruiseAltitude)
				OnCruisingAltitudeReached();
		}

		void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
		{
			RemovedFromWorld(self);
		}

		protected virtual void RemovedFromWorld(Actor self)
		{
			UnReserve();
			self.World.RemoveFromMaps(self, this);

			OnCruisingAltitudeLeft();
			OnAirborneAltitudeLeft();
		}

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

		protected virtual void Tick(Actor self)
		{
			// Add land activity if Aircraft trait is paused and the actor can land at the current location.
			if (!ForceLanding && IsTraitPaused && airborne && CanLand(self.Location)
				&& !((self.CurrentActivity is Land) || self.CurrentActivity is Turn))
			{
				self.QueueActivity(false, new Land(self));
				ForceLanding = true;
			}

			// Add takeoff activity if Aircraft trait is not paused and the actor should not land when idle.
			if (ForceLanding && !IsTraitPaused && !cruising && self.CurrentActivity is not TakeOff)
			{
				ForceLanding = false;

				if (Info.IdleBehavior != IdleBehaviorType.Land)
					self.QueueActivity(false, new TakeOff(self));
			}

			var oldCachedFacing = cachedFacing;
			cachedFacing = Facing;

			var oldCachedPosition = cachedPosition;
			cachedPosition = self.CenterPosition;

			var newMovementTypes = MovementType.None;
			if (oldCachedFacing != Facing)
				newMovementTypes |= MovementType.Turn;

			if ((oldCachedPosition - cachedPosition).HorizontalLengthSquared != 0)
				newMovementTypes |= MovementType.Horizontal;

			if ((oldCachedPosition - cachedPosition).VerticalLengthSquared != 0)
				newMovementTypes |= MovementType.Vertical;

			CurrentMovementTypes = newMovementTypes;

			if (!CurrentMovementTypes.HasMovementType(MovementType.Horizontal))
			{
				if (Info.Roll != WAngle.Zero && Roll != WAngle.Zero)
					Roll = Util.TickFacing(Roll, WAngle.Zero, Info.RollSpeed);

				if (Info.Pitch != WAngle.Zero && Pitch != WAngle.Zero)
					Pitch = Util.TickFacing(Pitch, WAngle.Zero, Info.PitchSpeed);
			}

			Repulse();
		}

		public void Repulse()
		{
			var repulsionForce = GetRepulsionForce();
			if (repulsionForce == WVec.Zero)
				return;

			var speed = Info.RepulsionSpeed != -1 ? Info.RepulsionSpeed : MovementSpeed;

			// HACK: Prevent updating visibility twice per tick. We really shouldn't be
			// moving twice in a tick in the first place.
			notify = false;
			SetPosition(self, CenterPosition + FlyStep(speed, repulsionForce.Yaw));
			notify = true;
		}

		public virtual WVec GetRepulsionForce()
		{
			if (!Info.Repulsable)
				return WVec.Zero;

			if (reservation != null)
			{
				var distanceFromReservationActor = (ReservedActor.CenterPosition - self.CenterPosition).HorizontalLength;
				if (distanceFromReservationActor < Info.WaitDistanceFromResupplyBase.Length)
					return WVec.Zero;
			}

			// Repulsion only applies when we're flying at CruiseAltitude!
			if (!cruising)
				return WVec.Zero;

			// PERF: Avoid LINQ.
			var repulsionForce = WVec.Zero;
			foreach (var actor in self.World.FindActorsInCircle(self.CenterPosition, Info.IdealSeparation))
			{
				if (actor.IsDead)
					continue;

				var ai = actor.Info.TraitInfoOrDefault<AircraftInfo>();
				if (ai == null || !ai.Repulsable || ai.CruiseAltitude != Info.CruiseAltitude)
					continue;

				repulsionForce += GetRepulsionForce(actor);
			}

			// Actors outside the map bounds receive an extra nudge towards the center of the map
			if (!self.World.Map.Contains(self.Location))
			{
				// The map bounds are in projected coordinates, which is technically wrong for this,
				// but we avoid the issues in practice by guessing the middle of the map instead of the edge
				var center = WPos.Lerp(self.World.Map.ProjectedTopLeft, self.World.Map.ProjectedBottomRight, 1, 2);
				repulsionForce += new WVec(0, 1024, 0).Rotate(WRot.FromYaw((self.CenterPosition - center).Yaw));
			}

			if (Info.CanSlide)
				return repulsionForce;

			// Non-hovering actors mush always keep moving forward, so they need extra calculations.
			var currentDir = FlyStep(Facing);
			var length = currentDir.HorizontalLength * repulsionForce.HorizontalLength;
			if (length == 0)
				return WVec.Zero;

			var dot = WVec.Dot(currentDir, repulsionForce) / length;

			// avoid stalling the plane
			return dot >= 0 ? repulsionForce : WVec.Zero;
		}

		public WVec GetRepulsionForce(Actor other)
		{
			if (self == other || other.CenterPosition.Z < self.CenterPosition.Z)
				return WVec.Zero;

			var d = self.CenterPosition - other.CenterPosition;
			var distSq = d.HorizontalLengthSquared;
			if (distSq > Info.IdealSeparation.LengthSquared)
				return WVec.Zero;

			if (distSq < 1)
			{
				var yaw = self.World.SharedRandom.Next(0, 1023);
				var rot = new WRot(WAngle.Zero, WAngle.Zero, new WAngle(yaw));
				return new WVec(1024, 0, 0).Rotate(rot);
			}

			return d * 1024 * 8 / (int)distSq;
		}

		public Actor GetActorBelow()
		{
			// Map.DistanceAboveTerrain(WPos pos) is called directly because Aircraft is an IPositionable trait
			// and all calls occur in Tick methods.
			if (self.World.Map.DistanceAboveTerrain(CenterPosition) != LandAltitude)
				return null; // Not on the resupplier.

			return self.World.ActorMap.GetActorsAt(self.Location)
				.FirstOrDefault(a => a.Info.HasTraitInfo<ReservableInfo>());
		}

		public void MakeReservation(Actor target)
		{
			UnReserve();
			var reservable = target.TraitOrDefault<Reservable>();
			if (reservable != null)
			{
				reservation = reservable.Reserve(target, self, this);
				ReservedActor = target;
			}
		}

		public void AllowYieldingReservation()
		{
			if (reservation == null)
				return;

			MayYieldReservation = true;
		}

		public void UnReserve()
		{
			if (reservation == null)
				return;

			reservation.Dispose();
			reservation = null;
			ReservedActor = null;
			MayYieldReservation = false;
		}

		bool AircraftCanEnter(Actor a, TargetModifiers modifiers)
		{
			if (RequireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove))
				return false;

			return AircraftCanEnter(a);
		}

		bool AircraftCanEnter(Actor a)
		{
			if (self.AppearsHostileTo(a))
				return false;

			var canRearmAtActor = rearmable != null && rearmable.Info.RearmActors.Contains(a.Info.Name);
			var canRepairAtActor = repairable != null && repairable.Info.RepairActors.Contains(a.Info.Name);

			return canRearmAtActor || canRepairAtActor;
		}

		bool AircraftCanResupplyAt(Actor a, bool allowedToForceEnter = false)
		{
			if (self.AppearsHostileTo(a))
				return false;

			var canRearmAtActor = rearmable != null && rearmable.Info.RearmActors.Contains(a.Info.Name);
			var canRepairAtActor = repairable != null && repairable.Info.RepairActors.Contains(a.Info.Name);

			var allowedToEnterRearmer = canRearmAtActor && (allowedToForceEnter || rearmable.RearmableAmmoPools.Any(p => !p.HasFullAmmo));
			var allowedToEnterRepairer = canRepairAtActor && (allowedToForceEnter || self.GetDamageState() != DamageState.Undamaged);

			return allowedToEnterRearmer || allowedToEnterRepairer;
		}

		public int MovementSpeed => !IsTraitDisabled && !IsTraitPaused ? Util.ApplyPercentageModifiers(Info.Speed, speedModifiers) : 0;
		public int IdleMovementSpeed => Info.IdleSpeed < 0 ? MovementSpeed :
			!IsTraitDisabled && !IsTraitPaused ? Util.ApplyPercentageModifiers(Info.IdleSpeed, speedModifiers) : 0;

		public (CPos Cell, SubCell SubCell)[] OccupiedCells()
		{
			return landingCells;
		}

		public WVec FlyStep(WAngle facing)
		{
			return FlyStep(MovementSpeed, facing);
		}

		public WVec FlyStep(int speed, WAngle facing)
		{
			var dir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(facing));
			return speed * dir / 1024;
		}

		public CPos? FindLandingLocation(CPos targetCell, WDist maxSearchDistance)
		{
			// The easy case
			if (CanLand(targetCell, blockedByMobile: false))
				return targetCell;

			var cellRange = (maxSearchDistance.Length + 1023) / 1024;
			var centerPosition = self.World.Map.CenterOfCell(targetCell);
			foreach (var c in self.World.Map.FindTilesInCircle(targetCell, cellRange))
			{
				if (!CanLand(c, blockedByMobile: false))
					continue;

				var delta = self.World.Map.CenterOfCell(c) - centerPosition;
				if (delta.LengthSquared < maxSearchDistance.LengthSquared)
					return c;
			}

			return null;
		}

		public bool CanLand(IEnumerable<CPos> cells, Actor dockingActor = null, bool blockedByMobile = true)
		{
			foreach (var c in cells)
				if (!CanLand(c, dockingActor, blockedByMobile))
					return false;

			return true;
		}

		public bool CanLand(CPos cell, Actor dockingActor = null, bool blockedByMobile = true)
		{
			if (!self.World.Map.Contains(cell))
				return false;

			foreach (var otherActor in self.World.ActorMap.GetActorsAt(cell))
				if (IsBlockedBy(self, otherActor, dockingActor, blockedByMobile))
					return false;

			// Terrain type is ignored when docking with an actor
			if (dockingActor != null)
				return true;

			var landableTerrain = overrideAircraftLanding != null ? overrideAircraftLanding.LandableTerrainTypes : Info.LandableTerrainTypes;
			return landableTerrain.Contains(self.World.Map.GetTerrainInfo(cell).Type);
		}

		bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, bool blockedByMobile = true)
		{
			// We are not blocked by the actor we are ignoring.
			if (otherActor == self || otherActor == ignoreActor)
				return false;

			// We are not blocked by actors we can nudge out of the way
			// TODO: Generalize blocker checks and handling here and in Locomotor
			if (!blockedByMobile && self.Owner.RelationshipWith(otherActor.Owner) == PlayerRelationship.Ally &&
				otherActor.TraitOrDefault<Mobile>() != null && otherActor.CurrentActivity == null)
				return false;

			// PERF: Only perform ITemporaryBlocker trait look-up if mod/map rules contain any actors that are temporary blockers
			if (self.World.RulesContainTemporaryBlocker)
			{
				// If there is a temporary blocker in our path, but we can remove it, we are not blocked.
				var temporaryBlocker = otherActor.TraitOrDefault<ITemporaryBlocker>();
				if (temporaryBlocker != null && temporaryBlocker.CanRemoveBlockage(otherActor, self))
					return false;
			}

			// If we cannot crush the other actor in our way, we are blocked.
			if (Info.Crushes.IsEmpty)
				return true;

			// If the other actor in our way cannot be crushed, we are blocked.
			// PERF: Avoid LINQ.
			foreach (var crushable in otherActor.Crushables)
				if (crushable.CrushableBy(otherActor, self, Info.Crushes))
					return false;

			return true;
		}

		public bool CanRearmAt(Actor host)
		{
			return rearmable != null && rearmable.Info.RearmActors.Contains(host.Info.Name) && rearmable.RearmableAmmoPools.Any(p => !p.HasFullAmmo);
		}

		public bool CanRepairAt(Actor host)
		{
			return repairable != null && repairable.Info.RepairActors.Contains(host.Info.Name) && self.GetDamageState() != DamageState.Undamaged;
		}

		public void ModifyDeathActorInit(Actor self, TypeDictionary init)
		{
			init.Add(new FacingInit(Facing));
		}

		void INotifyBecomingIdle.OnBecomingIdle(Actor self)
		{
			OnBecomingIdle(self);
		}

		protected virtual void OnBecomingIdle(Actor self)
		{
			if (Info.IdleBehavior == IdleBehaviorType.LeaveMap)
			{
				self.QueueActivity(new FlyOffMap(self));
				self.QueueActivity(new RemoveSelf());
			}
			else if (Info.IdleBehavior == IdleBehaviorType.LeaveMapAtClosestEdge)
			{
				var edgeCell = self.World.Map.ChooseClosestEdgeCell(self.Location);
				self.QueueActivity(new FlyOffMap(self, Target.FromCell(self.World, edgeCell)));
				self.QueueActivity(new RemoveSelf());
			}
			else if (Info.IdleBehavior == IdleBehaviorType.ReturnToBase && GetActorBelow() == null)
				self.QueueActivity(new ReturnToBase(self, null, !Info.TakeOffOnResupply));
			else
			{
				var dat = self.World.Map.DistanceAboveTerrain(CenterPosition);
				if (dat == LandAltitude)
				{
					if (!CanLand(self.Location) && ReservedActor == null)
						self.QueueActivity(new TakeOff(self));

					// All remaining idle behaviors rely on not being at LandAltitude, so unconditionally return
					return;
				}

				if (Info.IdleBehavior == IdleBehaviorType.Land && Info.LandableTerrainTypes.Count > 0)
					self.QueueActivity(new Land(self));
				else
					self.QueueActivity(new FlyIdle(self));
			}
		}

		#region Implement IPositionable

		public bool CanExistInCell(CPos cell) { return true; }
		public bool IsLeavingCell(CPos location, SubCell subCell = SubCell.Any) { return false; } // TODO: Handle landing
		public bool CanEnterCell(CPos cell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All) { return true; }
		public SubCell GetValidSubCell(SubCell preferred) { return SubCell.Invalid; }
		public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
		{
			// Does not use any subcell
			return SubCell.Invalid;
		}

		public void SetCenterPosition(Actor self, WPos pos) { SetPosition(self, pos); }

		// Changes position, but not altitude
		public void SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any)
		{
			SetPosition(self, self.World.Map.CenterOfCell(cell) + new WVec(0, 0, CenterPosition.Z));
		}

		public void SetPosition(Actor self, WPos pos)
		{
			CenterPosition = pos;

			if (!self.IsInWorld)
				return;

			var altitude = self.World.Map.DistanceAboveTerrain(CenterPosition);

			// LandingCells define OccupiedCells, so we need to keep current position with LandindCells in sync.
			// Though we don't want to update LandingCells when the unit is airborne, as when non-VTOL units reserve
			// their landing position it is expected for their landing cell to not match their current position.
			if (HasInfluence() && altitude.Length <= Info.MinAirborneAltitude)
			{
				var currentPos = new[] { (TopLeft, SubCell.FullCell) };
				if (landingCells.SequenceEqual(currentPos))
				{
					self.World.ActorMap.RemoveInfluence(self, this);
					landingCells = currentPos;
					self.World.ActorMap.AddInfluence(self, this);
				}
			}

			self.World.UpdateMaps(self, this);

			var isAirborne = altitude.Length >= Info.MinAirborneAltitude;
			if (isAirborne && !airborne)
				OnAirborneAltitudeReached();
			else if (!isAirborne && airborne)
				OnAirborneAltitudeLeft();

			var isCruising = altitude == Info.CruiseAltitude;
			if (isCruising && !cruising)
				OnCruisingAltitudeReached();
			else if (!isCruising && cruising)
				OnCruisingAltitudeLeft();

			// NB: This can be called from the constructor before notifyCenterPositionChanged is assigned.
			if (notify && notifyCenterPositionChanged != null)
				foreach (var n in notifyCenterPositionChanged)
					n.CenterPositionChanged(self, 0, 0);

			FinishedMoving(self);
		}

		public void FinishedMoving(Actor self)
		{
			// Only crush actors on having landed
			if (!self.IsAtGroundLevel())
				return;

			CrushAction(self, (notifyCrushed) => notifyCrushed.OnCrush);
		}

		public void EnteringCell(Actor self)
		{
			CrushAction(self, (notifyCrushed) => notifyCrushed.WarnCrush);
		}

		void CrushAction(Actor self, Func<INotifyCrushed, Action<Actor, Actor, BitSet<CrushClass>>> action)
		{
			var crushables = self.World.ActorMap.GetActorsAt(TopLeft).Where(a => a != self)
				.SelectMany(a => a.Crushables.Select(t => new TraitPair<ICrushable>(a, t)));

			// Only crush actors that are on the ground level
			foreach (var crushable in crushables)
				if (crushable.Trait.CrushableBy(crushable.Actor, self, Info.Crushes) && crushable.Actor.IsAtGroundLevel())
					foreach (var notifyCrushed in crushable.Actor.TraitsImplementing<INotifyCrushed>())
						action(notifyCrushed)(crushable.Actor, self, Info.Crushes);
		}

		public void AddInfluence((CPos, SubCell)[] landingCells)
		{
			if (HasInfluence())
				throw new InvalidOperationException(
					$"Cannot {nameof(AddInfluence)} until previous influence is removed with {nameof(RemoveInfluence)}");

			this.landingCells = landingCells;
			if (self.IsInWorld)
				self.World.ActorMap.AddInfluence(self, this);
		}

		public void AddInfluence(CPos landingCell)
		{
			AddInfluence(new[] { (landingCell, SubCell.FullCell) });
		}

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

			landingCells = Array.Empty<(CPos, SubCell)>();
		}

		public bool HasInfluence()
		{
			return landingCells.Length > 0;
		}

		#endregion

		#region Implement IMove

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

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

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

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

		public Activity ReturnToCell(Actor self) { return null; }

		public Activity MoveToTarget(Actor self, in Target target,
			WPos? initialTargetPosition = null, Color? targetLineColor = null)
		{
			return new Fly(self, target, initialTargetPosition, targetLineColor);
		}

		public Activity MoveIntoTarget(Actor self, in Target target)
		{
			return new Land(self, target);
		}

		public Activity MoveOntoTarget(Actor self, in Target target, in WVec offset, WAngle? facing, Color? targetLineColor = null)
		{
			return new Land(self, target, offset, facing, targetLineColor);
		}

		public Activity LocalMove(Actor self, WPos fromPos, WPos toPos)
		{
			// TODO: Ignore repulsion when moving
			var activities = new CallFunc(() => SetCenterPosition(self, fromPos));
			activities.Queue(new Fly(self, Target.FromPos(toPos)));
			return activities;
		}

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

		public CPos NearestMoveableCell(CPos cell) { return cell; }

		public MovementType CurrentMovementTypes
		{
			get => movementTypes;

			set
			{
				var oldValue = movementTypes;
				movementTypes = value;
				if (value != oldValue)
					foreach (var n in notifyMoving)
						n.MovementTypeChanged(self, value);
			}
		}

		public bool CanEnterTargetNow(Actor self, in Target target)
		{
			// Lambdas can't use 'in' variables, so capture a copy for later
			var targetActor = target;
			if (target.Positions.Any(p => self.World.ActorMap.GetActorsAt(self.World.Map.CellContaining(p)).Any(a => a != self && a != targetActor.Actor)))
				return false;

			MakeReservation(target.Actor);
			return true;
		}

		#endregion

		#region Implement order interfaces

		public IEnumerable<IOrderTargeter> Orders
		{
			get
			{
				yield return new EnterAlliedActorTargeter<BuildingInfo>(
					"ForceEnter",
					6,
					Info.EnterCursor,
					Info.EnterBlockedCursor,
					(target, modifiers) => Info.CanForceLand && modifiers.HasModifier(TargetModifiers.ForceMove) && AircraftCanEnter(target),
					target => Reservable.IsAvailableFor(target, self) && AircraftCanResupplyAt(target, true));

				yield return new EnterAlliedActorTargeter<BuildingInfo>(
					"Enter",
					5,
					Info.EnterCursor,
					Info.EnterBlockedCursor,
					AircraftCanEnter,
					target => Reservable.IsAvailableFor(target, self) && AircraftCanResupplyAt(target, !Info.TakeOffOnResupply));

				yield return new AircraftMoveOrderTargeter(this);
			}
		}

		public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued)
		{
			if (!IsTraitDisabled &&
				(order.OrderID == "Enter" || order.OrderID == "Move" || order.OrderID == "Land" || order.OrderID == "ForceEnter"))
				return new Order(order.OrderID, self, target, queued);

			return null;
		}

		Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued)
		{
			if (IsTraitDisabled || rearmable == null || rearmable.Info.RearmActors.Count == 0)
				return null;

			return new Order("ReturnToBase", self, queued);
		}

		bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) { return rearmable != null && rearmable.Info.RearmActors.Count > 0; }

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

			switch (order.OrderString)
			{
				case "Land":
				case "Move":
					if (!Info.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 "Enter":
				case "ForceEnter":
				case "Stop":
				case "Scatter":
					return Info.Voice;
				case "ReturnToBase":
					return rearmable != null && rearmable.Info.RearmActors.Count > 0 ? Info.Voice : null;
				default: return null;
			}
		}

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

			var orderString = order.OrderString;
			if (orderString == "Move")
			{
				if (!order.Target.IsValidFor(self))
					return;

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

				if (!order.Queued)
					UnReserve();

				var target = Target.FromCell(self.World, cell);

				// TODO: this should scale with unit selection group size.
				self.QueueActivity(order.Queued, new Fly(self, target, WDist.FromCells(8), targetLineColor: Info.TargetLineColor));
				self.ShowTargetLines();
			}
			else if (orderString == "Land")
			{
				if (!order.Target.IsValidFor(self))
					return;

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

				if (!order.Queued)
					UnReserve();

				var target = Target.FromCell(self.World, cell);

				self.QueueActivity(order.Queued, new Land(self, target, targetLineColor: Info.TargetLineColor));
				self.ShowTargetLines();
			}
			else if (orderString == "Enter" || orderString == "ForceEnter" || orderString == "Repair")
			{
				// Enter, ForceEnter and Repair orders are only valid for own/allied actors,
				// which are guaranteed to never be frozen.
				if (order.Target.Type != TargetType.Actor)
					return;

				var targetActor = order.Target.Actor;
				var isForceEnter = orderString == "ForceEnter";
				var canResupplyAt = AircraftCanResupplyAt(targetActor, isForceEnter || !Info.TakeOffOnResupply);

				// This is what the order targeter checks to display the correct cursor, so we need to make sure
				// the behavior matches the cursor if the player clicks despite a "blocked" cursor.
				if (!canResupplyAt || !Reservable.IsAvailableFor(targetActor, self))
					return;

				if (!order.Queued)
					UnReserve();

				// Aircraft with TakeOffOnResupply would immediately take off again, so there's no point in automatically forcing
				// them to land on a resupplier. For aircraft without it, it makes more sense to land than to idle above a
				// free resupplier.
				var forceLand = isForceEnter || !Info.TakeOffOnResupply;
				self.QueueActivity(order.Queued, new ReturnToBase(self, targetActor, forceLand));
				self.ShowTargetLines();
			}
			else if (orderString == "Stop")
			{
				// We don't want the Stop order to cancel a running Resupply activity.
				// Resupply is always either the main activity or a child of ReturnToBase.
				if (self.CurrentActivity is Resupply ||
					(self.CurrentActivity is ReturnToBase && GetActorBelow() != null))
					return;

				self.CancelActivity();
				UnReserve();
			}
			else if (orderString == "ReturnToBase")
			{
				// Do nothing if not rearmable and don't restart activity every time deploy hotkey is triggered
				if (rearmable == null || rearmable.Info.RearmActors.Count == 0 || self.CurrentActivity is ReturnToBase || GetActorBelow() != null)
					return;

				if (!order.Queued)
					UnReserve();

				// Aircraft with TakeOffOnResupply would immediately take off again, so there's no point in forcing them to land
				// on a resupplier. For aircraft without it, it makes more sense to land than to idle above a free resupplier.
				self.QueueActivity(order.Queued, new ReturnToBase(self, null, !Info.TakeOffOnResupply));
				self.ShowTargetLines();
			}
			else if (orderString == "Scatter")
			{
				self.QueueActivity(order.Queued, new Nudge(self));
				self.ShowTargetLines();
			}
		}

		#endregion

		#region Airborne conditions

		void OnAirborneAltitudeReached()
		{
			if (airborne)
				return;

			airborne = true;
			if (airborneToken == Actor.InvalidConditionToken)
				airborneToken = self.GrantCondition(Info.AirborneCondition);
		}

		void OnAirborneAltitudeLeft()
		{
			if (!airborne)
				return;

			airborne = false;
			if (airborneToken != Actor.InvalidConditionToken)
				airborneToken = self.RevokeCondition(airborneToken);
		}

		#endregion

		#region Cruising conditions

		void OnCruisingAltitudeReached()
		{
			if (cruising)
				return;

			cruising = true;
			if (cruisingToken == Actor.InvalidConditionToken)
				cruisingToken = self.GrantCondition(Info.CruisingCondition);
		}

		void OnCruisingAltitudeLeft()
		{
			if (!cruising)
				return;

			cruising = false;
			if (cruisingToken != Actor.InvalidConditionToken)
				cruisingToken = self.RevokeCondition(cruisingToken);
		}

		#endregion

		void INotifyActorDisposing.Disposing(Actor self)
		{
			UnReserve();
		}

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

		Activity ICreationActivity.GetCreationActivity()
		{
			if (creationRallyPoint != null || creationActivityDelay > 0 || creationByMap)
				return new AssociateWithAirfieldActivity(this, creationActivityDelay, creationRallyPoint, creationByMap);

			return null;
		}

		sealed class AssociateWithAirfieldActivity : Activity
		{
			readonly Aircraft aircraft;
			readonly int delay;
			readonly CPos[] rallyPoint;
			readonly bool creationByMap;

			public AssociateWithAirfieldActivity(Aircraft self, int delay, CPos[] rallyPoint, bool creationByMap)
			{
				aircraft = self;
				this.delay = delay;
				this.rallyPoint = rallyPoint;
				this.creationByMap = creationByMap;
			}

			protected override void OnFirstRun(Actor self)
			{
				var cpos = self.Location;
				var pos = self.CenterPosition;
				bool TryDock()
				{
					var host = aircraft.GetActorBelow();
					if (host != null)
					{
						// Center the actor on the resupplier.
						var exit = host.NearestExitOrDefault(pos);
						pos = host.CenterPosition;
						pos = new WPos(pos.X, pos.Y, pos.Z - self.World.Map.DistanceAboveTerrain(pos).Length);
						if (exit != null)
						{
							pos += exit.Info.SpawnOffset;
							if (exit.Info.Facing != null)
								aircraft.Facing = exit.Info.Facing.Value;
						}

						aircraft.AddInfluence(cpos);
						aircraft.SetPosition(self, pos);
						aircraft.MakeReservation(host);

						// Freshly created aircraft shouldn't block the exit, so we allow them to yield their reservation.
						aircraft.AllowYieldingReservation();
						return true;
					}

					return false;
				}

				if (creationByMap)
				{
					if (TryDock())
						return;

					pos = new WPos(pos.X, pos.Y, pos.Z - self.World.Map.DistanceAboveTerrain(pos).Length);

					if (!aircraft.Info.TakeOffOnCreation && aircraft.CanLand(cpos))
					{
						aircraft.AddInfluence(cpos);
						aircraft.SetPosition(self, pos);
					}
					else
						aircraft.SetPosition(self, new WPos(pos.X, pos.Y, pos.Z + aircraft.Info.CruiseAltitude.Length));
				}
				else
				{
					TryDock();
					if (delay > 0)
						QueueChild(new Wait(delay));
				}
			}

			public override bool Tick(Actor self)
			{
				if (!aircraft.Info.TakeOffOnCreation || IsCanceling)
					return true;

				if (rallyPoint != null && rallyPoint.Length > 0)
					foreach (var cell in rallyPoint)
						QueueChild(new AttackMoveActivity(self, () => aircraft.MoveTo(cell, 1, evaluateNearestMovableCell: true, targetLineColor: Color.OrangeRed)));

				if (!creationByMap)
					aircraft.UnReserve();

				return true;
			}

			public override IEnumerable<Target> GetTargets(Actor self)
			{
				if (ChildActivity != null)
					return ChildActivity.GetTargets(self);

				return Target.None;
			}

			public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
			{
				if (ChildActivity != null)
					foreach (var n in ChildActivity.TargetLineNodes(self))
						yield return n;
			}
		}

		public class AircraftMoveOrderTargeter : IOrderTargeter
		{
			readonly Aircraft aircraft;

			public string OrderID { get; protected set; }
			public int OrderPriority => 4;
			public bool IsQueued { get; protected set; }

			public AircraftMoveOrderTargeter(Aircraft aircraft)
			{
				this.aircraft = aircraft;
				OrderID = "Move";
			}

			public bool TargetOverridesSelection(Actor self, in 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 virtual bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor)
			{
				if (target.Type != TargetType.Terrain || (aircraft.RequireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove)))
					return false;

				var location = self.World.Map.CellContaining(target.CenterPosition);

				// Aircraft can be force-landed by issuing a force-move order on a clear terrain cell
				// Cells that contain a blocking building are treated as regular force move orders, overriding
				// selection for left-mouse orders
				if (modifiers.HasModifier(TargetModifiers.ForceMove) && aircraft.Info.CanForceLand)
				{
					var buildingAtLocation = self.World.ActorMap.GetActorsAt(location)
						.Any(a => a.TraitOrDefault<Building>() != null && a.TraitOrDefault<Selectable>() != null);

					if (!buildingAtLocation || aircraft.CanLand(location, blockedByMobile: false))
						OrderID = "Land";
				}

				IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);

				var explored = self.Owner.Shroud.IsExplored(location);
				cursor = !aircraft.IsTraitPaused && (explored || aircraft.Info.MoveIntoShroud) && self.World.Map.Contains(location) ?
					aircraft.Info.Cursor : aircraft.Info.BlockedCursor;

				return true;
			}
		}
	}
}
