//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//
using UnityEngine;
using System.Collections;

namespace HoloToolkit.Unity
{
    /// <summary>
    /// RadialViewPoser solver locks a tag-along type object within a view cone
    /// </summary>
    public class SolverRadialView : Solver
    {
        #region public enums
        public enum ReferenceDirectionEnum
        {
            /// <summary>
            /// Orient towards head including roll, pitch and yaw
            /// </summary>
            ObjectOriented,
            /// <summary>
            /// Orient toward head but ignore roll
            /// </summary>
            FacingWorldUp,
            /// <summary>
            /// Orient torwards the head movement direction found in CameraMotionInfo singleton
            /// </summary>
            HeadMoveDirection,
            /// <summary>
            /// Orient towards head but remain vertical or gravity aligned
            /// </summary>
            GravityAligned
        }
        #endregion

        #region public members
        [Tooltip("Which direction to position the element relative to: HeadOriented rolls with the head, HeadFacingWorldUp view dir but ignores head roll, and HeadMoveDirection uses the direction the head last moved without roll")]
        public ReferenceDirectionEnum ReferenceDirection = ReferenceDirectionEnum.FacingWorldUp;

        [Tooltip("Min distance from eye to position element around, i.e. the sphere radius")]
        public float MinDistance = 1f;
        [Tooltip("Max distance from eye to element")]
        public float MaxDistance = 2f;

        [Tooltip("The element will stay at least this far away from the center of view")]
        public float MinViewDegrees = 0f;
        [Tooltip("The element will stay at least this close to the center of view")]
        public float MaxViewDegrees = 30f;
        [Tooltip("Apply a different clamp to vertical FOV than horizontal.  Vertical = Horizontal * AspectV")]
        public float AspectV = 1f;

        [Tooltip("Option to ignore angle clamping")]
        public bool IgnoreAngleClamp = false;
        [Tooltip("Option to ignore distance clamping")]
        public bool IgnoreDistanceClamp = false;

        [Tooltip("If true, element will orient to ReferenceDirection, otherwise it will orient to ref pos (the head is the only option currently)")]
        public bool OrientToRefDir = false;
        #endregion

        /// <summary>
        ///   ReferenceDirectoin is the direction of the cone.  Position to the view direction, or the movement direction
        /// </summary>
        /// <returns>Vector3, the forward direction to use for positioning</returns>
        private Vector3 GetReferenceDirection()
        {
            Vector3 ret = Vector3.one;
            if (ReferenceDirection == ReferenceDirectionEnum.HeadMoveDirection && solverHandler.TrackedObjectToReference == SolverHandler.TrackedObjectToReferenceEnum.Head)
            {
                ret = Camera.main.GetComponent<InputModule.CameraMotionInfo>().MoveDirection;
            }
            else
            {
                ret = base.solverHandler.TransformTarget != null ? base.solverHandler.TransformTarget.forward : Vector3.forward;
            }
            return ret;
        }

        /// <summary>
        ///   Cone may roll with head, or not.
        /// </summary>
        /// <returns>Vector3, the up direction to use for orientation</returns>
        private Vector3 GetReferenceUp()
        {
            Vector3 ret = Vector3.up;
            if (ReferenceDirection == ReferenceDirectionEnum.ObjectOriented)
            {
                ret = base.solverHandler.TransformTarget != null ? base.solverHandler.TransformTarget.up : Vector3.up;
            }
            return ret;
        }

        private Vector3 GetReferencePoint()
        {
            return base.solverHandler.TransformTarget != null ? base.solverHandler.TransformTarget.position : Vector3.zero;
        }

