// Bezier Pointer Renderer|PointerRenderers|10030
namespace VRTK
{
    using UnityEngine;

    /// <summary>
    /// A visual pointer representation of a curved beam made from multiple objects with an optional cursor at the end.
    /// </summary>
    /// <remarks>
    ///   > The bezier curve generation code is in another script located at `VRTK/Source/Scripts/Internal/VRTK_CurveGenerator.cs` and was heavily inspired by the tutorial and code from [Catlike Coding](http://catlikecoding.com/unity/tutorials/curves-and-splines/).
    ///
    /// **Optional Components:**
    ///  * `VRTK_PlayAreaCursor` - A Play Area Cursor that will track the position of the pointer cursor.
    ///  * `VRTK_PointerDirectionIndicator` - A Pointer Direction Indicator that will track the position of the pointer cursor.
    /// 
    /// **Script Usage:**
    ///  * Place the `VRTK_BezierPointerRenderer` script on the same GameObject as the Pointer script it is linked to.
    ///  * Link this Pointer Renderer script to the `Pointer Renderer` parameter on the required Pointer script.
    ///
    /// **Script Dependencies:**
    ///  * A Pointer script to control the activation of this Pointer Renderer script.
    /// </remarks>
    /// <example>
    /// `VRTK/Examples/009_Controller_BezierPointer` is used in conjunction with the Height Adjust Teleporter shows how it is possible to traverse different height objects using the curved pointer without needing to see the top of the object.
    ///
    /// `VRTK/Examples/036_Controller_CustomCompoundPointer' shows how to display an object (a teleport beam) only if the teleport location is valid, and can create an animated trail along the tracer curve.
    /// </example>
    [AddComponentMenu("VRTK/Scripts/Pointers/Pointer Renderers/VRTK_BezierPointerRenderer")]
    public class VRTK_BezierPointerRenderer : VRTK_BasePointerRenderer
    {
        [Header("Bezier Pointer Appearance Settings")]

        [Tooltip("The maximum length of the projected beam. The x value is the length of the forward beam, the y value is the length of the downward beam.")]
        public Vector2 maximumLength = new Vector2(10f, float.PositiveInfinity);
        [Tooltip("The number of items to render in the bezier curve tracer beam. A high number here will most likely have a negative impact of game performance due to large number of rendered objects.")]
        public int tracerDensity = 10;
        [Tooltip("The size of the ground cursor. This number also affects the size of the objects in the bezier curve tracer beam. The larger the radius, the larger the objects will be.")]
        public float cursorRadius = 0.5f;

        [Header("Bezier Pointer Render Settings")]

        [Tooltip("The maximum angle in degrees of the origin before the beam curve height is restricted. A lower angle setting will prevent the beam being projected high into the sky and curving back down.")]
        [Range(1, 100)]
        public float heightLimitAngle = 100f;
        [Tooltip("The amount of height offset to apply to the projected beam to generate a smoother curve even when the beam is pointing straight.")]
        public float curveOffset = 1f;
        [Tooltip("Rescale each tracer element according to the length of the Bezier curve.")]
        public bool rescaleTracer = false;
        [Tooltip("The cursor will be rotated to match the angle of the target surface if this is true, if it is false then the pointer cursor will always be horizontal.")]
        public bool cursorMatchTargetRotation = false;
        [Tooltip("The number of points along the bezier curve to check for an early beam collision. Useful if the bezier curve is appearing to clip through teleport locations. 0 won't make any checks and it will be capped at `Pointer Density`. The higher the number, the more CPU intensive the checks become.")]
        public int collisionCheckFrequency = 0;

        [Header("Bezier Pointer Custom Appearance Settings")]

        [Tooltip("A custom game object to use as the appearance for the pointer tracer. If this is empty then a collection of Sphere primitives will be created and used.")]
        public GameObject customTracer;
        [Tooltip("A custom game object to use as the appearance for the pointer cursor. If this is empty then a Cylinder primitive will be created and used.")]
        public GameObject customCursor;
        [Tooltip("A custom game object can be applied here to appear only if the location is valid.")]
        public GameObject validLocationObject = null;
        [Tooltip("A custom game object can be applied here to appear only if the location is invalid.")]
        public GameObject invalidLocationObject = null;

