using System.Collections;
using UnityEngine;

namespace Passer {

    /// <summary>Sockets can hold a Handles.</summary>
    /// \image html SocketGizmo.png
    /// \image rtf SocketGizmo.png
    /// 
    /// Attaching and Releasing
    /// =======================
    /// Attaching a Handle using Scripting
    /// ---------------------------------
    /// You can attach handles to the Socket GameObject by calling an Attach() function on the Socket.
    /// This will try to attach the Transform, Rigidbody or Handle parameter to the Socket.
    /// On the other hand you can release Handles again by calling an Release() function on the Socket.
    /// This will release the currently attached Handle from the Socket.
    /// 
    /// Attaching a Prefab with a Handle in the Editor
    /// -----------------------------------------------
    /// It is possible to attach a Handle prefab to a socket in the Unity editor.
    /// In this way the socket will start with a Handle attached.
    /// This is done by assigning a Prefab to the Attached Prefab field of the Socket.
    /// This will automatically create an instance of the prefab which is attached to the socket.
    /// When the Attached Prefab field is cleared again, 
    /// the prefab instance attached to the socket is released and destroyed again.
    /// 
    /// Releasing a Handle to another Socket
    /// ------------------------------------
    /// When an handle is attached to a socket and the Handle is attached to another socket
    /// it will be automatically released from the original socket.
    /// 
    /// Socket Tag
    /// ==========
    /// With the Socket Tag field to is possible to limit the Handles
    /// which can attach to this socket to those who have the right Tag.
    /// This construction is using the standard 
    /// <a href="https://docs.unity3d.com/Manual/Tags.html">Unity tags</a>.
    /// 
    /// Events
    /// ======
    /// * \ref Socket::attachEvent "Attach Events"
    /// 
    /// Attachment Implementation
    /// =========================
    /// Depending on the type of socket and its GameObject handles are attached to sockets in various ways.
    /// 
    /// A Static Socket...
    /// -------------------
    /// ...with a Non-Kinematic Rigidbody Handle
    /// -----------------------------------------
    /// A non-kinematic Rigidbody will get a Joint which limits its movement to 
    /// the location and orientation of the Socket. This joint is implemented using a Configurable Joint.
    /// When the Handle is released, the joint is destroyed again.
    /// 
    /// ...with a Kinematic Rigidbody Handle
    /// --------------------------------------
    /// A kinematic Rigidbody with an handle will the parented to the static Socket Transform.
    /// The result will be that the Transform of the Rigidbody is a child of the Socket Transform.
    /// The Rigidbody will be temporarily removed.The settings will be conserved
    /// in a RigidbodyDisabled Component which is used to restore the Rigidbody when the Handle
    /// is released from the Socket.
    /// 
    /// ...with a Static Handle
    /// ------------------------
    /// Static Handles cannot be attached to static Sockets.
    /// This will lead to a warning message in the Console: “Cannot attach static handle to static socket”.
    /// 
    /// A Kinematic Rigidbody Socket...
    /// -------------------------------
    /// ...with a Non-Kinematic Rigidbody Handle
    /// -----------------------------------------
    /// If the Non-Kinematic Rigidbody Handle has one or more Joints or when it has constraints set,
    /// it will be attached to the Socket using a Joint.
    /// This joint is implemented using a Configurable Joint.
    /// When the Handle is released, the joint is destroyed again.
    /// In all other case, the non-kinematic Rigidbody will the parented to 
    /// the kinematic Rigidbody Socket Transform.
    /// The result will be that the Transform of the Rigidbody is a child of the Socket Transform.
    /// The Rigidbody will be temporarily removed.
    /// The settings will be conserved in a RigidbodyDisabled Component
    /// which is used to restore the Rigidbody when the Handle is released from the Socket.
    /// 
    /// ...with a Kinematic Rigidbody Handle
    /// -------------------------------------
    /// A kinematic Rigidbody with an handle will the parented to the kinematic Rigidbody Socket Transform.
    /// The result will be that the Transform of the Rigidbody is a child of the Socket Transform.
    /// 
    /// ...with a Static Handle
    /// ------------------------
    /// In this case, the kinematic Rigidbody Socket is parented to the static Handle Transform.
    /// The result will be that the Transform of the Scoket is a child of the Handle Transform.
    /// The Rigidbody of the Socket will be temporarily removed.
    /// The settings will be conserved in a RigidbodyDisabled Component
    /// which is used to restore the Rigidbody when the Handle is released from the Socket.
    /// 
    /// A Non-Kinematic Rigidbody Socket...
    /// -----------------------------------
    /// ...with a Non-Kinematic Rigidbody Handle
    /// ----------------------------------------
    /// If the Non-Kinematic Rigidbody Handle has one or more Joints or when it has constraints set,
    /// it will be attached to the Socket using a Joint.
    /// This joint is implemented using a Configurable Joint.
    /// When the Handle is released, the joint is destroyed again.
    /// In all other cases, the non-kinematic Rigidbody will the parented to
    /// the kinematic Rigidbody Socket Transform.
    /// The result will be that the Transform of the Rigidbody is a child of the Socket Transform.
    /// The Rigidbody will be temporarily removed.
    /// The settings will be conserved in a RigidbodyDisabled Component
    /// which is used to restore the Rigidbody when the Handle is released from the Socket.
    /// 
    /// ...with a Kinematic Rigidbody Handle
    /// ------------------------------------
    /// A kinematic Rigidbody with an handle will the parented to the non-kinematic Socket Transform.
    /// The result will be that the Transform of the Rigidbody is a child of the Socket Transform.
    /// The Rigidbody will be temporarily removed.
    /// The settings will be conserved in a RigidbodyDisabled Component
    /// which is used to restore the Rigidbody when the Handle is released from the Socket.
    /// 
    /// ...with a Static Handle
    /// -----------------------
    /// In this case, the socket Rigidbody will be attached to the Handle using a Joint.
    /// This joint is implemented using a Configurable Joint
    /// such that it will follow the Handle’s Transform as closely as possible.
    /// When the Handle is released, the joint is destroyed again.
    [HelpURL("https://passervr.com/apis/HumanoidControl/Unity/class_passer_1_1_socket.html")]
    public class Socket : MonoBehaviour {

