﻿////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2007-2021 zSpace, Inc.  All Rights Reserved.
//
////////////////////////////////////////////////////////////////////////////////

using UnityEngine;

using zSpace.Core.Extensions;

namespace zSpace.Core.Input
{
    public class ZMouseCursor : ZPointerVisualization
    {
        ////////////////////////////////////////////////////////////////////////
        // Inspector Fields
        ////////////////////////////////////////////////////////////////////////

        /// <summary>
        /// The duration in seconds to flip the cursor.
        /// </summary>
        /// 
        /// <remarks>
        /// A value of 0 will cause the flip to occur instantaneously.
        /// </remarks>
        [Tooltip("The duration in seconds to flip the cursor.")]
        public float FlipDuration = 0.1f;

        /// <summary>
        /// The duration in seconds for the cursor to snap to objects.
        /// </summary>
        [Tooltip("The duration in seconds for the cursor to snap to objects.")]
        public float SnapDuration = 0.05f;

        ////////////////////////////////////////////////////////////////////////
        // MonoBehaviour Callbacks
        ////////////////////////////////////////////////////////////////////////

        private void Awake()
        {
            this._spriteRenderer = 
                this.GetComponentInChildren<SpriteRenderer>();
            // Do not show the in-scene mouse cursor in WebGL builds because
            // the latency between mouse movement and updates to the visible
            // position of the in-scene mouse cursor is to too high in WebGL
            // builds.
#if UNITY_WEBGL
            this._spriteRenderer.enabled = false;
#endif
        }

        ////////////////////////////////////////////////////////////////////////
        // Public Properties
        ////////////////////////////////////////////////////////////////////////

        public override void Process(ZPointer pointer, Vector3 worldScale)
        {
            base.Process(pointer, worldScale);

            // Update the mouse cursor's position and rotation.
            this.transform.SetPose(this.GetEndPointPose(pointer));

            // Update the mouse cursor's scale based on whether 
            // stereoscopic 3D rendering is currently active. When 
            // rendering in monoscopic 3D, scale the cursor based on its 
            // distance from the camera.
            float distanceScale = this.GetCameraDistanceScale(
                pointer.EventCamera, this.transform.position);

            this.transform.localScale = Vector3.Lerp(
                Vector3.one * distanceScale,
                worldScale,
                pointer.EventCamera.StereoWeight);

            // Update whether the mouse cursor should be flipped about its
            // horizontal and vertical axes based on the hit normal in
            // screen space. This will minimize the chance that the mouse 
            // cursor will be occluded by the object it is intersecting.
            if (!pointer.AnyButtonPressed)
            {
                float flipThreshold = 89;

                Vector3 screenNormal =
                    Quaternion.Inverse(this.transform.rotation) *
                    pointer.HitInfo.worldNormal;

                this._flipHorizontal =
                    (Vector3.Angle(screenNormal, Vector3.left) < flipThreshold);

                this._flipVertical =
                    (Vector3.Angle(screenNormal, Vector3.up) < flipThreshold);
            }

            // Update the mouse cursor's corresponding sprite.
            this.UpdateSprite();
        }

        ////////////////////////////////////////////////////////////////////////
        // Private Methods
        ////////////////////////////////////////////////////////////////////////

        private Pose GetEndPointPose(ZPointer pointer)
        {
            if (pointer.AnyButtonPressed || pointer.MaxHitRadius == 0)
            {
                this._positionWeight = 1;
            }
            else if (pointer.HitInfo.gameObject != this._previousHitObject)
            {
                this._positionWeight = 0;
                this._startPosition = this.transform.position;
            }

            Pose pose = pointer.EndPointWorldPose;
            pose.position =
                Vector3.Lerp(this._startPosition, pose.position, this._positionWeight) +
                (pose.rotation * Vector3.back * 0.0001f);

            this._previousHitObject = pointer.HitInfo.gameObject;
            this._positionWeight += Time.unscaledDeltaTime / this.SnapDuration;
            this._positionWeight = Mathf.Clamp01(this._positionWeight);

            return pose;
        }

        public float GetCameraDistanceScale(ZCamera camera, Vector3 point)
        {
            Plane monoCameraPlane = new Plane(
                -camera.ZeroParallaxPlane.normal,
                camera.transform.position);

            float distanceToCameraPlane = 
                monoCameraPlane.GetDistanceToPoint(point);

            float distanceFromCameraToZeroParallax = 
                camera.CameraOffset.magnitude;

            return (distanceToCameraPlane / distanceFromCameraToZeroParallax);
        }

        private void UpdateSprite()
        {
            if (this._spriteRenderer == null)
            {
                return;
            }

            Transform spriteTransform = this._spriteRenderer.transform;

            // Update the sprite's local rotation.
            Quaternion targetRotation = Quaternion.Euler(
                this._flipVertical ? (this._flipHorizontal ? 180 : -180) : 0,
                this._flipHorizontal ? -180 : 0,
                0);

            if (this.FlipDuration == 0)
            {
                spriteTransform.localRotation = targetRotation;
            }
            else
            {
                spriteTransform.localRotation = Quaternion.RotateTowards(
                    spriteTransform.localRotation,
                    targetRotation,
                    180 / this.FlipDuration * Time.unscaledDeltaTime);
            }

            // Update the sprite's local scale.
            Vector3 metersPerPixel =
                ZProvider.DisplayMetersPerPixelForNativeResolution;

            spriteTransform.localScale =
                Vector3.one * Mathf.Min(metersPerPixel.x, metersPerPixel.y);

            // The size of sprite is defined with respective to a 1920 by 1080
            // resolution display.  Scale the sprite based on the ratio of the
            // current display resolution and a 1920 by 1080 resolution to keep
            // the sprite size in meters similar regardless of the display
            // pixel density.

            float resolutionScaleFactor = Mathf.Min(
                1920.0f / ZProvider.DisplayResolution.x,
                1080.0f / ZProvider.DisplayResolution.y);

            spriteTransform.localScale /= resolutionScaleFactor;
        }

        ////////////////////////////////////////////////////////////////////////
        // Private Members
        ////////////////////////////////////////////////////////////////////////

        private SpriteRenderer _spriteRenderer = null;

        private bool _flipHorizontal = false;
        private bool _flipVertical = false;

        private GameObject _previousHitObject = null;
        private float _positionWeight = 1;
        private Vector3 _startPosition = Vector3.zero;
    }
}