        /// <summary>
        /// Solver update function used to orient to the user
        /// </summary>
        public override void SolverUpdate()
        {

            Vector3 desiredPos = this.WorkingPos;

            if (IgnoreAngleClamp)
            {
                if (IgnoreDistanceClamp)
                {
                    desiredPos = transform.position;
                }
                else
                {
                    GetDesiredOrientation_DistanceOnly(ref desiredPos);
                }
            }
            else
            {
                GetDesiredOrientation(ref desiredPos);
            }

            // Element orientation
            Vector3 refDirUp = GetReferenceUp();
            Quaternion desiredRot = Quaternion.identity;

            if (OrientToRefDir)
            {
                desiredRot = Quaternion.LookRotation(GetReferenceDirection(), refDirUp);
            }
            else
            {
                Vector3 refPoint = GetReferencePoint();
                desiredRot = Quaternion.LookRotation(desiredPos - refPoint, refDirUp);
            }

            // If gravity aligned then zero out the x and z eulers on the rotation
            if (ReferenceDirection == ReferenceDirectionEnum.GravityAligned)
            {
                desiredRot.x = desiredRot.z = 0f;
            }

            this.GoalPosition = desiredPos;
            this.GoalRotation = desiredRot;

            UpdateWorkingPosToGoal();
            UpdateWorkingRotToGoal();
        }

        /// <summary>
        ///   Optimized version of GetDesiredOrientation.  There should be a different solver for distance contraint though
        /// </summary>
        /// <param name="desiredPos"></param>
        private void GetDesiredOrientation_DistanceOnly(ref Vector3 desiredPos)
        {
            // Determine reference locations and directions
            Vector3 refPoint = GetReferencePoint();
            Vector3 elementPoint = transform.position;
            Vector3 elementDelta = elementPoint - refPoint;
            float elementDist = elementDelta.magnitude;
            Vector3 elementDir = elementDist > 0 ? elementDelta / elementDist : Vector3.one;

            // Clamp distance too
            float clampedDistance = Mathf.Clamp(elementDist, MinDistance, MaxDistance);

            if (clampedDistance != elementDist)
            {
                desiredPos = refPoint + clampedDistance * elementDir;
            }
        }

        private void GetDesiredOrientation(ref Vector3 desiredPos)
        {
            // Determine reference locations and directions
            Vector3 refDir = GetReferenceDirection();
            Vector3 refDirUp = GetReferenceUp();
            Vector3 refPoint = GetReferencePoint();
            Vector3 elementPoint = transform.position;
            Vector3 elementDelta = elementPoint - refPoint;
            float elementDist = elementDelta.magnitude;
            Vector3 elementDir = elementDist > 0 ? elementDelta / elementDist : Vector3.one;
            float flip = Vector3.Dot(elementDelta, refDir);

            // Generate basis: First get axis perp to refDir pointing toward element
            Vector3 elementDirPerp = (elementDir - refDir);
            elementDirPerp -= refDir * Vector3.Dot(elementDirPerp, refDir);
            elementDirPerp.Normalize();

            // Calculate the clamping angles, accounting for aspect (need the angle relative to view plane)
            float HtoVAng = Vector3.Angle(elementDirPerp, refDirUp);
            float VAspectScale = Mathf.Lerp(AspectV, 1f, Mathf.Abs(Mathf.Sin(HtoVAng * Mathf.Deg2Rad)));

            // Calculate the current angle
            float angDegree = Vector3.Angle(elementDir, refDir);
            float angDegreeClamped = Mathf.Clamp(angDegree, MinViewDegrees * VAspectScale, MaxViewDegrees * VAspectScale);

            // Clamp distance too, if desired
            float clampedDistance = IgnoreDistanceClamp ? elementDist : Mathf.Clamp(elementDist, MinDistance, MaxDistance);

            // If the angle was clamped, do some special update stuff
            if (flip < 0)
            {
                desiredPos = refPoint + refDir;
            }
            else if (angDegree != angDegreeClamped)
            {
                float angRad = angDegreeClamped * Mathf.Deg2Rad;

                // Calculate new position
                desiredPos = refPoint + clampedDistance * (refDir * Mathf.Cos(angRad) + elementDirPerp * Mathf.Sin(angRad));
            }
            else if (clampedDistance != elementDist)
            {
                // Only need to apply distance
                desiredPos = refPoint + clampedDistance * elementDir;
            }
        }
    }
}