        protected static void DebugLog(string s) {
            //Debug.Log(s);
        }

        /// <summary>
        /// Does the socket currently have a handle attached?
        /// </summary>
        public bool isOccupied {
            get {
                return this.attachedTransform != null;
            }
        }

        /// <summary>
        /// A prefab which is used to attach to the socket at startup.
        /// </summary>
        public GameObject attachedPrefab;
        /// <summary>The Transform attached to this socket</summary>
        /// If the socket holds a Handle, this will contain the Transform of the Handle.
        /// It will be null otherwise
        public Transform attachedTransform;
        protected Transform releasingTransform;

        public Handle attachedHandle;

        /// <summary>The parent of the attached transform before it was attached</summary>
        /// This is used to restore the parent when the transform is released again.
        protected Transform attachedTransformParent;

        /// <summary>A tag for limiting which handles can be held by the socket</summary>
        /// If set (not null or empty) only Handles with the given tag will fit in the socket.
        public string socketTag;

        public enum AttachMethod {
            Unknown,
            Parenting,
            ReverseParenting,
            Joint,
            StaticJoint,
            ColliderDuplication
        }
        public AttachMethod attachMethod = AttachMethod.Unknown;

        protected RigidbodyDisabled rigidbodyDisabled = null;

        public bool destroyOnLoad = false;

        #region Alignment

        protected virtual void MoveHandleToSocket(Transform socketTransform, Rigidbody handleRigidbody, Handle handle) {
            DebugLog("MoveHandleToSocket");

            Transform handleTransform = handle.GetComponent<Transform>();
            if (handleRigidbody != null)
                handleTransform = handleRigidbody.transform;

            if (handle.grabType == Handle.GrabType.RailGrab) {
                MoveRailToSocket(socketTransform, handleTransform, handle);
            }
            else {
                handleTransform.rotation = handle.RotationTo(socketTransform.rotation) * handleTransform.rotation;
                handleTransform.position += handle.TranslationTo(socketTransform.position);
            }
        }

        protected virtual void MoveRailToSocket(Transform socketTransform, Transform railTransform, Handle rail) {
            Vector3 handleYaxis = rail.transform.up;
            Vector3 socketYaxis = socketTransform.up;
            float angle = Vector3.Angle(handleYaxis, socketYaxis);
            if (angle > 90)
                socketYaxis = -socketTransform.up;

            Quaternion socketToHandleRotation = Quaternion.FromToRotation(socketYaxis, handleYaxis);
            Quaternion targetRotation = Quaternion.Inverse(socketToHandleRotation) * railTransform.rotation;

            railTransform.rotation = rail.RotationTo(targetRotation) * railTransform.rotation;

            // Socket along rail
            Vector3 localSocketPosition = socketTransform.position - rail.transform.position;
            Vector3 positionOnRail = Vector3.Project(localSocketPosition, rail.transform.up);
            Vector3 railToSocket = localSocketPosition - positionOnRail;

            // Socket within rail length
            float maxDistance = rail.transform.lossyScale.y / 2;
            float distance = positionOnRail.magnitude - maxDistance;
            if (distance > 0) {
                float scale = distance / positionOnRail.magnitude;
                railToSocket += Vector3.Scale(positionOnRail, Vector3.one * scale);
            }

            railTransform.position += railToSocket;
        }

        protected virtual void MoveHandleToSocket(Transform socketTransform, Handle handle) {
            DebugLog("MoveHandleToSocket");

            Transform handleTransform = handle.GetComponent<Transform>();
            Rigidbody handleRigidbody = handle.GetComponentInParent<Rigidbody>();
            if (handleRigidbody != null) {
                handleTransform = handleRigidbody.transform;

                MechanicalJoint handleLimitations = handle.GetComponent<MechanicalJoint>();
                if (handleRigidbody.isKinematic && handleLimitations != null && handleLimitations.enabled) {
                    //handleTransform.position += handle.TranslationTo(socketTransform.position);
                    // No rotations for kinematic Limitations
                    return;
                }
            }

            if (handle.grabType == Handle.GrabType.RailGrab) {
                MoveRailToSocket(socketTransform, handleTransform, handle);
            }
            else {
                handleTransform.rotation = handle.RotationTo(socketTransform.rotation) * handleTransform.rotation;
                handleTransform.position += handle.TranslationTo(socketTransform.position);
            }
        }