        protected VRTK_CurveGenerator actualTracer;
        protected GameObject actualContainer;
        protected GameObject actualCursor;
        protected GameObject actualValidLocationObject = null;
        protected GameObject actualInvalidLocationObject = null;
        protected Vector3 fixedForwardBeamForward;

        /// <summary>
        /// The UpdateRenderer method is used to run an Update routine on the pointer.
        /// </summary>
        public override void UpdateRenderer()
        {
            if ((controllingPointer != null && (controllingPointer.IsPointerActive()) || IsVisible()))
            {
                Vector3 jointPosition = ProjectForwardBeam();
                Vector3 downPosition = ProjectDownBeam(jointPosition);
                AdjustForEarlyCollisions(jointPosition, downPosition);
                MakeRenderersVisible();
            }
            base.UpdateRenderer();
        }

        /// <summary>
        /// The GetPointerObjects returns an array of the auto generated GameObjects associated with the pointer.
        /// </summary>
        /// <returns>An array of pointer auto generated GameObjects.</returns>
        public override GameObject[] GetPointerObjects()
        {
            return new GameObject[] { actualContainer, actualCursor };
        }

        protected override void ToggleRenderer(bool pointerState, bool actualState)
        {
            TogglePointerCursor(pointerState, actualState);
            TogglePointerTracer(pointerState, actualState);
            if (actualTracer != null && actualState && tracerVisibility != VisibilityStates.AlwaysOn)
            {
                ToggleRendererVisibility(actualTracer.gameObject, false);
                AddVisibleRenderer(actualTracer.gameObject);
            }
        }

