// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.Utilities;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Serialization;

namespace Microsoft.MixedReality.Toolkit.Input
{
    /// <summary>
    /// A simple line pointer for drawing lines from the input source origin to the current pointer position.
    /// </summary>
    [AddComponentMenu("Scripts/MRTK/SDK/LinePointer")]
    public class LinePointer : BaseControllerPointer
    {
        [SerializeField]
        protected Gradient LineColorSelected = new Gradient();

        [SerializeField]
        protected Gradient LineColorValid = new Gradient();

        [SerializeField]
        protected Gradient LineColorInvalid = new Gradient();

        [SerializeField]
        protected Gradient LineColorNoTarget = new Gradient();

        [SerializeField]
        protected Gradient LineColorLockFocus = new Gradient();

        [SerializeField, FormerlySerializedAs("lineBase")]
        private BaseMixedRealityLineDataProvider lineDataProvider;

        /// <summary>
        /// The Line Data Provider driving this pointer.
        /// </summary>
        public BaseMixedRealityLineDataProvider LineBase => lineDataProvider;

        [SerializeField]
        [Tooltip("If no line renderers are specified, this array will be auto-populated on startup.")]
        private BaseMixedRealityLineRenderer[] lineRenderers;

        /// <summary>
        /// The current line renderers that this pointer is utilizing.
        /// </summary>
        /// <remarks>
        /// If no line renderers are specified, this array will be auto-populated on startup.
        /// </remarks>
        public BaseMixedRealityLineRenderer[] LineRenderers => lineRenderers;

        /// <inheritdoc />
        public override bool IsInteractionEnabled =>
                // If IsTracked is not true, then we don't have position data yet (or have stale data),
                // so remain disabled until we know where to appear (not just at the origin).
                IsFocusLocked || (IsTracked && Controller.IsNotNull() && Controller.IsInPointingPose && base.IsInteractionEnabled);

        private Vector3 lineStartPoint;
        private Vector3 lineEndPoint;

        private void CheckInitialization()
        {
            if (lineDataProvider == null)
            {
                lineDataProvider = GetComponent<BaseMixedRealityLineDataProvider>();
            }

            if (lineDataProvider == null)
            {
                Debug.LogError($"No Mixed Reality Line Data Provider found on {gameObject.name}. Did you forget to add a Line Data provider?");
            }

            if (lineDataProvider != null && (lineRenderers == null || lineRenderers.Length == 0))
            {
                lineRenderers = lineDataProvider.GetComponentsInChildren<BaseMixedRealityLineRenderer>();
            }

            if (lineRenderers == null || lineRenderers.Length == 0)
            {
                Debug.LogError($"No Mixed Reality Line Renderers found on {gameObject.name}. Did you forget to add a Mixed Reality Line Renderer?");
            }

            for (int i = 0; i < lineRenderers.Length; i++)
            {
                lineRenderers[i].enabled = true;
            }
        }

        #region MonoBehaviour Implementation

        protected override void OnEnable()
        {
            base.OnEnable();
            CheckInitialization();
        }

        protected override void OnDisable()
        {
            base.OnDisable();

            for (int i = 0; i < lineRenderers.Length; i++)
            {
                lineRenderers[i].enabled = false;
            }
        }

        #endregion MonoBehaviour Implementation

        #region IMixedRealityPointer Implementation

        private static readonly ProfilerMarker OnPreSceneQueryPerfMarker = new ProfilerMarker("[MRTK] LinePointer.OnPreSceneQuery");

        /// <inheritdoc />
        public override void OnPreSceneQuery()
        {
            using (OnPreSceneQueryPerfMarker.Auto())
            {
                if (!IsInteractionEnabled)
                {
                    return;
                }

                PreUpdateLineRenderers();
                UpdateRays();
            }
        }

        private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] LinePointer.OnPostSceneQuery");

        /// <inheritdoc />
        public override void OnPostSceneQuery()
        {
            using (OnPostSceneQueryPerfMarker.Auto())
            {
                base.OnPostSceneQuery();

                bool isEnabled = IsInteractionEnabled;
                lineDataProvider.enabled = isEnabled;
                if (BaseCursor != null)
                {
                    BaseCursor.SetVisibility(isEnabled);
                }

                PostUpdateLineRenderers();
            }
        }