        protected virtual void MoveSocketToHandle(Transform socketTransform, Handle handle) {
            DebugLog("MoveSocketToHandle");

            socketTransform.rotation *= Quaternion.Inverse(handle.RotationTo(socketTransform.rotation));
            socketTransform.position -= handle.TranslationTo(socketTransform.position);
        }

        protected virtual void MoveSocketToHandle(Transform socketTransform, Rigidbody socketRigidbody, Handle handle) {
            DebugLog("MoveSocketToHandle");

            //Quaternion rotation = Quaternion.Inverse(handle.RotationTo(socketTransform.rotation));
            //socketRigidbody.transform.rotation *= rotation;

            //Vector3 translation = handle.TranslationTo(socketTransform.position);
            //socketRigidbody.transform.position -= translation;

            // code based on HandSocket, potentially better, but rotation is somehow wrong
            Quaternion socket2rigidbodyRotation = Quaternion.Inverse(socketTransform.localRotation);
            socketRigidbody.transform.rotation = handle.transform.rotation * socket2rigidbodyRotation;

            Vector3 socket2RigidbodyPosition = socketRigidbody.transform.position - socketTransform.position;
            socketRigidbody.transform.position = handle.transform.position + socket2RigidbodyPosition;

        }

        #endregion Alignment

        #region Attach

        public void Attach(GameObject objectToAttach) {
            Attach(objectToAttach, false);
        }
        public bool Attach(GameObject objectToAttach, bool rangeCheck) {
            bool success = Attach(objectToAttach.transform, rangeCheck);
            return success;
        }

        /// <summary>Tries to attach the given Transform to this socket</summary>
        /// If the Transform has a Handle with the right Socket Tag it will be attached to the socket.
        /// Static and Kinematic Rigidbodies will be attached by parenting.
        /// Non-Kinematic Rigidbodies will be attached using a joint.
        /// <param name="transformToAttach">The Transform to attach to this socket</param>
        /// <returns>Boolean indicating whether attachment succeeded</returns>
        public virtual bool Attach(Transform transformToAttach, bool rangeCheck = true) {
            DebugLog("Attach transform " + transformToAttach);

            Handle handle;

            Rigidbody rigidbodyToAttach = transformToAttach.GetComponentInParent<Rigidbody>();
            if (rigidbodyToAttach != null)
                handle = rigidbodyToAttach.GetComponentInChildren<Handle>();
            else
                handle = transformToAttach.GetComponentInChildren<Handle>();

            if (handle == null) {
                // Transform does not have a handle
                DebugLog(gameObject.name + ": Attach failed. Object " + transformToAttach.name + " does not have a handle");
                return false;
            }

            if (handle.socket != null) {
                // Handle is already in a socket
                return false;
            }

            if (socketTag != null && socketTag != "" && !transformToAttach.gameObject.CompareTag(socketTag)) {
                // Object did not have the right tag
                Debug.Log(gameObject.name + ": Attach failed. Object " + transformToAttach + " does not have the right tag"); ;
                return false;
            }

            Rigidbody handleRigidbody = handle.GetComponentInParent<Rigidbody>();
            if (handleRigidbody != rigidbodyToAttach) {
                // Object does not have a handle,
                // found handle is of child rigidbody
                //Debug.Log(gameObject.name + ": Attach failed. Object does not have a handle. Handle is on child Rigidbody.");
                return false;
            }

            bool success = Attach(handle, rangeCheck);
            return success;
        }

        /// <summary>Tries to attach the given Transform to this socket</summary>
        /// If the Handle has the right Socket Tag it will be attached to the socket.
        /// Static and Kinematic Rigidbodies will be attached by parenting.
        /// Non-Kinematic Rigidbodies will be attached using a joint.
        /// <param name="handle">The Handle to attach to this socket</param>
        /// <returns>Boolean indicating whether attachment succeeded</returns>
        public virtual bool Attach(Handle handle, bool rangeCheck = true) {

            DebugLog(this.gameObject.name + " Attach handle " + handle);
            if (handle == null) {
                // Transform does not have a handle
                DebugLog(gameObject.name + ": Attach failed. Transform does not have a handle");

                return false;
            }

            if (handle.socket != null) {
                // Handle is already in a socket
                return false;
            }

            if (socketTag != null && socketTag != "" && !handle.gameObject.CompareTag(socketTag)) {
                // Object did not have the right tag
                Debug.Log(gameObject.name + ": Attach failed. Handle " + handle + " does not have the right tag"); ;
                return false;
            }

            Rigidbody rigidbodyToAttach = handle.GetComponentInParent<Rigidbody>();
            if (rigidbodyToAttach == null)
                return AttachStaticObject(handle.transform, handle);
            else {
                RigidbodyDisabled disabledRigidbody = RigidbodyDisabled.Get(rigidbodyToAttach.transform);
                if (disabledRigidbody != null && rigidbodyToAttach.transform.parent != null)
                    rigidbodyToAttach = rigidbodyToAttach.transform.parent.GetComponentInParent<Rigidbody>();
                if (rigidbodyToAttach == null)
                    return AttachStaticObject(handle.transform, handle);
                else
                    return AttachRigidbody(rigidbodyToAttach, handle, rangeCheck);
            }
        }