        protected override void CreatePointerObjects()
        {
            actualContainer = new GameObject(VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BezierPointerRenderer_Container"));
            VRTK_PlayerObject.SetPlayerObject(actualContainer, VRTK_PlayerObject.ObjectTypes.Pointer);
            actualContainer.SetActive(false);

            CreateTracer();
            CreateCursor();
            Toggle(false, false);
            if (controllingPointer != null)
            {
                controllingPointer.ResetActivationTimer(true);
                controllingPointer.ResetSelectionTimer(true);
            }
        }

        protected override void DestroyPointerObjects()
        {
            if (actualCursor != null)
            {
                Destroy(actualCursor);
            }
            if (actualTracer != null)
            {
                Destroy(actualTracer);
            }
            if (actualContainer != null)
            {
                Destroy(actualContainer);
            }
        }

        protected override void UpdateObjectInteractor()
        {
            base.UpdateObjectInteractor();
            //if the object interactor is too far from the pointer tip then set it to the pointer tip position to prevent glitching.
            if (objectInteractor != null && actualCursor != null && Vector3.Distance(objectInteractor.transform.position, actualCursor.transform.position) > 0f)
            {
                objectInteractor.transform.position = actualCursor.transform.position;
            }
        }

        protected override void ChangeMaterial(Color givenColor)
        {
            base.ChangeMaterial(givenColor);
            ChangeMaterialColor(actualCursor, givenColor);
        }

        protected virtual void CreateTracer()
        {
            actualTracer = actualContainer.gameObject.AddComponent<VRTK_CurveGenerator>();
            actualTracer.transform.SetParent(null);
            actualTracer.Create(tracerDensity, cursorRadius, customTracer, rescaleTracer);
        }

        protected virtual GameObject CreateCursorObject()
        {
            float cursorYOffset = 0.02f;
            GameObject createdCursor = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            MeshRenderer createdCursorRenderer = createdCursor.GetComponent<MeshRenderer>();

            createdCursor.transform.localScale = new Vector3(cursorRadius, cursorYOffset, cursorRadius);
            createdCursorRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
            createdCursorRenderer.receiveShadows = false;
            createdCursorRenderer.material = defaultMaterial;
            Destroy(createdCursor.GetComponent<CapsuleCollider>());
            return createdCursor;
        }

        protected virtual void CreateCursorLocations()
        {
            if (validLocationObject != null)
            {
                actualValidLocationObject = Instantiate(validLocationObject);
                actualValidLocationObject.name = VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BezierPointerRenderer_ValidLocation");
                actualValidLocationObject.transform.SetParent(actualCursor.transform);
                actualValidLocationObject.layer = LayerMask.NameToLayer("Ignore Raycast");
                actualValidLocationObject.SetActive(false);
            }

            if (invalidLocationObject != null)
            {
                actualInvalidLocationObject = Instantiate(invalidLocationObject);
                actualInvalidLocationObject.name = VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BezierPointerRenderer_InvalidLocation");
                actualInvalidLocationObject.transform.SetParent(actualCursor.transform);
                actualInvalidLocationObject.layer = LayerMask.NameToLayer("Ignore Raycast");
                actualInvalidLocationObject.SetActive(false);
            }
        }

        protected virtual void CreateCursor()
        {
            actualCursor = (customCursor != null ? Instantiate(customCursor) : CreateCursorObject());
            CreateCursorLocations();
            actualCursor.name = VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BezierPointerRenderer_Cursor");
            VRTK_PlayerObject.SetPlayerObject(actualCursor, VRTK_PlayerObject.ObjectTypes.Pointer);
            actualCursor.layer = LayerMask.NameToLayer("Ignore Raycast");
            actualCursor.SetActive(false);
        }

        protected virtual Vector3 ProjectForwardBeam()
        {
            Transform origin = GetOrigin();
            float attachedRotation = Vector3.Dot(Vector3.up, origin.forward.normalized);
            float calculatedLength = maximumLength.x;
            Vector3 useForward = origin.forward;
            if ((attachedRotation * 100f) > heightLimitAngle)
            {
                useForward = new Vector3(useForward.x, fixedForwardBeamForward.y, useForward.z);
                float controllerRotationOffset = 1f - (attachedRotation - (heightLimitAngle / 100f));
                calculatedLength = (maximumLength.x * controllerRotationOffset) * controllerRotationOffset;
            }
            else
            {
                fixedForwardBeamForward = origin.forward;
            }

            float actualLength = calculatedLength;
            Ray pointerRaycast = new Ray(origin.position, useForward);

            RaycastHit collidedWith;
            bool hasRayHit = VRTK_CustomRaycast.Raycast(customRaycast, pointerRaycast, out collidedWith, defaultIgnoreLayer, calculatedLength);

            float contactDistance = 0f;
            //reset if beam not hitting or hitting new target
            if (!hasRayHit || (destinationHit.collider && destinationHit.collider != collidedWith.collider))
            {
                contactDistance = 0f;
            }

            //check if beam has hit a new target
            if (hasRayHit)
            {
                contactDistance = collidedWith.distance;
            }

            //adjust beam length if something is blocking it
            if (hasRayHit && contactDistance < calculatedLength)
            {
                actualLength = contactDistance;
            }

            //Use BEAM_ADJUST_OFFSET to move point back and up a bit to prevent beam clipping at collision point
            return (pointerRaycast.GetPoint(actualLength - BEAM_ADJUST_OFFSET) + (Vector3.up * BEAM_ADJUST_OFFSET));
        }

        protected virtual Vector3 ProjectDownBeam(Vector3 jointPosition)
        {
            Vector3 downPosition = Vector3.zero;
            Ray projectedBeamDownRaycast = new Ray(jointPosition, Vector3.down);
            RaycastHit collidedWith;

            bool downRayHit = VRTK_CustomRaycast.Raycast(customRaycast, projectedBeamDownRaycast, out collidedWith, defaultIgnoreLayer, maximumLength.y);

            if (!downRayHit || (destinationHit.collider && destinationHit.collider != collidedWith.collider))
            {
                if (destinationHit.collider != null)
                {
                    PointerExit(destinationHit);
                }
                destinationHit = new RaycastHit();
                downPosition = projectedBeamDownRaycast.GetPoint(0f);
            }

            if (downRayHit)
            {
                downPosition = projectedBeamDownRaycast.GetPoint(collidedWith.distance);
                PointerEnter(collidedWith);
                destinationHit = collidedWith;
            }
            return downPosition;
        }

        protected virtual void AdjustForEarlyCollisions(Vector3 jointPosition, Vector3 downPosition)
        {
            Vector3 newDownPosition = downPosition;
            Vector3 newJointPosition = jointPosition;

            if (collisionCheckFrequency > 0 && actualTracer != null)
            {
                collisionCheckFrequency = Mathf.Clamp(collisionCheckFrequency, 0, tracerDensity);
                Vector3[] beamPoints = new Vector3[]
                {
                    GetOrigin().position,
                    jointPosition + new Vector3(0f, curveOffset, 0f),
                    downPosition,
                    downPosition,
                };

                Vector3[] checkPoints = actualTracer.GetPoints(beamPoints);
                int checkFrequency = tracerDensity / collisionCheckFrequency;

                for (int i = 0; i < tracerDensity - checkFrequency; i += checkFrequency)
                {
                    Vector3 currentPoint = checkPoints[i];
                    Vector3 nextPoint = (i + checkFrequency < checkPoints.Length ? checkPoints[i + checkFrequency] : checkPoints[checkPoints.Length - 1]);
                    Vector3 nextPointDirection = (nextPoint - currentPoint).normalized;
                    float nextPointDistance = Vector3.Distance(currentPoint, nextPoint);

                    Ray checkCollisionRay = new Ray(currentPoint, nextPointDirection);
                    RaycastHit checkCollisionHit;

                    if (VRTK_CustomRaycast.Raycast(customRaycast, checkCollisionRay, out checkCollisionHit, defaultIgnoreLayer, nextPointDistance))
                    {
                        Vector3 collisionPoint = checkCollisionRay.GetPoint(checkCollisionHit.distance);
                        Ray downwardCheckRay = new Ray(collisionPoint + (Vector3.up * 0.01f), Vector3.down);
                        RaycastHit downwardCheckHit;

                        if (VRTK_CustomRaycast.Raycast(customRaycast, downwardCheckRay, out downwardCheckHit, defaultIgnoreLayer, float.PositiveInfinity))
                        {
                            destinationHit = downwardCheckHit;
                            newDownPosition = downwardCheckRay.GetPoint(downwardCheckHit.distance); ;
                            newJointPosition = (newDownPosition.y < jointPosition.y ? new Vector3(newDownPosition.x, jointPosition.y, newDownPosition.z) : jointPosition);
                            break;
                        }
                    }
                }
            }

            DisplayCurvedBeam(newJointPosition, newDownPosition);
            SetPointerCursor();
        }

        protected virtual void DisplayCurvedBeam(Vector3 jointPosition, Vector3 downPosition)
        {
            if (actualTracer != null)
            {
                Vector3[] beamPoints = new Vector3[]
                {
                GetOrigin(false).position,
                jointPosition + new Vector3(0f, curveOffset, 0f),
                downPosition,
                downPosition,
                };
                Material tracerMaterial = (customTracer != null ? null : defaultMaterial);
                actualTracer.SetPoints(beamPoints, tracerMaterial, currentColor);
                if (tracerVisibility == VisibilityStates.AlwaysOff)
                {
                    TogglePointerTracer(false, false);
                }
                else if (controllingPointer != null)
                {
                    TogglePointerTracer(controllingPointer.IsPointerActive(), controllingPointer.IsPointerActive());
                }
            }
        }

        protected virtual void TogglePointerCursor(bool pointerState, bool actualState)
        {
            ToggleElement(actualCursor, pointerState, actualState, cursorVisibility, ref cursorVisible);
        }

        protected virtual void TogglePointerTracer(bool pointerState, bool actualState)
        {
            tracerVisible = (tracerVisibility == VisibilityStates.AlwaysOn ? true : pointerState);
            if (actualTracer != null)
            {
                actualTracer.TogglePoints(tracerVisible);
            }
        }

        protected virtual void SetPointerCursor()
        {
            if (controllingPointer != null && destinationHit.transform)
            {
                TogglePointerCursor(controllingPointer.IsPointerActive(), controllingPointer.IsPointerActive());
                actualCursor.transform.position = destinationHit.point;
                if (cursorMatchTargetRotation)
                {
                    actualCursor.transform.rotation = Quaternion.FromToRotation(Vector3.up, destinationHit.normal);
                }
                base.UpdateDependencies(actualCursor.transform.position);

                ChangeColor(validCollisionColor);
                if (actualValidLocationObject != null)
                {
                    actualValidLocationObject.SetActive(ValidDestination() && IsValidCollision());
                }
                if (actualInvalidLocationObject != null)
                {
                    actualInvalidLocationObject.SetActive(!ValidDestination() || !IsValidCollision());
                }
            }
            else
            {
                TogglePointerCursor(false, false);
                ChangeColor(invalidCollisionColor);
            }
        }
    }
}