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


namespace BNG {

    public class PunctureCollider : MonoBehaviour {

        [Header("Puncture properties : ")]

        [Tooltip("Minimum distance (in meters) an object must be attached once punctured. Upon initial puncture the object will be inserted this distance from the puncture point.")]
        public float FRequiredPenetrationForce = 150f;

        [Tooltip("Minimum distance (in meters) an object must be attached once punctured. Upon initial puncture the object will be inserted this distance from the puncture point.")]
        public float MinPenetration = 0.01f;

        [Tooltip("Minimum distance the object can be penetrated (in meters).")]
        public float MaxPenetration = 0.2f;

        [Tooltip("How far away the object must be from it's entry point to consider breaking the joint. Set to 0 if you do not want to break the joint based on distance.")]
        public float BreakDistance = 0.2f;

        [Tooltip("How far away the object must be from it's entry point to consider breaking the joint. Set to 0 if you do not want to break the joint based on distance.")]
        public List<Collider> PunctureColliders;

        [Header("Shown for Debug : ")]
        [Tooltip("Is the object currently embedded in another object?")]
        public bool HasPunctured = false;

        [Tooltip("The object currently embedded in")]
        public GameObject PuncturedObject;

        [Tooltip("How far (in meters) our object has been embedded into")]
        public float PunctureValue;
        float previousPunctureValue;

        Collider col;
        Collider hitCollilder;
        Collider[] ignoreColliders;
        Rigidbody rigid;
        GameObject jointHelper;
        Rigidbody jointHelperRigid;
        ConfigurableJoint jointHelperJoint;
        Grabbable thisGrabbable;
        FixedJoint fj;

        // Used to store min / max puncture values
        float yPuncture, yPunctureMin, yPunctureMax;

        void Start() {
            col = GetComponent<Collider>();
            rigid = col.attachedRigidbody;
            ignoreColliders = GetComponentsInChildren<Collider>();
            thisGrabbable = GetComponent<Grabbable>();
        }

        public float TargetDistance;

        public void FixedUpdate() {
            UpdatePunctureValue();
            CheckBreakDistance();
            CheckPunctureRelease();
            AdjustJointMass();
            ApplyResistanceForce();

            if(jointHelperJoint) {
                TargetDistance = Vector3.Distance(jointHelperJoint.targetPosition, jointHelperJoint.transform.position);
            }
        }
                
        // Get distance of puncture and move up / down if possible
        public virtual void UpdatePunctureValue() {

            if (HasPunctured && PuncturedObject != null && jointHelper != null) {
                // How far away from the pouncture point we are on the Y axis
                PunctureValue = transform.InverseTransformVector(jointHelper.transform.position - PuncturedObject.transform.position).y * -1;
                if (PunctureValue > 0 && PunctureValue < 0.0001f) {
                    PunctureValue = 0;
                }
                if (PunctureValue < 0 && PunctureValue > -0.0001f) {
                    PunctureValue = 0;
                }

                if (PunctureValue > 0.001f) {
                    MovePunctureUp();
                }
                else if (PunctureValue < -0.001f) {
                    MovePunctureDown();
                }
            }
            else {
                PunctureValue = 0;
            }
        }

        public virtual void MovePunctureUp() {

            jointHelperJoint.autoConfigureConnectedAnchor = false;

            float updatedYValue = jointHelperJoint.connectedAnchor.y + (Time.deltaTime);

            // Set min / max
            if (updatedYValue > yPunctureMin) {
                updatedYValue = yPunctureMin;
            }
            else if (updatedYValue < yPunctureMax) {
                updatedYValue = yPunctureMax;
            }

            // Apply the changes
            jointHelperJoint.connectedAnchor = new Vector3(jointHelperJoint.connectedAnchor.x, updatedYValue, jointHelperJoint.connectedAnchor.z);
        }

        public virtual void MovePunctureDown() {
            jointHelperJoint.autoConfigureConnectedAnchor = false;

            float updatedYValue = jointHelperJoint.connectedAnchor.y - (Time.deltaTime);

            if (updatedYValue > yPunctureMin) {
                updatedYValue = yPunctureMin;
            }
            else if (updatedYValue < yPunctureMax) {
                updatedYValue = yPunctureMax;
            }

            // Apply the changes
            jointHelperJoint.connectedAnchor = new Vector3(jointHelperJoint.connectedAnchor.x, updatedYValue, jointHelperJoint.connectedAnchor.z);
        }

        public virtual void CheckBreakDistance() {
            if (BreakDistance != 0 && HasPunctured && PuncturedObject != null && jointHelper != null) {
                if (PunctureValue > BreakDistance) {
                    ReleasePuncture();
                }
            }
        }

        public virtual void CheckPunctureRelease() {
            // Did an object get updated?
            if (HasPunctured && (PuncturedObject == null || jointHelper == null)) {
                ReleasePuncture();
            }
        }

        public virtual void AdjustJointMass() {
            // If this is a grabbable object we can adjust the physics a bit to make this smoother while being held
            if (thisGrabbable != null && jointHelperJoint != null) {
                // If being held, fix the mass scale so our mass is greater
                if (HasPunctured && thisGrabbable.BeingHeld) {
                    jointHelperJoint.massScale = 1f;
                    jointHelperJoint.connectedMassScale = 0.0001f;
                }
                // Otherwise use the default
                else {
                    jointHelperJoint.massScale = 1f;
                    jointHelperJoint.connectedMassScale = 1f;
                }
            }
        }