        #region Rigidbody

        protected virtual bool AttachRigidbody(Rigidbody objRigidbody, Handle handle, bool rangeCheck = true) {
            DebugLog("AttachRigidbody " + objRigidbody);

            float grabDistance = Vector3.Distance(this.transform.position, handle.transform.position);
            if (rangeCheck && handle.range > 0 && grabDistance > handle.range) {
                //Debug.Log("Socket is outside range of handle");
                return false;
            }

            Transform objTransform = objRigidbody.transform;

            Rigidbody thisRigidbody = this.GetComponentInParent<Rigidbody>();
            Joint joint = objRigidbody.GetComponent<Joint>();
            // See if these joints are being destroyed
            DestroyedJoints destroyedJoints = objRigidbody.GetComponent<DestroyedJoints>();

            if (objRigidbody.isKinematic) {
                if (thisRigidbody == null)
                    AttachRigidbodyParenting(objRigidbody, handle);
                else if (thisRigidbody.isKinematic)
                    AttachTransformParenting(objRigidbody.transform, handle);
                else
                    AttachSocketParenting(objRigidbody, handle, thisRigidbody);
            }
            else if (thisRigidbody == null) {
                AttachRigidbodyReverseJoint(objRigidbody, handle);
            }
            else if (
                (joint != null && destroyedJoints == null) ||
                objRigidbody.constraints != RigidbodyConstraints.None
                ) {
                //|| otherHandPhysics != null) {

                AttachRigidbodyJoint(objRigidbody, handle);
            }
            else {
                AttachRigidbodyParenting(objRigidbody, handle);
            }

            releasingTransform = null;
            attachedTransform = objTransform;
            handle.socket = this;
            return true;
        }

        protected void AttachTransformParenting(Transform objTransform, Handle handle) {
            Debug.Log("AttachTransformParenting: " + objTransform);

            attachedTransformParent = objTransform.parent;

            Rigidbody handleRigidbody = handle.GetComponentInParent<Rigidbody>();
            MechanicalJoint handleLimitations = handleRigidbody.GetComponent<MechanicalJoint>();
            if (handleRigidbody.isKinematic && handleLimitations != null && handleLimitations.enabled) {
                MoveSocketToHandle(this.transform, handle);
            }
            else
                MoveHandleToSocket(this.transform, handle);

            objTransform.SetParent(this.transform);

            attachedTransform = objTransform;
            attachedHandle = handle;
            handle.socket = this;
        }

        protected virtual void AttachRigidbodyParenting(Rigidbody objRigidbody, Handle handle) {
            DebugLog("AttachRigidbodyParenting");

            MoveHandleToSocket(this.transform, objRigidbody, handle);

            attachedTransform = objRigidbody.transform;

            Rigidbody thisRigidbody = this.GetComponentInParent<Rigidbody>();
            if (thisRigidbody != null)
                MassRedistribution(thisRigidbody, objRigidbody);

            RigidbodyDisabled.ParentRigidbody(this.transform, objRigidbody);

            Humanoid.HumanoidNetworking.DisableNetworkSync(attachedTransform.gameObject);

            attachedHandle = handle;
            handle.socket = this;
        }

        protected virtual void AttachRigidbodyJoint(Rigidbody objRigidbody, Handle handle) {
            DebugLog("AttachRigidbodyJoint " + objRigidbody);

            //MassRedistribution(thisRididbody, objRigidbody);

            MoveHandleToSocket(this.transform, handle);

            ConfigurableJoint joint = this.gameObject.AddComponent<ConfigurableJoint>();
            joint.xMotion = ConfigurableJointMotion.Locked;
            joint.yMotion = ConfigurableJointMotion.Locked;
            joint.zMotion = ConfigurableJointMotion.Locked;

            joint.angularXMotion = ConfigurableJointMotion.Locked;
            joint.angularYMotion = ConfigurableJointMotion.Locked;
            joint.angularZMotion = ConfigurableJointMotion.Locked;

            joint.projectionMode = JointProjectionMode.PositionAndRotation;
            joint.projectionDistance = 0.01F;
            joint.projectionAngle = 1;

            Collider c = objRigidbody.transform.GetComponentInChildren<Collider>();
            joint.connectedBody = c.attachedRigidbody;

            attachedTransform = objRigidbody.transform;
            attachedHandle = handle;
            handle.socket = this;
        }

