#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.Collections.Generic;
using OpenRA.Activities;
using OpenRA.Mods.Common.Activities;
using OpenRA.Primitives;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Traits
{
	public sealed class DockType { DockType() { } }

	[Desc("A generic dock that services DockClients.")]
	public class DockHostInfo : ConditionalTraitInfo, IDockHostInfo
	{
		[Desc("Docking type.")]
		public readonly BitSet<DockType> Type;

		[Desc("How many clients can this dock be reserved for?")]
		public readonly int MaxQueueLength = 3;

		[Desc("How long should the client wait before starting the docking sequence.")]
		public readonly int DockWait = 10;

		[Desc("Actual client facing when docking.")]
		public readonly WAngle DockAngle = WAngle.Zero;

		[Desc("Docking cell relative to the centre of the actor.")]
		public readonly WVec DockOffset = WVec.Zero;

		[Desc("Does client need to be dragged in?")]
		public readonly bool IsDragRequired = false;

		[Desc("Vector by which the client will be dragged when docking.")]
		public readonly WVec DragOffset = WVec.Zero;

		[Desc("In how many steps to perform the dragging?")]
		public readonly int DragLength = 0;

		public override object Create(ActorInitializer init) { return new DockHost(init.Self, this); }
	}

	public class DockHost : ConditionalTrait<DockHostInfo>,
		IDockHost, ITick, INotifySold, INotifyCapture, INotifyOwnerChanged, ISync, INotifyKilled, INotifyActorDisposing
	{
		readonly Actor self;

		public BitSet<DockType> GetDockType => Info.Type;
		public bool IsEnabledAndInWorld => !preventDock && !IsTraitDisabled && !self.IsDead && self.IsInWorld;
		public int ReservationCount => ReservedDockClients.Count;
		public bool CanBeReserved => ReservationCount < Info.MaxQueueLength;
		protected readonly List<DockClientManager> ReservedDockClients = new();

		public WPos DockPosition => self.CenterPosition + Info.DockOffset;

		[Sync]
		bool preventDock = false;

		[Sync]
		protected Actor dockedClientActor = null;
		protected DockClientManager dockedClient = null;

		public DockHost(Actor self, DockHostInfo info)
			: base(info)
		{
			this.self = self;
		}

		public virtual bool IsDockingPossible(Actor clientActor, IDockClient client, bool ignoreReservations = false)
		{
			return !IsTraitDisabled && (ignoreReservations || CanBeReserved || ReservedDockClients.Contains(client.DockClientManager));
		}

		public virtual bool Reserve(Actor self, DockClientManager client)
		{
			if (CanBeReserved && !ReservedDockClients.Contains(client))
			{
				ReservedDockClients.Add(client);
				client.ReserveHost(self, this);
				return true;
			}

			return false;
		}

		public virtual void UnreserveAll()
		{
			while (ReservedDockClients.Count > 0)
				Unreserve(ReservedDockClients[0]);
		}

		public virtual void Unreserve(DockClientManager client)
		{
			if (ReservedDockClients.Remove(client))
				client.UnreserveHost();
		}

		public virtual void OnDockStarted(Actor self, Actor clientActor, DockClientManager client)
		{
			dockedClientActor = clientActor;
			dockedClient = client;
		}

		public virtual void OnDockCompleted(Actor self, Actor clientActor, DockClientManager client)
		{
			dockedClientActor = null;
			dockedClient = null;
		}

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

		protected virtual void Tick(Actor self)
		{
			// Client was killed during docking.
			if (dockedClientActor != null && (dockedClientActor.IsDead || !dockedClientActor.IsInWorld))
				OnDockCompleted(self, dockedClientActor, dockedClient);
		}

		public virtual bool QueueMoveActivity(
			Activity moveToDockActivity, Actor self, Actor clientActor, DockClientManager client, MoveCooldownHelper moveCooldownHelper)
		{
			var move = clientActor.Trait<IMove>();

			// Make sure the actor is at dock, at correct facing, and aircraft are landed.
			// Mobile cannot freely move in WPos, so when we calculate close enough we convert to CPos.
			if ((move is Mobile ? clientActor.Location != clientActor.World.Map.CellContaining(DockPosition) : clientActor.CenterPosition != DockPosition)
				|| move is not IFacing facing || facing.Facing != Info.DockAngle)
			{
				moveCooldownHelper.NotifyMoveQueued();
				moveToDockActivity.QueueChild(move.MoveOntoTarget(clientActor, Target.FromActor(self), DockPosition - self.CenterPosition, Info.DockAngle));
				return true;
			}

			return false;
		}

		public virtual void QueueDockActivity(Activity moveToDockActivity, Actor self, Actor clientActor, DockClientManager client)
		{
			moveToDockActivity.QueueChild(new GenericDockSequence(
				clientActor,
				client,
				self,
				this,
				Info.DockWait,
				Info.IsDragRequired,
				Info.DragOffset,
				Info.DragLength));
		}

		protected override void TraitDisabled(Actor self) { UnreserveAll(); }

		void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { UnreserveAll(); }

		void INotifyCapture.OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner, BitSet<CaptureType> captureTypes)
		{
			// Steal any docked unit too.
			if (dockedClientActor != null && !dockedClientActor.IsDead && dockedClientActor.IsInWorld)
			{
				dockedClientActor.ChangeOwner(newOwner);

				// On capture OnOwnerChanged event is called first, so we need to re-reserve.
				dockedClient.ReserveHost(self, this);
			}
		}

		void INotifySold.Selling(Actor self) { preventDock = true; }

		void INotifySold.Sold(Actor self) { UnreserveAll(); }

		void INotifyKilled.Killed(Actor self, AttackInfo e) { UnreserveAll(); }

		void INotifyActorDisposing.Disposing(Actor self) { preventDock = true; UnreserveAll(); }
	}
}