        // Apply a resistance force to the object if currently inserted
        public virtual void ApplyResistanceForce() {
            if (HasPunctured) {

                // Currently only apply resistance if holding the object
                if(thisGrabbable != null && thisGrabbable.BeingHeld) {
                    float punctureDifference = previousPunctureValue - PunctureValue;
                    // Apply opposing force
                    if (punctureDifference != 0 && Mathf.Abs(punctureDifference) > 0.0001f) {
                        rigid.AddRelativeForce(rigid.transform.up * punctureDifference, ForceMode.VelocityChange);
                    }
                }

                // Store our previous puncture value so we can compare it later
                previousPunctureValue = PunctureValue;
            }
            else {
                previousPunctureValue = 0;
            }
        }

        public virtual void DoPuncture(Collider colliderHit, Vector3 connectPosition) {

            // Bail early if no rigidbody is present
            if(colliderHit == null || colliderHit.attachedRigidbody == null) {
                return;
            }

            hitCollilder = colliderHit;
            PuncturedObject = hitCollilder.attachedRigidbody.gameObject;

            // Ignore physics with this collider            
            for (int x = 0; x < ignoreColliders.Length; x++) {
                Physics.IgnoreCollision(ignoreColliders[x], hitCollilder, true);
            }

            // Set up the joint helpter
            if (jointHelper == null) {
                // Set up config joint
                jointHelper = new GameObject("JointHelper");
                jointHelper.transform.parent = null;
                jointHelperRigid = jointHelper.AddComponent<Rigidbody>();

                jointHelper.transform.position = PuncturedObject.transform.position;
                jointHelper.transform.rotation = transform.rotation;

                jointHelperJoint = jointHelper.AddComponent<ConfigurableJoint>();
                jointHelperJoint.connectedBody = rigid;
                jointHelperJoint.autoConfigureConnectedAnchor = true;

                jointHelperJoint.xMotion = ConfigurableJointMotion.Locked;
                jointHelperJoint.yMotion = ConfigurableJointMotion.Limited;
                jointHelperJoint.zMotion = ConfigurableJointMotion.Locked;

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

                // Set out current puncture state. This is our 0-based location
                yPuncture = jointHelperJoint.connectedAnchor.y;
                yPunctureMin = yPuncture - MinPenetration;
                yPunctureMax = yPuncture - MaxPenetration;

                // Start the object punctured in a bit
                SetPenetration(MinPenetration);
            }

            // Attach fixed joint to our helper
            fj = PuncturedObject.AddComponent<FixedJoint>();
            fj.connectedBody = jointHelperRigid;
            //fj.massScale = 1;
            //fj.connectedMassScale = 100;

            HasPunctured = true;
        }

        /// <summary>
        /// Set penetration amount between MinPenetration and MaxPenetration
        /// </summary>
        /// <param name="penetrationAmount"></param>
        public void SetPenetration(float penetrationAmount) {

            float minPenVal = yPuncture - MinPenetration;
            float maxPenVal = yPuncture - MaxPenetration;
            float currentPenVal = yPuncture - penetrationAmount;

            float formattedPenVal = Mathf.Clamp(currentPenVal, maxPenVal, minPenVal);

            if (jointHelperJoint != null && jointHelperJoint.connectedAnchor != null) {

                // Make sure we aren't still in auto config mode
                jointHelperJoint.autoConfigureConnectedAnchor = false;
                
                jointHelperJoint.connectedAnchor = new Vector3(jointHelperJoint.connectedAnchor.x, formattedPenVal, jointHelperJoint.connectedAnchor.z);
            }
        }

        public void ReleasePuncture() {

            if(HasPunctured) {

                // Unignore the colliders
                for (int x = 0; x < ignoreColliders.Length; x++) {
                    // Colliders may have changed, make sure they are still valid before unignoring
                    if(ignoreColliders[x] != null && hitCollilder != null) {
                        Physics.IgnoreCollision(ignoreColliders[x], hitCollilder, false);
                    }
                }

                // Disconnect the jointHelper
                if(jointHelperJoint) {
                    jointHelperJoint.connectedBody = null;
                    GameObject.Destroy(jointHelper);
                }

                if(fj) {
                    fj.connectedBody = null;
                }

                // Disconnect FixedJoint
                GameObject.Destroy(fj);
            }

            PuncturedObject = null;
            HasPunctured = false;
        }

        public virtual bool CanPunctureObject(GameObject go) {
            // Override this method if you have custom puncture logic
            Rigidbody rigid = go.GetComponent<Rigidbody>();

            // Don't currently support kinematic objects
            if(rigid != null && rigid.isKinematic) {
                return false;
            }

            // Don't support static objects since joint can't be moved
            if(go.isStatic) {
                return false;
            }

            return true;
        }

        void OnCollisionEnter(Collision collision) {

            ContactPoint contact = collision.contacts[0];
            Vector3 hitPosition = contact.point;
            Quaternion hitRotation = Quaternion.FromToRotation(Vector3.up, contact.normal);
            float collisionForce = collision.impulse.magnitude / Time.fixedDeltaTime;

            // Debug.Log("Collision Force : " + collisionForce);

            // Do puncture
            if (collisionForce > FRequiredPenetrationForce && CanPunctureObject(collision.collider.gameObject) && !HasPunctured) {
                DoPuncture(collision.collider, hitPosition);
            }
        }
    }
}