        /// <summary>Attach handle to socket using a static joint on the handle</summary>
        protected virtual void AttachRigidbodyReverseJoint(Rigidbody objRigidbody, Handle handle) {
            DebugLog("AttachRigidbodyReverseJoint " + objRigidbody);

            MoveHandleToSocket(this.transform, handle);

            ConfigurableJoint joint = objRigidbody.gameObject.AddComponent<ConfigurableJoint>();
            joint.xMotion = ConfigurableJointMotion.Locked;
            joint.yMotion = ConfigurableJointMotion.Locked;
            joint.zMotion = ConfigurableJointMotion.Locked;

            joint.angularXMotion = ConfigurableJointMotion.Locked;
            joint.angularYMotion = ConfigurableJointMotion.Locked;
            joint.angularZMotion = ConfigurableJointMotion.Locked;

            joint.projectionMode = JointProjectionMode.PositionAndRotation;
            joint.projectionDistance = 0.01F;
            joint.projectionAngle = 1;

            joint.breakForce = float.PositiveInfinity;
            joint.breakTorque = float.PositiveInfinity;

            attachedTransform = objRigidbody.transform;
            attachedHandle = handle;
            handle.socket = this;
        }

        protected virtual void AttachSocketParenting(Rigidbody objRigidbody, Handle handle, Rigidbody socketRigidbody) {
            DebugLog("AttachSocketParenting");

            RigidbodyDisabled.ParentRigidbody(objRigidbody, socketRigidbody);

            MoveSocketToHandle(this.transform, handle);

            attachedTransform = objRigidbody.transform;
            attachedHandle = handle;
            handle.socket = this;
            attachMethod = AttachMethod.ReverseParenting;
        }


        protected float originalMass = 1;
        protected bool originalUseGravity = false;
        protected virtual void MassRedistribution(Rigidbody socketRigidbody, Rigidbody objRigidbody) {
            originalMass = socketRigidbody.mass;
            socketRigidbody.mass = HybridPhysics.CalculateTotalMass(objRigidbody);

            originalUseGravity = socketRigidbody.useGravity;
            // This gives an issue when attaching things to a socket which is already in the hand
            // It will enable gravity on the hand in that case (not on the object in the hand)
            // Which gives less stable hand tracking.
            // Update: added additional check such that gravity is only enabled when
            // holding heavy objects
            if (objRigidbody.useGravity && objRigidbody.mass > HybridPhysics.kinematicMass)
                socketRigidbody.useGravity = true;
        }

        #endregion Rigidbody

        #region Static Object

        virtual protected bool AttachStaticObject(Transform objTransform, Handle handle) {
            DebugLog("AttachStaticObject " + objTransform);

            Rigidbody thisRigidbody = this.GetComponentInParent<Rigidbody>();
            if (thisRigidbody == null) {
                Debug.LogWarning("Cannot attach static handle to static socket");
                return false;
            }

            // To do: rangecheck

            if (thisRigidbody.isKinematic) {
                AttachSocketParenting(objTransform, handle, thisRigidbody);
            }
            else {
                MoveSocketToHandle(this.transform, handle);
                if (handle.grabType == Handle.GrabType.RailGrab)
                    AttachStaticJointRotY(objTransform);
                else
                    AttachStaticJoint(objTransform);
            }

            releasingTransform = null;
            attachedTransform = objTransform;
            attachedHandle = handle;
            handle.socket = this;
            return true;
        }

        protected virtual void AttachSocketParenting(Transform objTransform, Handle handle, Rigidbody thisRigidbody) {
            DebugLog("AttachSocketParenting");

            RigidbodyDisabled.ParentRigidbody(objTransform, thisRigidbody);

            MoveSocketToHandle(this.transform, handle);
        }

        public virtual void AttachStaticJoint(Transform objTransform) {
            DebugLog("AttachStaticJoint");

            FixedJoint joint = this.transform.gameObject.AddComponent<FixedJoint>();

            Collider c = objTransform.GetComponentInChildren<Collider>();
            if (c == null)
                c = objTransform.GetComponentInParent<Collider>();
            joint.connectedBody = c.attachedRigidbody;
        }

        virtual protected void AttachStaticJointRotY(Transform objTransform) {
            DebugLog("AttachStaticJointRotY is still fixed");

            FixedJoint joint = this.transform.gameObject.AddComponent<FixedJoint>();

            Collider c = objTransform.GetComponentInChildren<Collider>();
            if (c == null)
                c = objTransform.GetComponentInParent<Collider>();
            joint.connectedBody = c.attachedRigidbody;
        }

        #endregion Static Object

        #endregion Attach

        #region Release

