/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * Licensed under the Oculus SDK License Agreement (the "License");
 * you may not use the Oculus SDK except in compliance with the License,
 * which is provided at the time of installation or download, or which
 * otherwise accompanies this software in either electronic or hard copy form.
 *
 * You may obtain a copy of the License at
 *
 * https://developer.oculus.com/licenses/oculussdk/
 *
 * Unless required by applicable law or agreed to in writing, the Oculus SDK
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using Oculus.Interaction.Input;
using System;
using UnityEngine;

namespace Oculus.Interaction.Locomotion
{
    /// <summary>
    /// TeleportInteractor uses a provided ITeleportArc and an external Selector to find
    /// the best interactable and emit the event requesting a teleport to it. By itself
    /// it does not define the shape of the arc or even moves the player, instead it is the
    /// core class that brings these pieces together.
    /// </summary>
    public class TeleportInteractor : Interactor<TeleportInteractor, TeleportInteractable>,
        ILocomotionEventBroadcaster
    {
        [SerializeField, Interface(typeof(ISelector))]
        [Tooltip("A selector indicating when the Interactor should" +
            "Select or Unselect the best available interactable." +
            "Typically when using controllers this selector is drivenby the joystick value," +
            "and for hands it is driven by the index pinch value.")]
        private UnityEngine.Object _selector;

        [SerializeField, Optional, Interface(typeof(IHmd))]
        [Tooltip("When provided, the Interactor will perform an extra check to ensure" +
            "nothing is blocking the line between the Hmd and the teleport origin")]
        private UnityEngine.Object _hmd;
        /// <summary>
        /// When provided, the Interactor will perform an extra check to ensure nothing
        /// is blocking the line between the Hmd (head of the player) and the teleport
        /// origin (hand). Making it impossible to teleport if the user is placing their
        /// hands behind a virtual wall.
        /// </summary>
        private IHmd Hmd { get; set; }

        [SerializeField, Interface(typeof(IPolyline))]
        [Optional(OptionalAttribute.Flag.AutoGenerated)]
        [Tooltip("Specifies the shape of the arc used for detecting available interactables." +
            "If none is provided TeleportArcGravity will be used.")]
        private UnityEngine.Object _teleportArc;
        /// <summary>
        ///  Specifies the shape of the arc used for detecting available interactables.
        ///  If none is provided TeleportArcGravity will be used.
        /// </summary>
        public IPolyline TeleportArc { get; private set; }

        [SerializeField]
        [Tooltip("(Meters, World) The threshold below which distances to a interactable " +
                 "are treated as equal for the purposes of ranking.")]
        private float _equalDistanceThreshold = 0.1f;
        /// <summary>
        /// (Meters, World) The threshold below which distances to a interactable are treated as equal for the purposes of ranking.
        /// </summary>
        public float EqualDistanceThreshold
        {
            get
            {
                return _equalDistanceThreshold;
            }
            set
            {
                _equalDistanceThreshold = value;
            }
        }

        public Pose ArcOrigin
        {
            get
            {
                Vector3 start = TeleportArc.PointAtIndex(0);
                Vector3 next = TeleportArc.PointAtIndex(1);
                return new Pose(start, Quaternion.LookRotation(next - start));
            }
        }

        private TeleportHit _arcEnd;
        public TeleportHit ArcEnd => _arcEnd;

        public Pose TeleportTarget
        {
            get
            {
                Vector3 forward = Vector3.ProjectOnPlane(ArcOrigin.forward, _arcEnd.Normal);
                Quaternion rotation = Quaternion.LookRotation(forward, _arcEnd.Normal);
                Pose pose = new Pose(_arcEnd.Point, rotation);
                if (HasInteractable)
                {
                    return Interactable.TargetPose(pose);
                }
                return pose;
            }
        }

        private Action<LocomotionEvent> _whenLocomotionPerformed = delegate { };
        public event Action<LocomotionEvent> WhenLocomotionPerformed
        {
            add
            {
                _whenLocomotionPerformed += value;
            }
            remove
            {
                _whenLocomotionPerformed -= value;
            }
        }

        protected override void Awake()
        {
            base.Awake();
            TeleportArc = _teleportArc as IPolyline;
            Selector = _selector as ISelector;
            Hmd = _hmd as IHmd;
        }

        protected override void Start()
        {
            base.Start();
            this.AssertField(Selector, nameof(Selector));

            if (TeleportArc == null)
            {
                var arc = this.gameObject.AddComponent<TeleportArcGravity>();
                InjectOptionalTeleportArc(arc);
            }
        }

        public override bool CanSelect(TeleportInteractable interactable)
        {
            Pose origin = ArcOrigin;
            float maxSqrDistance = (TeleportArc.PointAtIndex(0) -
                TeleportArc.PointAtIndex(TeleportArc.PointsCount - 1)).sqrMagnitude;
            if (!interactable.IsInRange(origin, maxSqrDistance))
            {
                return false;
            }

            return base.CanSelect(interactable);
        }

        protected override void InteractableSelected(TeleportInteractable interactable)
        {
            base.InteractableSelected(interactable);
            if (interactable == null
                || !interactable.AllowTeleport)
            {
                LocomotionEvent deniedLocomotionEvent = new LocomotionEvent(this.Identifier, TeleportTarget,
                    LocomotionEvent.TranslationType.None, LocomotionEvent.RotationType.None);
                _whenLocomotionPerformed.Invoke(deniedLocomotionEvent);
                return;
            }

            LocomotionEvent locomotionEvent = new LocomotionEvent(this.Identifier, TeleportTarget,
                interactable.EyeLevel ?
                LocomotionEvent.TranslationType.AbsoluteEyeLevel
                : LocomotionEvent.TranslationType.Absolute,
                interactable.FaceTargetDirection ?
                LocomotionEvent.RotationType.Absolute
                : LocomotionEvent.RotationType.None);
            _whenLocomotionPerformed.Invoke(locomotionEvent);
        }

        protected override TeleportInteractable ComputeCandidate()
        {
            Pose origin = ArcOrigin;
            float bestScore = float.PositiveInfinity;
            Vector3 arcEndPosition = TeleportArc.PointAtIndex(TeleportArc.PointsCount - 1);

            TeleportInteractable bestCandidate = null;
            TeleportHit bestHit = new TeleportHit(null, arcEndPosition, Vector3.up);
            var interactables = TeleportInteractable.Registry.List(this);

            Pose headPose = Pose.identity;
            if (Hmd != null)
            {
                Hmd.TryGetRootPose(out headPose);
            }

            foreach (TeleportInteractable interactable in interactables)
            {
                if (interactable.AllowTeleport)
                {
                    continue;
                }

                if (Hmd != null)
                {
                    CheckViewToOriginBlockers(headPose.position, interactable);
                }
                CheckCandidate(interactable);
            }

            foreach (TeleportInteractable interactable in interactables)
            {
                if (interactable.AllowTeleport)
                {
                    CheckCandidate(interactable);
                }
            }

            _arcEnd = bestHit;
            return bestCandidate;

            void CheckViewToOriginBlockers(Vector3 viewPosition, TeleportInteractable candidate)
            {
                if (candidate.DetectHit(viewPosition, origin.position, out TeleportHit hit))
                {
                    float score = -Vector3.Distance(origin.position, hit.Point);
                    bool isTie = Mathf.Abs(bestScore - score) < _equalDistanceThreshold;
                    if (bestCandidate == null
                            || (!isTie && score < bestScore)
                            || (isTie && ComputeCandidateTiebreaker(candidate, bestCandidate) > 0))
                    {
                        bestScore = score;
                        bestHit = hit;
                        bestCandidate = candidate;
                    }
                }
            }

            void CheckCandidate(TeleportInteractable candidate)
            {
                Vector3 prevPoint = origin.position;
                float accumulatedDistance = 0;
                for (int i = 1; i < TeleportArc.PointsCount; i++)
                {
                    if (accumulatedDistance > bestScore)
                    {
                        break;
                    }
                    Vector3 point = TeleportArc.PointAtIndex(i);
                    if (candidate.DetectHit(prevPoint, point, out TeleportHit hit))
                    {
                        float score = accumulatedDistance
                            + Vector3.Distance(prevPoint, hit.Point);

                        bool isTie = Mathf.Abs(bestScore - score) < _equalDistanceThreshold;
                        if (!isTie
                            && bestCandidate != null
                            && !bestCandidate.AllowTeleport
                            && candidate.AllowTeleport)
                        {
                            float snapRadius = hit.SnapRadius;
                            float pointToPoint = (bestHit.Point - hit.Point).sqrMagnitude;
                            if (pointToPoint < snapRadius * snapRadius)
                            {
                                isTie = true;
                            }
                        }

                        if (bestCandidate == null
                            || (!isTie && score < bestScore)
                            || (isTie && ComputeCandidateTiebreaker(candidate, bestCandidate) > 0))
                        {
                            bestScore = score;
                            bestHit = hit;
                            bestCandidate = candidate;
                            break;
                        }
                    }
                    accumulatedDistance += Vector3.Distance(prevPoint, point);
                    prevPoint = point;
                }
            }
        }

        protected override int ComputeCandidateTiebreaker(TeleportInteractable a, TeleportInteractable b)
        {
            int result = base.ComputeCandidateTiebreaker(a, b);
            if (result != 0)
            {
                return result;
            }

            return a.TieBreakerScore.CompareTo(b.TieBreakerScore);
        }

        #region Inject
        public void InjectAllTeleportInteractor(ISelector selector)
        {
            InjectSelector(selector);
        }
        public void InjectSelector(ISelector selector)
        {
            _selector = selector as UnityEngine.Object;
            Selector = selector;
        }
        public void InjectOptionalHmd(IHmd hmd)
        {
            _hmd = hmd as UnityEngine.Object;
            Hmd = hmd;
        }
        public void InjectOptionalTeleportArc(IPolyline teleportArc)
        {
            _teleportArc = teleportArc as UnityEngine.Object;
            TeleportArc = teleportArc;
        }
        #endregion
    }
}
