using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
    /// <summary>
    /// Component to control the docking and undocking of ships on larger ships (motherships)
    /// or stationary objects like hangars. Works with ShipDockingStation.
    /// </summary>
    [AddComponentMenu("Sci-Fi Ship Controller/Docking Components/Ship Docking")]
    [HelpURL("http://scsmmedia.com/ssc-documentation")]
    [RequireComponent(typeof(ShipControlModule))]
    public class ShipDocking : MonoBehaviour
    {
        #region Enumerations

        /// <summary>
        /// The possible states of a ship which supports docking.
        /// </summary>
        public enum DockingState
        {
            /// <summary>
            /// Ship is not docked and can fly around unhindered
            /// </summary>
            NotDocked = 0,
            /// <summary>
            /// Ship is currently attempting to dock at a docking point on a Ship Docking Station.
            /// It may be assigned to a docking point Entry Path.
            /// </summary>
            Docking = 1,
            /// <summary>
            /// Ship is currently attempting to depart from a docking point on a Ship Docking station.
            /// It may be assigned to a docking point Exit Path.
            /// </summary>
            Undocking = 2,
            /// <summary>
            /// Ship is docked at a docking point on a Ship Docking Station. Typically it wll be set
            /// to kinematic.
            /// </summary>
            Docked = 3
        }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// This is a subset of DockingState
        /// </summary>
        public enum InitialDockingState
        {
            NotDocked = 0,
            Docked = 3
        }

        #endregion

        #region Public Static Properties

        // variables to avoid enumeration lookups
        public static readonly int notDockedInt = (int)DockingState.NotDocked;
        public static readonly int dockingInt = (int)DockingState.Docking;
        public static readonly int undockingInt = (int)DockingState.Undocking;
        public static readonly int dockedInt = (int)DockingState.Docked;

        #endregion

        #region Public Variables - General

        /// <summary>
        /// If enabled, the Initialise() will be called as soon as Awake() runs. This should be disabled if you are
        /// instantiating the ShipDocking through code.
        /// </summary>
        public bool initialiseOnAwake = false;

        /// <summary>
        /// Ships can start in a state of Docked or Undocked.
        /// </summary>
        public InitialDockingState initialDockingState = InitialDockingState.NotDocked;

        /// <summary>
        /// How close the ship has to be (in metres) to the docking position before it can become docked.
        /// </summary>
        public float landingDistancePrecision = 0.01f;

        [Range(0.1f, 10f)]
        /// <summary>
        /// How close the ship has to be (in degrees) to the docking rotation before it can become docked.
        /// </summary>
        public float landingAnglePrecision = 2f;

        /// <summary>
        /// How close the ship has to be (in metres) to the hovering position before it is deemed to have reached the hover position.
        /// </summary>
        public float hoverDistancePrecision = 1f;

        /// <summary>
        /// How close the ship has to be (in degrees) to the hovering rotation before it is deemed to have reached the hover position.
        /// </summary>
        [Range(0.1f, 30f)]
        public float hoverAnglePrecision = 10f;

        /// <summary>
        /// Target time to lift off from the landing position and move to the hover position.
        /// Has no effect if the docking point hover height is 0.
        /// </summary>
        [Range(0f, 60f)]
        public float liftOffDuration = 2f;

        /// <summary>
        /// Target time to move from the hover position to the landing position.
        /// Has no effect if the docking point hover height is 0.
        /// </summary>
        [Range(0f, 60f)]
        public float landingDuration = 2f;

        /// <summary>
        /// Should physics collisions been detected when the state is Docked?
        /// </summary>
        public bool detectCollisionsWhenDocked = false;

        /// <summary>
        /// When used with ShipDockingStation.UndockShip(..), the number of seconds
        /// that the undocking manoeuvre is delayed. This allows you to create cinematic
        /// effects or perform other actions, before the Undocking process begins.
        /// </summary>
        [Range(0f, 60f)] public float undockingDelay = 0f;

        /// <summary>
        /// When value is greater than 0, the number of seconds the ship waits while docked,
        /// before automatically attempting to start the undocking procedure.
        /// </summary>
        [Range(0f, 300f)] public float autoUndockTime = 0f;

        /// <summary>
        /// This is additional velocity in an upwards direction relative to the mothership.
        /// </summary>
        public float undockVertVelocity = 2f;

        /// <summary>
        /// This is additional velocity in a forward direction relative to the mothership.
        /// </summary>
        public float undockFwdVelocity = 2f;

        /// <summary>
        /// The amount of force applied by the catapult when undocking in KiloNewtons.
        /// </summary>
        public float catapultThrust = 0f;

        /// <summary>
        /// The number of seconds that the force is applied from the catapult to the ship
        /// </summary>
        [Range(0f, 30f)] public float catapultDuration = 2f;

        /// <summary>
        /// A list of ship docking adapters. These are points on the ship where
        /// it can dock with a ShipDockingPoint on a ShipDockingStation.
        /// Typically you should not be updating this list yourself.
        /// </summary>
        public List<ShipDockingAdapter> adapterList;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        [HideInInspector] public bool allowRepaint = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// Is the adapter list expanded in the ShipDockingEditor?
        /// </summary>
        public bool isAdapterListExpanded = false;

        #endregion

        #region Public Variables - Events

        /// <summary>
        /// Methods that get called immediately after the ship has finished docking.
        /// </summary>
        public SSCDockingEvt1 onPostDocked = null;

        /// <summary>
        /// The time, in seconds, to delay the actioning of any onPostDocked methods.
        /// </summary>
        [Range(0f, 30f)] public float onPostDockedDelay = 0f;

        /// <summary>
        /// Methods that get called immediately after the Hover point is reached when docking.
        /// Typically used to perform a non-docking API action like lowering landing gear,
        /// disabling radar, disarming weapons etc.
        /// WARNING: Be careful not to call other docking APIs that might create a circular loop.
        /// </summary>
        public SSCDockingEvt1 onPostDockingHover = null;

        /// <summary>
        /// The time, in seconds, to delay the actioning of any onPostDockingHover methods.
        /// </summary>
        [Range(0f, 30f)] public float onPostDockingHoverDelay = 0f;

        /// <summary>
        /// Methods that get called immediately after docking has started.
        /// Typically used to perform a non-docking API action like chatter with ground staff,
        /// disabling radar, preparing for landing etc.
        /// WARNING: Be careful not to call other docking APIs that might create a circular loop.
        /// </summary>
        public SSCDockingEvt1 onPostDockingStart = null;

        /// <summary>
        /// The time, in seconds, to delay the actioning of any onPostDockingStart methods.
        /// </summary>
        [Range(0f, 30f)] public float onPostDockingStartDelay = 0f;

        /// <summary>
        /// Methods that get called immediately after the ship has finished undocking.
        /// </summary>
        public SSCDockingEvt1 onPostUndocked = null;

        /// <summary>
        /// The time, in seconds, to delay the actioning of any onPostUndocked methods.
        /// </summary>
        [Range(0f, 30f)] public float onPostUndockedDelay = 0f;

        /// <summary>
        /// Methods that get called immediately after the Hover point is reached when undocking.
        /// Typically used to perform a non-docking API action like raising landing gear, enabling
        /// radar, arming weapons etc.
        /// WARNING: Be careful not to call other docking APIs that might create a circular loop.
        /// </summary>
        public SSCDockingEvt1 onPostUndockingHover = null;

        /// <summary>
        /// The time, in seconds, to delay the actioning of any onPostUndockingHover methods.
        /// </summary>
        [Range(0f, 30f)] public float onPostUndockingHoverDelay = 0f;

        /// <summary>
        /// Methods that get called immediately after undocking has started.
        /// Typically used to perform a non-docking API action like dust or steam particle effects
        /// or opening hanger doors.
        /// WARNING: Be careful not to call other docking APIs that might create a circular loop.
        /// </summary>
        public SSCDockingEvt1 onPostUndockingStart = null;

        /// <summary>
        /// The time, in seconds, to delay the actioning of any onPostUndockingStart methods.
        /// </summary>
        [Range(0f, 30f)] public float onPostUndockingStartDelay = 0f;

        #endregion

        #region Public Properties

        /// <summary>
        /// [READ ONLY] The ID or the docking point on the shipDockingStation.
        /// To set, call shipDockingStation.AssignShipToDockingPoint(..).
        /// Internally uses the index in the list of docking points - however,
        /// this is subject to change.
        /// </summary>
        public int DockingPointId { get; internal set; }

        /// <summary>
        /// Typically used for debugging, is the Hover Point the target?
        /// </summary>
        public bool IsHoverPointTarget { get { return isHoverTarget; } }

        /// <summary>
        /// Is the docking component initialised?
        /// </summary>
        public bool IsInitialised { get; private set; }

        /// <summary>
        /// [READ ONLY] The docking station the ship may be docked with.
        /// To set, call shipDockingStation.AssignShipToDockingPoint(..)
        /// </summary>
        public ShipDockingStation shipDockingStation { get; internal set; }

        #endregion

        #region Public Delegates

        public delegate void CallbackOnStateChange(ShipDocking shipDocking, ShipControlModule shipControlModule, ShipAIInputModule shipAIInputModule, DockingState previousDockingState);

        /// <summary>
        /// The name of the custom method that is called immediately after the state is changed.
        /// Your method must take 4 parameters: shipDocking (never null), shipControlModule (never null),
        /// shipAIInputModule (could be null) and previousDockingState.
        /// This should be a lightweight method to avoid performance issues.
        /// Your method will NOT be called if ShipDocking.IsInitialised is false.
        /// </summary>
        public CallbackOnStateChange callbackOnStateChange = null;

        #endregion

        #region Private variables

        /// <summary>
        /// [INTERNAL ONLY]
        /// Remember which tabs etc were shown in the editor
        /// </summary>
        [SerializeField] private int selectedTabInt = 0;

        private ShipControlModule shipControlModule;
        private ShipAIInputModule shipAIInputModule;
        private bool isShipInitialised = false;
        internal ShipControlModule dockWithShip;
        private SSCManager sscManager;

        /// <summary>
        /// The current state of the ship
        /// </summary>
        private DockingState dockingState = DockingState.NotDocked;
        private int dockingStateInt = 0;

        // TODO: CHECK USAGE
        private Vector3 dockedRelativePosition;
        private Quaternion dockedRelativeRotation;

        internal RigidbodyInterpolation originalRBInterpolation = RigidbodyInterpolation.None;
        internal bool isOriginalRBInterpolationSet = false;

        private Vector3 targetDockingPosition = Vector3.zero;
        private Quaternion targetDockingRotation = Quaternion.identity;

        private bool isInAIDockingState = false;
        private bool isHoverTarget = false;
        private float dockingActionCompletedDistance = 1f;
        private float dockingActionCompletedAngle = 1f;

        private float autoUndockTimer = 0f;

        private ShipAIInputModule.CallbackCompletedStateAction originalCompletedStateActionCallback = null;

        #endregion

        #region Initialisation Methods

        void Awake()
        {
            if (initialiseOnAwake) { Initialise(); }
        }

        public void Initialise()
        {
            if (!IsInitialised)
            {
                shipControlModule = GetComponent<ShipControlModule>();
                if (shipControlModule != null)
                {
                    shipAIInputModule = GetComponent<ShipAIInputModule>();

                    // cache to avoid having to check for null etc in FixedUpdate
                    isShipInitialised = shipControlModule.IsInitialised;
                }

                // Add capacity for 1 docking adapter as this is the current default.
                if (adapterList == null) { adapterList = new List<ShipDockingAdapter>(1); }
                if (adapterList != null && adapterList.Count == 0)
                {
                    adapterList.Add(new ShipDockingAdapter());
                }

                // Keep compiler happy
                if (selectedTabInt < 0) { }

                IsInitialised = true;
            }
        }

        #endregion

        #region Event Methods

        /// <summary>
        /// Automatically called by Unity immediately before the object is destroyed
        /// </summary>
        private void OnDestroy()
        {
            RemoveListeners();
            CancelInvoke();
            StopAllCoroutines();
        }

        #endregion

        #region Update Methods

        private void Update ()
        {
            if (isShipInitialised)
            {
                // Is docked?
                if (dockingStateInt == dockedInt)
                {
                    // IsKinematic should be enabled because ship is disabled
                    if (shipControlModule.ShipRigidbody.isKinematic)
                    {
                        if (dockWithShip != null && dockWithShip.ShipIsEnabled())
                        {
                            // Translate the relative local-space offset of the docked ship
                            // Update the rotation of the docked ship by adding the local rotation of the docked ship relative to the mother ship.
                            transform.SetPositionAndRotation(
                                dockWithShip.transform.position + dockWithShip.transform.TransformDirection(dockedRelativePosition),
                                dockWithShip.transform.rotation * dockedRelativeRotation);
                        }
                    }
                }

                // Is this a moving ship docking station?
                if (isInAIDockingState && shipDockingStation != null && shipDockingStation.IsMotherShip)
                {
                    UpdateDockingWSPositionAndRotation(shipDockingStation.GetDockingPoint(DockingPointId),
                        shipAIInputModule, isHoverTarget, dockingActionCompletedDistance, dockingActionCompletedAngle);
                }

                // Auto undocking - do last
                // Check if auto undock is active (gets activated at end of SetState(..)
                if (autoUndockTime > 0f && dockingStateInt == dockedInt && autoUndockTimer > 0f)
                {
                    autoUndockTimer += Time.deltaTime;

                    if (autoUndockTimer > autoUndockTime)
                    {
                        // Reset time (auto undock is not active)
                        autoUndockTimer = 0f;

                        shipDockingStation.UnDockShip(shipControlModule);
                    }
                }
            }
        }

        #endregion

        #region Private Methods

        private bool IsShipDocked()
        {
            if (dockingStateInt == dockedInt)
            {
                // May be other conditions...

                return true;
            }
            else { return false; }
        }

        /// <summary>
        /// Assign a new path to an AI Ship.
        /// If there is no matching path, then this will return false.
        /// </summary>
        /// <param name="pathGUIDHash"></param>
        private bool AssignNewPath(int pathGUIDHash)
        {
            bool isAssigned = false;

            if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); }
            if (sscManager != null)
            {
                PathData pathData = sscManager.GetPath(pathGUIDHash);

                if (pathData != null)
                {
                    // Set the ship's state to the "move to" state
                    shipAIInputModule.SetState(AIState.moveToStateID);
                    // Set the target path to the new path
                    shipAIInputModule.AssignTargetPath(pathData);
                    isAssigned = true;
                }
            }
            return isAssigned;
        }

        /// <summary>
        /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent)
        /// after the delayTime in seconds.
        /// </summary>
        /// <param name="shipDockingStationID"></param>
        /// <param name="shipId"></param>
        /// <param name="dockingPointId"></param>
        /// <param name="delayTime"></param>
        /// <returns></returns>
        private IEnumerator OnPostDockedDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime)
        {
            yield return new WaitForSeconds(delayTime);

            // These will ALWAYS fire although events are checked before calling this method.
            if (onPostDocked != null)
            {
                onPostDocked.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero);
            }
        }

        /// <summary>
        /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent)
        /// after the delayTime in seconds.
        /// </summary>
        /// <param name="shipDockingStationID"></param>
        /// <param name="shipId"></param>
        /// <param name="dockingPointId"></param>
        /// <param name="delayTime"></param>
        /// <returns></returns>
        private IEnumerator OnPostDockingHoverDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime)
        {
            yield return new WaitForSeconds(delayTime);

            // These will ALWAYS fire although events are checked before calling this method.
            if (onPostDockingHover != null)
            {
                onPostDockingHover.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero);
            }
        }

        /// <summary>
        /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent)
        /// after the delayTime in seconds.
        /// </summary>
        /// <param name="shipDockingStationID"></param>
        /// <param name="shipId"></param>
        /// <param name="dockingPointId"></param>
        /// <param name="delayTime"></param>
        /// <returns></returns>
        private IEnumerator OnPostDockingStartDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime)
        {
            yield return new WaitForSeconds(delayTime);

            // These will ALWAYS fire although events are checked before calling this method.
            if (onPostDockingStart != null)
            {
                onPostDockingStart.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero);
            }
        }

        /// <summary>
        /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent)
        /// after the delayTime in seconds.
        /// </summary>
        /// <param name="shipDockingStationID"></param>
        /// <param name="shipId"></param>
        /// <param name="dockingPointId"></param>
        /// <param name="delayTime"></param>
        /// <returns></returns>
        private IEnumerator OnPostUndockedDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime)
        {
            yield return new WaitForSeconds(delayTime);

            // These will ALWAYS fire although events are checked before calling this method.
            if (onPostUndocked != null)
            {
                onPostUndocked.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero);
            }
        }

        /// <summary>
        /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent)
        /// after the delayTime in seconds.
        /// </summary>
        /// <param name="shipDockingStationID"></param>
        /// <param name="shipId"></param>
        /// <param name="dockingPointId"></param>
        /// <param name="delayTime"></param>
        /// <returns></returns>
        private IEnumerator OnPostUndockingHoverDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime)
        {
            yield return new WaitForSeconds(delayTime);

            // These will ALWAYS fire although events are checked before calling this method.
            if (onPostUndockingHover != null)
            {
                onPostUndockingHover.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero);
            }
        }

        /// <summary>
        /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent)
        /// after the delayTime in seconds.
        /// </summary>
        /// <param name="shipDockingStationID"></param>
        /// <param name="shipId"></param>
        /// <param name="dockingPointId"></param>
        /// <param name="delayTime"></param>
        /// <returns></returns>
        private IEnumerator OnPostUndockingStartDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime)
        {
            yield return new WaitForSeconds(delayTime);

            // These will ALWAYS fire although events are checked before calling this method.
            if (onPostUndockingStart != null)
            {
                onPostUndockingStart.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero);
            }
        }

        /// <summary>
        /// Calculates the world space docking position and rotation for an AI ship at a given docking point. 
        /// If hoverPosition is true, it will calculate the offset hover position.
        /// Then it updates the ship AI Input module with this information.
        /// Important: It does not set the docking state.
        /// </summary>
        /// <param name="shipDockingPoint"></param>
        /// <param name="shipAIInputModule"></param>
        /// <param name="hoverPosition"></param>
        /// <param name="actionCompletedDistance"></param>
        private void UpdateDockingWSPositionAndRotation (ShipDockingPoint shipDockingPoint, ShipAIInputModule shipAIInputModule, 
            bool hoverPosition, float actionCompletedDistance, float actionCompletedAngularDistance)
        {
            if (shipDockingPoint != null)
            {
                Vector3 targetDockingPos = Vector3.zero;
                Quaternion targetDockingRot = Quaternion.identity;

                // Calculate the world space docking position and rotation
                CalculateDockingWSPositionAndRotation(shipDockingPoint, hoverPosition, ref targetDockingPos, ref targetDockingRot);

                // Assign target information to AI ship
                AssignDockingWSPositionAndRotation(shipAIInputModule, targetDockingPos, targetDockingRot, 
                    shipDockingPoint, actionCompletedDistance, actionCompletedAngularDistance);
            }
        }

        /// <summary>
        /// Calculates the world space docking position and rotation for an AI ship at a given docking point. 
        /// If hoverPosition is true, it will calculate the offset hover position.
        /// </summary>
        /// <param name="shipDockingPoint"></param>
        /// <param name="hoverPosition"></param>
        /// <param name="targetDockingPos"></param>
        /// <param name="targetDockingRot"></param>
        private void CalculateDockingWSPositionAndRotation (ShipDockingPoint shipDockingPoint, 
            bool hoverPosition, ref Vector3 targetDockingPos, ref Quaternion targetDockingRot)
        {
            // Target docking position calculation:
            // Start with the world space position of the docking point
            targetDockingPos = shipDockingStation.GetDockingPointPositionWS(shipDockingPoint);
            // If required, offset the position by a hover distance
            if (hoverPosition)
            {
                targetDockingPos += shipDockingStation.GetDockingPointRotation(shipDockingPoint) * Vector3.up * shipDockingPoint.hoverHeight;
            }

            // Calculate the target rotation of the ship relative to the docking point
            Vector3 adapterDirection = adapterList[0].relativeDirection;
            float XYPlaneLength = Mathf.Sqrt((adapterDirection.x * adapterDirection.x) + (adapterDirection.y * adapterDirection.y));
            // Idea is that we first rotate the ship around the z-axis to get the XY direction pointing downwards, then
            // we rotate around the x-axis to get the YZ direction pointing downwards
            Quaternion shipRelativeRotation = Quaternion.Euler(Mathf.Atan2(adapterDirection.z, XYPlaneLength) * Mathf.Rad2Deg, 0f,
                -Mathf.Atan2(adapterDirection.x, -adapterDirection.y) * Mathf.Rad2Deg);
            // For special case of directly forwards direction, flip upside down to give more natural direction
            if ((adapterDirection.x > 0f ? adapterDirection.x : -adapterDirection.x) < 0.001f &&
                (adapterDirection.y > 0f ? adapterDirection.y : -adapterDirection.y) < 0.001f &&
                adapterDirection.z > 0.001f)
            {
                shipRelativeRotation *= Quaternion.Euler(0f, 0f, 180f);
            }

            // Target docking rotation calculation:
            // Start with the world space rotation of the docking point
            targetDockingRot = shipDockingStation.GetDockingPointRotation(shipDockingPoint);
            // Then add the rotation of the ship relative to the docking point
            targetDockingRot *= shipRelativeRotation;

            // Subtract the adapter relative position (in world space), so that the adapter point will line up with the docking point
            targetDockingPos -= targetDockingRot * adapterList[0].relativePosition;
        }

        /// <summary>
        /// Assigns a target position, rotation and radius to an AI ship in the docking AI state.
        /// </summary>
        /// <param name="shipAIInputModule"></param>
        /// <param name="targetDockingPos"></param>
        /// <param name="targetDockingRot"></param>
        /// <param name="shipDockingPoint"></param>
        /// <param name="actionCompletedDistance"></param>
        /// <param name="actionCompletedAngularDistance"></param>
        private void AssignDockingWSPositionAndRotation (ShipAIInputModule shipAIInputModule, Vector3 targetDockingPos, 
            Quaternion targetDockingRot, ShipDockingPoint shipDockingPoint, float actionCompletedDistance,
            float actionCompletedAngularDistance)
        {
            // Assign target position, rotation and radius data to AI ship
            shipAIInputModule.AssignTargetPosition(targetDockingPos);
            shipAIInputModule.AssignTargetRotation(targetDockingRot);
            // Target radius must be at least 1 metre
            shipAIInputModule.AssignTargetRadius(shipDockingPoint.hoverHeight > 1f ? shipDockingPoint.hoverHeight : 1f);
            shipAIInputModule.AssignTargetDistance(actionCompletedDistance);
            shipAIInputModule.AssignTargetAngularDistance(actionCompletedAngularDistance);
            shipAIInputModule.AssignTargetVelocity(shipDockingStation.GetStationVelocity());
        }

        /// <summary>
        /// Try to join an entry or exit path at the closest point to the ship
        /// </summary>
        /// <param name="guidHashPath"></param>
        /// <returns></returns>
        private bool TryJointPathAtClosestPoint(int guidHashPath)
        {
            bool isJoinedPath = false;

            // Attempt ot join exit path (if there is one)
            if (AssignNewPath(guidHashPath))
            {
                // Find the closest point on the exit path
                PathData pathData = sscManager.GetPath(guidHashPath);

                if (pathData != null)
                {
                    Vector3 closestPointOnPath = Vector3.zero;
                    float closestPointOnPathTValue = 0f;
                    int prevPathLocationIdx = 0;

                    if (SSCMath.FindClosestPointOnPath(pathData, shipControlModule.shipInstance.TransformPosition, ref closestPointOnPath, ref closestPointOnPathTValue, ref prevPathLocationIdx))
                    {
                        shipAIInputModule.SetPreviousTargetPathLocationIndex(prevPathLocationIdx);
                        int nextExitPathLocationIndex = SSCManager.GetNextPathLocationIndex(pathData, prevPathLocationIdx, pathData.isClosedCircuit);

                        if (nextExitPathLocationIndex >= 0)
                        {
                            shipAIInputModule.SetCurrentTargetPathLocationIndex(nextExitPathLocationIndex, closestPointOnPathTValue);
                            shipAIInputModule.SetPreviousTargetPathLocationIndex(prevPathLocationIdx);
                            isJoinedPath = true;
                        }
                    }
                }
            }

            return isJoinedPath;
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// If it has not already been saved (set), record
        /// the original rigidbody interpolation setting
        /// of the ship this component is attached to.
        /// </summary>
        internal void SaveInterpolation()
        {
            if (!isOriginalRBInterpolationSet)
            {
                // If the ship is initialised use the cached rigidbody, else fetch it
                Rigidbody rbShip = isShipInitialised ? shipControlModule.ShipRigidbody : shipControlModule.GetComponent<Rigidbody>();

                // Remember the interpolation setting on the ship
                if (rbShip != null)
                {
                    originalRBInterpolation = rbShip.interpolation;
                    isOriginalRBInterpolationSet = true;
                }
            }
        }

        /// <summary>
        /// This can be used when another feature triggers undocking. It ensures
        /// that if the countdown has begun it doesn't continue.
        /// </summary>
        internal void ResetAutoUndock()
        {
            autoUndockTimer = 0f;
        }

        #endregion

        #region Internal Callback Methods

        /// <summary>
        /// Callback for when the ship AI has completed a state action.
        /// </summary>
        /// <param name="shipAIInputModule"></param>
        internal void AICompletedStateActionCallback(ShipAIInputModule shipAIInputModule)
        {
            if (shipAIInputModule != null && shipAIInputModule.IsInitialised)
            {
                // By default, not in AI docking state
                isInAIDockingState = false;

                dockingStateInt = (int)dockingState;
                int shipAIStateID = shipAIInputModule.GetState();

                #region If Docking
                if (dockingStateInt == dockingInt)
                {
                    ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId);

                    if (shipAIStateID == AIState.dockingStateID)
                    {
                        // Calculate the world space docking position and rotation for the landing position
                        Vector3 targetDockingPos = Vector3.zero;
                        Quaternion targetDockingRot = Quaternion.identity;
                        CalculateDockingWSPositionAndRotation(shipDockingPoint, false, ref targetDockingPos, ref targetDockingRot);

                        // Check if we have reached the landing position
                        if (Vector3.SqrMagnitude(shipAIInputModule.GetTargetPosition() - targetDockingPos) <  landingDistancePrecision * landingDistancePrecision)
                        {
                            // We have now finished the landing manoeuvre, set the state to docked
                            SetState(DockingState.Docked);
                            // Clean up - set the callback for CompletedStateAction back to its original value
                            shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback;
                            shipAIInputModule.SetState(AIState.idleStateID);

                            // If there are Post Docked event peristent AND/OR non-persistent listeners.
                            if (SSCUtils.HasListeners(onPostDocked))
                            {
                                if (onPostDockedDelay > 0f)
                                {
                                    StartCoroutine(OnPostDockedDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingHoverDelay));
                                }
                                else
                                {
                                    onPostDocked.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero);
                                }
                            }
                        }
                        else
                        {
                            // We have now finished the hover manoeuvre
                            // Now we need to start the landing manoeuvre

                            // Reset the state action completed flag
                            shipAIInputModule.SetHasCompletedStateAction(false);

                            // Set the ship to target the landing position (we have already calculated it)
                            // Action will be completed when we get with x metres of the target position
                            dockingActionCompletedDistance = landingDistancePrecision;
                            dockingActionCompletedAngle = landingAnglePrecision;
                            AssignDockingWSPositionAndRotation(shipAIInputModule, targetDockingPos, targetDockingRot,
                                shipDockingPoint, dockingActionCompletedDistance, dockingActionCompletedAngle);

                            shipAIInputModule.AssignTargetTime(landingDuration);
                            isInAIDockingState = true;
                            isHoverTarget = false;

                            // If there are Post Docking Hover event peristent AND/OR non-persistent listeners.
                            if (SSCUtils.HasListeners(onPostDockingHover))
                            {
                                if (onPostDockingHoverDelay > 0f)
                                {
                                    StartCoroutine(OnPostDockingHoverDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingHoverDelay));
                                }
                                else
                                {
                                    onPostDockingHover.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero);
                                }
                            }
                        }
                    }
                    else
                    {
                        // If we were docking, we have now reached the end of the entry path
                        // Now we need to start the landing manoeuvre
                        // Set the ship's state to the "docking" state
                        shipAIInputModule.SetState(AIState.dockingStateID);

                        // Set the ship to target the hover position
                        // Action will be completed when we get with hover distance & angle of the target (hover) position
                        dockingActionCompletedDistance = hoverDistancePrecision;
                        dockingActionCompletedAngle = hoverAnglePrecision;
                        UpdateDockingWSPositionAndRotation(shipDockingPoint, shipAIInputModule, true, 
                            dockingActionCompletedDistance, dockingActionCompletedAngle);

                        shipAIInputModule.AssignTargetTime(landingDuration);

                        isInAIDockingState = true;
                        isHoverTarget = true;
                    }
                }
                #endregion

                #region Else Undocking
                else if (dockingStateInt == undockingInt)
                {
                    if (shipAIStateID == AIState.dockingStateID)
                    {
                        // If we were undocking, we have now finished the liftoff manoeuvre
                        // We need to transition to following the exit path
                        ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId);
                        if (shipDockingPoint != null)
                        {
                            isHoverTarget = false;
                            if (!AssignNewPath(shipDockingPoint.guidHashExitPath))
                            {
                                // If no valid exit path, immediately finish undocking manoeuvre
                                SetState(DockingState.NotDocked);
                                // Clean up - set the callback for CompletedStateAction back to its original value
                                shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback;
                                shipAIInputModule.SetState(AIState.idleStateID);
                                // Call the callback to let the script know that it needs to take action
                                if (originalCompletedStateActionCallback != null) { originalCompletedStateActionCallback(shipAIInputModule); }
                            }
                            // Catapult launch for AI or AI assisted with an Exit path once hover height has been reached
                            else if (catapultThrust > 0f)
                            {
                                shipControlModule.shipInstance.AddBoost(Vector3.forward, catapultThrust, catapultDuration);
                            }

                            // If there is a Post Undocking Hover event peristent AND/OR non-persistent listeners.
                            if (SSCUtils.HasListeners(onPostUndockingHover))
                            {
                                if (onPostUndockingHoverDelay > 0f)
                                {
                                    StartCoroutine(OnPostUndockingHoverDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostUndockingHoverDelay));
                                }
                                else
                                {
                                    onPostUndockingHover.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero);
                                }
                            }
                        }
                    }
                    else
                    {
                        // If we were undocking, we have now reached the end of the exit path
                        // Set the state to not docked
                        SetState(DockingState.NotDocked);
                        // Clean up - set the callback for CompletedStateAction back to its original value
                        shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback;
                        // v1.2.3+ Undocking AI ships become idle at end of exit path
                        shipAIInputModule.SetState(AIState.idleStateID);
                        // Call the callback to let the script know that it needs to take action
                        if (originalCompletedStateActionCallback != null) { originalCompletedStateActionCallback(shipAIInputModule); }

                        // If there any Post Undocked event peristent AND/OR non-persistent listeners.
                        if (SSCUtils.HasListeners(onPostUndocked))
                        {
                            if (onPostUndockedDelay > 0f)
                            {
                                StartCoroutine(OnPostUndockedDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingHoverDelay));
                            }
                            else
                            {
                                onPostUndocked.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero);
                            }
                        }
                    }
                }
                #endregion
            }
        }

        #endregion

        #region Public API Methods

        /// <summary>
        /// Return the current docking state
        /// </summary>
        /// <returns></returns>
        public DockingState GetState()
        {
            return dockingState;
        }

        /// <summary>
        /// Return the current docking state as an integer.
        /// </summary>
        /// <returns></returns>
        public int GetStateInt()
        {
            return dockingStateInt;
        }

        /// <summary>
        /// Call this when you wish to remove any custom (non-persistent) event listeners,
        /// like after creating them in code and then destroying the object.
        /// This is automatically called by OnDestroy.
        /// </summary>
        public void RemoveListeners()
        {
            if (IsInitialised)
            {
                if (onPostDocked != null) { onPostDocked.RemoveAllListeners(); }
                if (onPostDockingHover != null) { onPostDockingHover.RemoveAllListeners(); }
                if (onPostDockingStart != null) { onPostDockingStart.RemoveAllListeners(); }
                if (onPostUndocked != null) { onPostUndocked.RemoveAllListeners(); }
                if (onPostUndockingHover != null) { onPostUndockingHover.RemoveAllListeners(); }
                if (onPostUndockingStart != null) { onPostUndockingStart.RemoveAllListeners(); }
            }
        }

        /// <summary>
        /// Set the docking state of the ship.
        /// When undocking, the velocity of ShipDockingStation (mothership) is considered.
        /// If configured, a custom method is called after the state has been changed.
        /// </summary>
        /// <param name="dockingState"></param>
        public void SetState (DockingState dockingState)
        {
            if (!IsInitialised)
            {
                #if UNITY_EDITOR
                Debug.LogWarning("ERROR: ShipDocking.SetState was called before it was initialised on " + gameObject.name + ". Either set initialiseOnAwake in the Editor or call Initialise() at runtime.");
                #endif
                return;
            }

            int previousDockingStateInt = dockingStateInt;

            this.dockingState = dockingState;
            dockingStateInt = (int)dockingState;

            // By default, not in AI docking state
            isInAIDockingState = false;

            if (shipControlModule != null)
            {
                // Used for AI docking without a path
                bool immediatelyCompleteState = false;

                // Ensure autoundocking is reset if the state is changed by another feature or API call.
                autoUndockTimer = 0f;

                #region Docked
                if (dockingStateInt == dockedInt)
                {
                    if (isShipInitialised && !shipControlModule.shipInstance.isThrusterFXStationary)
                    {
                        shipControlModule.StopThrusterEffects();
                    }
                    // NOTE: This will pause (but not stop) the thruster FX.
                    shipControlModule.DisableShipMovement();
                    // Check if we should override default behaviour after ship physics have been disabled
                    if (detectCollisionsWhenDocked) { shipControlModule.ShipRigidbody.detectCollisions = true; }
                    isHoverTarget = false;

                    if (shipDockingStation != null)
                    {
                        ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId);

                        if (shipDockingPoint != null)
                        {
                            // NOTE: The scaled relative position hasn't been tested with moving motherships.
                            dockedRelativePosition = shipDockingStation.GetScaledRelativePosition(shipDockingPoint);
                            dockedRelativeRotation = Quaternion.Euler(shipDockingPoint.relativeRotation);

                            // Set the initial position and rotation of the ship - snap to the docking point
                            CalculateDockingWSPositionAndRotation(shipDockingPoint, false, ref targetDockingPosition, ref targetDockingRotation);
                            transform.SetPositionAndRotation(targetDockingPosition, targetDockingRotation);
                        }
                    }

                    // If the ship is initialised use the cached rigidbody, else fetch it
                    Rigidbody rbShip = isShipInitialised ? shipControlModule.ShipRigidbody : shipControlModule.GetComponent<Rigidbody>();

                    // Remember the interpolation setting on the docking ship
                    if (rbShip != null && !isOriginalRBInterpolationSet)
                    {
                        originalRBInterpolation = rbShip.interpolation;
                        isOriginalRBInterpolationSet = true;
                    }

                    // Does this Docking Station have a mothership?
                    if (dockWithShip != null)
                    {
                        // Where possible use the cached rigidbodies
                        if (isShipInitialised && dockWithShip.IsInitialised)
                        {
                            // Match the interpolation setting on the ship being docked to, so that we avoid jerky behaviour
                            shipControlModule.ShipRigidbody.interpolation = dockWithShip.ShipRigidbody.interpolation;
                        }
                        else if (rbShip != null)
                        {
                            // Match the interpolation setting on the ship being docked to, so that we avoid jerky behaviour
                            Rigidbody rbShipToDockWith = dockWithShip.GetComponent<Rigidbody>();
                            if (rbShipToDockWith != null) { rbShip.interpolation = rbShipToDockWith.interpolation; }
                        }
                    }
                }
                #endregion

                #region NotDocked
                else if (dockingStateInt == notDockedInt)
                {
                    // If previous state was docked, reset the velocity, else don't.
                    shipControlModule.EnableShipMovement(previousDockingStateInt == dockedInt);

                    isHoverTarget = false;

                    // If previous state was docked, docking or undocking restore the original
                    // rigidbody interpolation.
                    if (previousDockingStateInt != notDockedInt)
                    {
                        // If the ship is initialised use the cached rigidbody, else fetch it
                        Rigidbody rbShip = shipControlModule.IsInitialised ? shipControlModule.ShipRigidbody : shipControlModule.GetComponent<Rigidbody>();

                        // Restore the original rigidbody interpolation
                        if (rbShip != null && isOriginalRBInterpolationSet)
                        {
                            rbShip.interpolation = originalRBInterpolation;
                            isOriginalRBInterpolationSet = false;
                        }

                        // Check for a mothership. i.e. a Docking Station on a Ship
                        if (shipControlModule.IsInitialised && dockWithShip != null && dockWithShip.IsInitialised)
                        {
                            // Undock with the same velocity as the mothership

                            shipControlModule.ShipRigidbody.velocity = dockWithShip.ShipRigidbody.velocity + (dockWithShip.shipInstance.RigidbodyUp * undockVertVelocity) + (dockWithShip.shipInstance.RigidbodyForward * undockFwdVelocity)
                                + Vector3.Cross(dockWithShip.ShipRigidbody.angularVelocity, shipControlModule.ShipRigidbody.position - dockWithShip.transform.TransformPoint(dockWithShip.shipInstance.centreOfMass));
                            shipControlModule.ShipRigidbody.angularVelocity = dockWithShip.ShipRigidbody.angularVelocity;
                        }

                        // Check for catapult with non-AI assisted undocking OR AI-assist with no Exit Path
                        if (previousDockingStateInt == dockedInt && catapultThrust > 0f)
                        {
                            shipControlModule.shipInstance.AddBoost(Vector3.forward, catapultThrust, catapultDuration);
                        }
                    }
                }
                #endregion

                #region Docking or UnDocking
                else if (dockingStateInt == dockingInt || dockingStateInt == undockingInt)
                {
                    if (previousDockingStateInt == dockedInt)
                    {
                        // If the ship was previously docked, we'll probably need to also reenable it.
                        if (!shipControlModule.ShipMovementIsEnabled() || !shipControlModule.ShipIsEnabled()) { shipControlModule.EnableShip(false, true); }
                    }

                    if (shipDockingStation != null && shipDockingStation.IsDockingPointPathsInitialised)
                    {
                        ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId);

                        if (shipDockingPoint != null)
                        {
                            // If this is an AI Ship (or AI-assisted player ship), assign it a path to follow (if it is setup in the docking point)
                            if (shipAIInputModule != null && shipAIInputModule.IsInitialised)
                            {
                                // Is switching directly to docking while currently undocking
                                if (previousDockingStateInt == undockingInt && dockingStateInt == dockingInt)
                                {
                                    // Restore user callback method (if any) - this gets updated again below
                                    shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback;

                                    // Where are we in the current undocking maneouvre?
                                    int shipAIStateID = shipAIInputModule.GetState();

                                    // While undocking, was ship heading towards the hover position?
                                    if (isHoverTarget)
                                    {
                                        // Move directly towards the docking point
                                        isHoverTarget = false;
                                        isInAIDockingState = true;
                                    }
                                    // On Exit path, attempt to join Entry path
                                    else if (shipAIStateID == AIState.moveToStateID)
                                    {
                                        //Debug.Log("[DEBUG] switching directly between undocking and docking while on Exit path. AIState: " + shipAIStateID + " T:" + Time.time);

                                        // Attempt ot join entry path (if there is one)
                                        if (!TryJointPathAtClosestPoint(shipDockingPoint.guidHashEntryPath))
                                        {
                                            // No valid path, one was not specified, or close to end of path, so assume entry path has been completed.
                                            immediatelyCompleteState = true;
                                        }
                                    }
                                }
                                // Is switching directly to undocking while currently docking
                                else if (previousDockingStateInt == dockingInt && dockingStateInt == undockingInt)
                                {
                                    // Restore user callback method (if any) - this gets updated again below
                                    shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback;

                                    // Where are we in the current docking maneouvre?
                                    int shipAIStateID = shipAIInputModule.GetState();

                                    // While docking, was ship heading towards the hover position?
                                    if (isHoverTarget)
                                    {
                                        // Stop flying towards hover position, and attempt to fly along exit path (if there is one).
                                        immediatelyCompleteState = true;
                                    }
                                    // Descending from hover positon to docking point
                                    else if (shipAIStateID == AIState.dockingStateID)
                                    {
                                        // Commence undocking
                                        shipAIInputModule.SetState(AIState.dockingStateID);

                                        // Action will be completed when we get close to the target position
                                        dockingActionCompletedDistance = hoverDistancePrecision;
                                        dockingActionCompletedAngle = hoverAnglePrecision;
                                        UpdateDockingWSPositionAndRotation(shipDockingPoint, shipAIInputModule, true,
                                            dockingActionCompletedDistance, dockingActionCompletedAngle);

                                        shipAIInputModule.AssignTargetTime(landingDuration);
                                            
                                        isInAIDockingState = true;
                                        isHoverTarget = true;
                                    }
                                    // On entry path
                                    else if (shipAIStateID == AIState.moveToStateID)
                                    {
                                        // Try to join the exit path at the closest point to the ship
                                        if (!TryJointPathAtClosestPoint(shipDockingPoint.guidHashExitPath))
                                        {
                                            immediatelyCompleteState = true;
                                        }
                                    }
                                }
                                // Assign the path when docking
                                else if (dockingStateInt == dockingInt)
                                {
                                    if (!AssignNewPath(shipDockingPoint.guidHashEntryPath))
                                    {
                                        // No path or one was not specified, so assume entry path has been completed.
                                        immediatelyCompleteState = true;
                                    }
                                }
                                else
                                {
                                    // When exiting (undocking), set the ship's AI state to the "docking"
                                    // state to carry out the liftoff manoeuvre, before following the 
                                    // exit path (if there is one).
                                    // NOTE: The ShipAI state of "docking" covers both docking and undocking.
                                    shipAIInputModule.SetState(AIState.dockingStateID);

                                    // Action will be completed when we get close to the target position
                                    dockingActionCompletedDistance = hoverDistancePrecision;
                                    dockingActionCompletedAngle = hoverAnglePrecision;
                                    UpdateDockingWSPositionAndRotation(shipDockingPoint, shipAIInputModule, true, 
                                        dockingActionCompletedDistance, dockingActionCompletedAngle);

                                    shipAIInputModule.AssignTargetTime(liftOffDuration);
                                    isInAIDockingState = true;
                                    isHoverTarget = true;
                                }
                                // Remember any callback the user had for CompletedStateAction
                                originalCompletedStateActionCallback = shipAIInputModule.callbackCompletedStateAction;
                                // Replace it with our own callback
                                shipAIInputModule.callbackCompletedStateAction = AICompletedStateActionCallback;
                            }
                        }
                    }
                    #if UNITY_EDITOR
                    else
                    {
                        Debug.Log("ERROR: attempting Docking or Undocking without a ShipDockingStation or without correctly initialising it");
                    }            
                    #endif
                }
                #endregion

                #region Callbacks, Notifications and AutoUndocking

                if (callbackOnStateChange != null) { callbackOnStateChange(this, shipControlModule, shipAIInputModule, (DockingState)previousDockingStateInt); }

                // AI docking without an entry path (added in 1.2.1)
                if (immediatelyCompleteState) { AICompletedStateActionCallback(shipAIInputModule); }

                if (dockingStateInt == dockedInt && shipDockingStation != null)
                {
                    // Do we need to notify the ship docking station that the ship has finished docking?
                    // This will call any event methods configured on the ship docking station.
                    if (SSCUtils.HasListeners(shipDockingStation.onPostDocked))
                    {
                        shipDockingStation.onPostDocked.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.GetShipId, DockingPointId + 1, Vector3.zero);
                    }

                    // If auto undocking is enabled, restart (activate) the timer.
                    if (autoUndockTime > 0f) { autoUndockTimer = 0.0001f; }
                }

                // Has started undocking from the Docked state
                if (dockingStateInt == undockingInt && previousDockingStateInt == dockedInt)
                {
                    // If there is a Post Undocking Start event peristent AND/OR non-persistent listeners.
                    if (SSCUtils.HasListeners(onPostUndockingStart))
                    {
                        if (onPostUndockingStartDelay > 0f)
                        {
                            StartCoroutine(OnPostUndockingStartDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostUndockingStartDelay));
                        }
                        else
                        {
                            onPostUndockingStart.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero);
                        }
                    }
                }
                // Has started docking from the NotDocked state
                else if (dockingStateInt == dockingInt && previousDockingStateInt == notDockedInt)
                {
                    // If there is a Post Docking Start event peristent AND/OR non-persistent listeners.
                    if (SSCUtils.HasListeners(onPostDockingStart))
                    {
                        if (onPostDockingStartDelay > 0f)
                        {
                            StartCoroutine(OnPostDockingStartDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingStartDelay));
                        }
                        else
                        {
                            onPostDockingStart.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero);
                        }
                    }
                }

                #endregion
            }
        }

        /// <summary>
        /// Prevent a delayed Post Docked event from running once the delay has been triggered by the ship has docked.
        /// </summary>
        public void StopOnPostDocked()
        {
            StopCoroutine("OnPostDockedDelayed");
        }

        /// <summary>
        /// Prevent a delayed Post Docking Hover event from running once the delay has been triggered
        /// by the ship reaching the hover point while docking.
        /// </summary>
        public void StopOnPostDockingHover()
        {
            StopCoroutine("OnPostDockingHoverDelayed");
        }

        /// <summary>
        /// Prevent a delayed Post Docking Start event from running once the delay has been triggered
        /// by the ship starting to dock.
        /// </summary>
        public void StopOnPostDockingStart()
        {
            StopCoroutine("OnPostDockingStartDelayed");
        }

        /// <summary>
        /// Prevent a delayed Post Undocked event from running once the delay has been triggered by the
        /// ship finishing the docking manoeuvre.
        /// </summary>
        public void StopOnPostUndocked()
        {
            StopCoroutine("OnPostUndockedDelayed");
        }

        /// <summary>
        /// Prevent a delayed Post Undocking Hover event from running once the delay has been triggered
        /// by the ship reaching the hover point while undocking.
        /// </summary>
        public void StopOnPostUndockingHover()
        {
            StopCoroutine("OnPostUndockingHoverDelayed");
        }

        /// <summary>
        /// Prevent a delayed Post Undocking Start event from running once the delay has been triggered
        /// by the ship starting to undock.
        /// </summary>
        public void StopOnPostUndockingStart()
        {
            StopCoroutine("OnPostUndockingStartDelayed");
        }

        #endregion
    }
}