        /// <summary>Releases a Transform from the socket</summary>
        /// Note that if the Transform is not taken out of the range of the socket
        /// or held by another Socket, it will automatically snap back to the Socket.
        public virtual void Release(bool releaseSticky = false) {
            DebugLog("Release " + attachedTransform);

            if (attachedTransform == null) {
                attachedHandle = null;
                return;
            }

            if (attachedHandle != null && attachedHandle.sticky && !releaseSticky)
                return;

            Rigidbody thisRigidbody = this.GetComponentInParent<Rigidbody>();
            Rigidbody objRigidbody = attachedTransform.GetComponentInParent<Rigidbody>();
            RigidbodyDisabled objRigidbodyDisabled = RigidbodyDisabled.Get(attachedTransform);
            // RigidbodyDisabled.GetInParent does not work well here.
            // When a socket is held in another socket, it can find the disabledRigidbody of the other socket
            //RigidbodyDisabled thisRigidbodyDisabled = RigidbodyDisabled.Get(this.transform);

            switch (attachMethod) {
                case AttachMethod.ReverseParenting:
                    if (this.rigidbodyDisabled != null)
                        ReleaseSocketParenting(objRigidbody, this.rigidbodyDisabled.transform);
                    else if (thisRigidbody != null)
                        ReleaseSocketParenting(objRigidbody, this.transform);
                    else {
                        Debug.LogError("Release ReverseParenting without a (distroyed) rigidbody present");
                        return;
                    }
                    break;
                default:

                    if (this.rigidbodyDisabled != null)
                        ReleaseSocketParenting(objRigidbody, this.rigidbodyDisabled.transform);
                    else if (objRigidbodyDisabled != null)
                        ReleaseRigidbodyParenting();
                    else if (objRigidbody == null)
                        ReleaseStaticObject();
                    else if (thisRigidbody != null && thisRigidbody.isKinematic)
                        ReleaseTransformParenting();
                    else if (!objRigidbody.isKinematic && thisRigidbody == null)
                        ReleaseRigidbodyReverseJoint();
                    else
                        ReleaseRigidbodyJoint();
                    break;
            }


            Handle handle = attachedHandle;
            if (handle != null) {
                handle.socket = null;
            }

            this.releasingTransform = this.attachedTransform;
            this.attachedTransform = null;
            this.attachedHandle = null;
            attachMethod = AttachMethod.Unknown;

            StartCoroutine(ClearReleasingTransform());
        }

        // Released objects should be gone within 1 second or they will be reattached
        protected IEnumerator ClearReleasingTransform() {
            yield return new WaitForSeconds(1);
            this.releasingTransform = null;
        }

        #region Rigidbody

        protected virtual void ReleaseRigidbodyParenting() {
            DebugLog("Release Rigidbody from Parenting");

            Rigidbody thisRigidbody = this.GetComponentInParent<Rigidbody>();
            Rigidbody objRigidbody;
            if (thisRigidbody == null)
                objRigidbody = RigidbodyDisabled.UnparentRigidbody(this.transform, attachedTransform);
            else
                objRigidbody = RigidbodyDisabled.UnparentRigidbody(thisRigidbody, attachedTransform);

            MassRestoration(thisRigidbody, objRigidbody);

            Humanoid.HumanoidNetworking.ReenableNetworkSync(objRigidbody.gameObject);

            if (thisRigidbody != null) {
                objRigidbody.velocity = thisRigidbody.velocity;
                objRigidbody.angularVelocity = thisRigidbody.angularVelocity;
            }

            // check if the object is released from an hybrid physics rigidbody (just hands for now)
            Humanoid.AdvancedHandPhysics handPhysics = this.transform.GetComponentInParent<Humanoid.AdvancedHandPhysics>();
            if (handPhysics != null) {
                Collider[] objColliders = objRigidbody.GetComponentsInChildren<Collider>();
                foreach (Collider objCollider in objColliders)
                    Target.UnsetColliderToTrigger(handPhysics.handTarget.colliders, objCollider);
            }
        }

        protected virtual void ReleaseRigidbodyJoint() {
            DebugLog("Release from Joint");

            Joint[] joints = this.gameObject.GetComponents<Joint>();
            foreach (Joint joint in joints) {
#if UNITY_EDITOR
                if (!Application.isPlaying)
                    DestroyImmediate(joint, true);
                else
#endif
                    Destroy(joint);
            }
            //MassRestoration(..., ...);

            // Trick: when released and immediately attached to anther socket (e.g. grabbing)
            // the joints are not yet destroyed, because Destroy is executed with a delay.
            // Adding the DestroyedJoints component indicates that the joints which may
            // still be there are to be destroyed.
            attachedTransform.gameObject.AddComponent<DestroyedJoints>();
        }

        protected void ReleaseRigidbodyReverseJoint() {
            DebugLog("Release from reverse Joint");

            Joint[] joints = attachedTransform.GetComponents<Joint>();
            foreach (Joint joint in joints) {
#if UNITY_EDITOR
                if (!Application.isPlaying)
                    DestroyImmediate(joint, true);
                else
#endif
                    Destroy(joint);
            }
            //MassRestoration(..., ...);

            // Trick: when released and immediately attached to anther socket (e.g. grabbing)
            // the joints are not yet destroyed, because Destroy is executed with a delay.
            // Adding the DestroyedJoints component indicates that the joints which may
            // still be there are to be destroyed.
            attachedTransform.gameObject.AddComponent<DestroyedJoints>();
        }
        protected virtual void ReleaseSocketParenting(Rigidbody objRigidbody, Transform socketTransform) {
            DebugLog("ReleaseSocketParenting");

            RigidbodyDisabled.UnparentRigidbody(objRigidbody, socketTransform);
        }