        private static readonly ProfilerMarker PreUpdateLineRenderersPerfMarker = new ProfilerMarker("[MRTK] LinePointer.PreUpdateLineRenderers");

        protected virtual void PreUpdateLineRenderers()
        {
            using (PreUpdateLineRenderersPerfMarker.Auto())
            {
                Debug.Assert(lineDataProvider != null);

                lineDataProvider.UpdateMatrix();

                // Set our first and last points on the Line Renderer
                if (IsFocusLocked && IsTargetPositionLockedOnFocusLock && Result != null)
                {
                    // Make the final point 'stick' to the target at the distance of the target
                    SetLinePoints(Position, Result.Details.Point);
                }
                else
                {
                    SetLinePoints(Position, Position + Rotation * Vector3.forward * DefaultPointerExtent);
                }
            }
        }

        private static readonly ProfilerMarker PostUpdateLineRenderersPerfMarker = new ProfilerMarker("[MRTK] LinePointer.PostUpdateLineRenderers");

        protected virtual void PostUpdateLineRenderers()
        {
            using (PostUpdateLineRenderersPerfMarker.Auto())
            {
                if (!IsInteractionEnabled)
                {
                    return;
                }

                // The distance the ray travels through the world before it hits something. Measured in world-units (as opposed to normalized distance).
                float clearWorldLength;
                Gradient lineColor = LineColorNoTarget;

                if (Result?.CurrentPointerTarget != null)
                {
                    // We hit something
                    clearWorldLength = Result.Details.RayDistance;
                    lineColor = IsSelectPressed || IsGrabPressed ? LineColorSelected : LineColorValid;
                }
                else
                {
                    clearWorldLength = DefaultPointerExtent;
                    lineColor = IsSelectPressed || IsGrabPressed ? LineColorSelected : LineColorNoTarget;
                }

                if (IsFocusLocked)
                {
                    lineColor = LineColorLockFocus;
                }

                int maxClampLineSteps = 2;
                for (int i = 0; i < LineRenderers.Length; i++)
                {
                    // Renderers are enabled by default if line is enabled
                    maxClampLineSteps = Mathf.Max(maxClampLineSteps, LineRenderers[i].LineStepCount);
                    LineRenderers[i].LineColor = lineColor;
                }

                // Used to ensure the line doesn't extend beyond the cursor
                float cursorOffsetWorldLength = (BaseCursor != null) ? BaseCursor.SurfaceCursorDistance : 0;

                // Readjust the Line renderer's endpoint to match the cursor's position if it is focus locked to a target
                if (IsFocusLocked && IsTargetPositionLockedOnFocusLock && Result != null)
                {
                    SetLinePoints(Position, Result.Details.Point + Rotation * Vector3.back * cursorOffsetWorldLength);
                }

                // If focus is locked, we're sticking to the target
                // So don't clamp the world length, the line data's end point is already set to the world cursor
                if (IsFocusLocked && IsTargetPositionLockedOnFocusLock)
                {
                    lineDataProvider.LineEndClamp = 1;
                }
                else
                {
                    // Otherwise clamp the line end by the clear distance
                    float clearLocalLength = lineDataProvider.GetNormalizedLengthFromWorldLength(clearWorldLength - cursorOffsetWorldLength, maxClampLineSteps);
                    lineDataProvider.LineEndClamp = clearLocalLength;
                }
            }
        }

        private static readonly ProfilerMarker UpdateRaysPerfMarker = new ProfilerMarker("[MRTK] LinePointer.UpdateRays");

        protected virtual void UpdateRays()
        {
            using (UpdateRaysPerfMarker.Auto())
            {
                const int LineLength = 1;

                // Make sure our array will hold
                if (Rays == null || Rays.Length != LineLength)
                {
                    Rays = new RayStep[LineLength];
                }

                Rays[0].UpdateRayStep(ref lineStartPoint, ref lineEndPoint);
            }
        }

        protected void SetLinePoints(Vector3 startPoint, Vector3 endPoint)
        {
            lineStartPoint = startPoint;
            lineEndPoint = endPoint;

            lineDataProvider.FirstPoint = startPoint;
            lineDataProvider.LastPoint = endPoint;
        }


        #endregion IMixedRealityPointer Implementation
    }
}