        protected virtual void MassRestoration(Rigidbody socketRigidbody, Rigidbody objRigidbody) {
            if (socketRigidbody != null) {
                socketRigidbody.mass = originalMass;
                socketRigidbody.useGravity = originalUseGravity;
            }
        }

        #endregion Rigidbody

        #region Static Object

        protected virtual void ReleaseStaticObject() {
            DebugLog("ReleaseStaticObject");

            Rigidbody thisRigidbody = this.GetComponent<Rigidbody>();
            RigidbodyDisabled thisDisabledRigidbody = RigidbodyDisabled.Get(this.transform);

            if (thisRigidbody != null)
                ReleaseStaticJoint();
            else if (thisDisabledRigidbody != null)
                ReleaseSocketParenting(attachedTransform);
            else
                ReleaseTransformParenting();
        }

        public virtual void ReleaseStaticJoint() {
            DebugLog("ReleaseStaticJoint");

            Joint[] joints = this.GetComponents<Joint>();
            foreach (Joint joint in joints) {
#if UNITY_EDITOR
                if (!Application.isPlaying)
                    DestroyImmediate(joint, true);
                else
#endif
                    Destroy(joint);
            }
        }

        protected void ReleaseTransformParenting() {
            DebugLog("Release Transform from Parenting");

            attachedTransform.SetParent(attachedTransformParent);
        }

        protected void ReleaseSocketParenting(Transform objTransform) {
            DebugLog("ReleaseSocketParenting");

            RigidbodyDisabled.UnparentRigidbody(objTransform, this.transform);
        }

        #endregion Static Object

        #endregion Release

        #region Start

        protected virtual void Awake() {
            UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnload;
        }

        #endregion Start

        #region Update
        protected virtual void Update() {
            UpdateHolding();
        }
        #endregion

        #region Events

        protected static string[] attachEventTypeLabels = {
                "Never",
                "On Attach",
                "On Release",
                "While Attached",
                "While Released",
                "When Changed",
                "Always"
        };

        /// <summary>A GameObject Event for triggering changes in the Transform held by the Socket</summary>
        public GameObjectEventHandlers attachEvent = new GameObjectEventHandlers() {
            label = "Hold Event",
            tooltip =
                "Call functions using what the socket is holding\n" +
                "Parameter: the GameObject held by the socket",
            eventTypeLabels = attachEventTypeLabels
        };

        protected void UpdateHolding() {
            if (attachedTransform != null)
                attachEvent.value = attachedTransform.gameObject;
            else
                attachEvent.value = null;
        }

        protected virtual void OnSceneUnload(UnityEngine.SceneManagement.Scene _) {
            if (destroyOnLoad) {
                if (attachedTransform != null) {
                    Destroy(attachedTransform.gameObject);
                }
            }
        }


        #endregion

        #region Gizmos
        protected Mesh gizmoMesh;

        public void OnDrawGizmosSelected() {
            if (gizmoMesh == null)
                gizmoMesh = GenerateGizmoMesh1();
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireMesh(gizmoMesh, transform.position, transform.rotation);
        }

        public static Mesh GenerateGizmoMesh1() {
            Vector3 p0 = new Vector3(-0.01F, 0.03F, -0.01F);
            Vector3 p1 = new Vector3(-0.01F, -0.03F, -0.01F);
            Vector3 p2 = new Vector3(-0.01F, -0.03F, 0.01F);
            Vector3 p3 = new Vector3(-0.01F, 0.01F, 0.01F);
            Vector3 p4 = new Vector3(-0.01F, 0.01F, 0.03F);
            Vector3 p5 = new Vector3(-0.01F, 0.03F, 0.03F);

            Vector3 p6 = new Vector3(0.01F, 0.03F, -0.01F);
            Vector3 p7 = new Vector3(0.01F, -0.03F, -0.01F);
            Vector3 p8 = new Vector3(0.01F, -0.03F, 0.01F);
            Vector3 p9 = new Vector3(0.01F, 0.01F, 0.01F);
            Vector3 p10 = new Vector3(0.01F, 0.01F, 0.03F);
            Vector3 p11 = new Vector3(0.01F, 0.03F, 0.03F);

            Vector3[] vertices = {
                p0, p1, p6, p7,     // 0, 1, 2, 3
                p1, p2, p7, p8,     // 4, 5, 6, 7
                p2, p3, p8, p9,     // 8, 9, 10, 11
                p3, p4, p9, p10,    // 12, 13, 14, 15
                p4, p5, p10, p11,   // 16, 17, 18, 19
                p5, p0, p11, p6,    // 20, 21, 22, 23

                //p0, p1, p2, p3, p4, p5,     // 24, 25, 26, 27, 28, 29
                //p6, p7, p8, p9, p10, p11,   // 30, 31, 32, 33, 34, 35
            };
            int[] triangles = {
                0, 2, 2,     3, 1, 1,   0, 1, 1,    3, 2, 2,
                4, 6, 6,     7, 5, 5,   4, 5, 5,    7, 6, 6,
                8, 10, 10,   11, 9, 9,  8, 9, 9,    11, 10, 10,
                12, 14, 14,  15, 13, 13,    12, 13, 13,     15, 14, 14,
                16, 18, 18,  19, 17, 17,    16, 17, 17,     19, 18, 18,
                20, 22, 22,  23, 21, 21,    20, 21, 21,     23, 22, 22,

            };
            Vector3[] normals = {
                Vector3.back, Vector3.back, Vector3.back, Vector3.back, //back
                Vector3.down, Vector3.down, Vector3.down, Vector3.down, //down  
                Vector3.forward, Vector3.forward, Vector3.forward, Vector3.forward, //forward
                Vector3.down, Vector3.down, Vector3.down, Vector3.down, //down  
                Vector3.forward, Vector3.forward, Vector3.forward, Vector3.forward, //forward
                Vector3.up, Vector3.up, Vector3.up, Vector3.up, //up

                //Vector3.left, Vector3.left, Vector3.left, Vector3.left, Vector3.left, Vector3.left,
                //Vector3.right, Vector3.right, Vector3.right, Vector3.right, Vector3.right, Vector3.right,
            };
            Mesh gizmoMesh = new Mesh() {
                vertices = vertices,
                triangles = triangles,
                normals = normals
            };
            return gizmoMesh;
        }

        public static Mesh GenerateGizmoMesh() {
            Vector3 p0 = new Vector3(-0.01F, 0.03F, -0.01F);
            Vector3 p1 = new Vector3(-0.01F, -0.03F, -0.01F);
            Vector3 p2 = new Vector3(-0.01F, -0.03F, 0.01F);
            Vector3 p3 = new Vector3(-0.01F, 0.01F, 0.01F);
            Vector3 p4 = new Vector3(-0.01F, 0.01F, 0.03F);
            Vector3 p5 = new Vector3(-0.01F, 0.03F, 0.03F);

            Vector3 p6 = new Vector3(0.01F, 0.03F, -0.01F);
            Vector3 p7 = new Vector3(0.01F, -0.03F, -0.01F);
            Vector3 p8 = new Vector3(0.01F, -0.03F, 0.01F);
            Vector3 p9 = new Vector3(0.01F, 0.01F, 0.01F);
            Vector3 p10 = new Vector3(0.01F, 0.01F, 0.03F);
            Vector3 p11 = new Vector3(0.01F, 0.03F, 0.03F);

            Vector3[] vertices = {
                p0, p1, p6, p7,     // 0, 1, 2, 3
                p1, p2, p7, p8,     // 4, 5, 6, 7
                p2, p3, p8, p9,     // 8, 9, 10, 11
                p3, p4, p9, p10,    // 12, 13, 14, 15
                p4, p5, p10, p11,   // 16, 17, 18, 19
                p5, p0, p11, p6,    // 20, 21, 22, 23

                p0, p1, p2, p3, p4, p5,     // 24, 25, 26, 27, 28, 29
                p6, p7, p8, p9, p10, p11,   // 30, 31, 32, 33, 34, 35
            };
            int[] triangles = {
                0, 2, 1,     3, 1, 2,
                4, 6, 5,     7, 5, 6,
                8, 10, 9,    11, 9, 10,
                12, 14, 13,  15, 13, 14,
                16, 18, 17,  19, 17, 18,
                20, 22, 21,  23, 21, 22,
                27,24,25,   27,25,26,   29,27,28,   27,29,24,
                30,33,31,   31,33,32,   33,35,34,   35,33,30,
            };
            Vector3[] normals = {
                Vector3.back, Vector3.back, Vector3.back, Vector3.back, //back
                Vector3.down, Vector3.down, Vector3.down, Vector3.down, //down  
                Vector3.forward, Vector3.forward, Vector3.forward, Vector3.forward, //forward
                Vector3.down, Vector3.down, Vector3.down, Vector3.down, //down  
                Vector3.forward, Vector3.forward, Vector3.forward, Vector3.forward, //forward
                Vector3.up, Vector3.up, Vector3.up, Vector3.up, //up

                Vector3.left, Vector3.left, Vector3.left, Vector3.left, Vector3.left, Vector3.left,
                Vector3.right, Vector3.right, Vector3.right, Vector3.right, Vector3.right, Vector3.right,
            };
            Mesh gizmoMesh = new Mesh() {
                vertices = vertices,
                triangles = triangles,
                normals = normals
            };
            return gizmoMesh;
        }
        #endregion
    }

    // <summary>Component to indicate that the joints are being destroyed</summary>
    // When a rigidbody is released and immediately attached to anther socket (e.g. grabbing)
    // the joints may not yet be destroyed when they are grabbed, because Destroy is executed with a delay.
    // Adding the DestroyedJoints component indicates that the joints which may
    // still be there are to be destroyed.
    public class DestroyedJoints : MonoBehaviour {
        private void LateUpdate() {
            Destroy(this);
        }
    }
}