/*
 * 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.Grab;
using Oculus.Interaction.GrabAPI;
using Oculus.Interaction.Input;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Oculus.Interaction.HandGrab
{
    /// <summary>
    /// The DistanceHandGrabInteractable allows grabbing the marked object from far away.
    /// Internally it uses HandGrabPoses to specify not only the poses of the hands but the
    /// required gestures to perform the grab. It is possible (and recommended) to reuse the same
    /// HandGrabPoses used by the HandGrabInteractable, and even select just a few so they become
    /// the default poses when distant grabbing.
    /// </summary>
    [Serializable]
    public class DistanceHandGrabInteractable : PointerInteractable<DistanceHandGrabInteractor, DistanceHandGrabInteractable>,
        IRigidbodyRef, IHandGrabbable, IRelativeToRef, ICollidersRef
    {
        [SerializeField]
        private Rigidbody _rigidbody;
        public Rigidbody Rigidbody => _rigidbody;

        [SerializeField, Optional]
        private PhysicsGrabbable _physicsGrabbable = null;

        [SerializeField]
        private bool _resetGrabOnGrabsUpdated = true;
        public bool ResetGrabOnGrabsUpdated
        {
            get
            {
                return _resetGrabOnGrabsUpdated;
            }
            set
            {
                _resetGrabOnGrabsUpdated = value;
            }
        }

        [Space]
        /// <summary>
        /// The available grab types dictates the available gestures for grabbing
        /// this interactable.
        /// </summary>
        [SerializeField]
        private GrabTypeFlags _supportedGrabTypes = GrabTypeFlags.Pinch;
        [SerializeField]
        private GrabbingRule _pinchGrabRules = GrabbingRule.DefaultPinchRule;
        [SerializeField]
        private GrabbingRule _palmGrabRules = GrabbingRule.DefaultPalmRule;

        /// <summary>
        /// The movement provider specifies how the selected interactable will
        /// align with the grabber.
        /// </summary>
        [SerializeField, Interface(typeof(IMovementProvider))]
        [Optional(OptionalAttribute.Flag.AutoGenerated)]
        private UnityEngine.Object _movementProvider;
        public IMovementProvider MovementProvider { get; set; }

        [SerializeField]
        private HandAlignType _handAligment = HandAlignType.AlignOnGrab;
        public HandAlignType HandAlignment
        {
            get
            {
                return _handAligment;
            }
            set
            {
                _handAligment = value;
            }
        }

        [SerializeField]
        [Optional(OptionalAttribute.Flag.DontHide)]
        [UnityEngine.Serialization.FormerlySerializedAs("_handGrabPoints")]
        private List<HandGrabPose> _handGrabPoses = new List<HandGrabPose>();
        /// <summary>
        /// General getter for the transform of the object this interactable refers to.
        /// </summary>
        public Transform RelativeTo => _rigidbody.transform;

        public GrabTypeFlags SupportedGrabTypes => _supportedGrabTypes;
        public GrabbingRule PinchGrabRules => _pinchGrabRules;
        public GrabbingRule PalmGrabRules => _palmGrabRules;

        public List<HandGrabPose> HandGrabPoses => _handGrabPoses;
        public Collider[] Colliders { get; private set; }

        private GrabPoseFinder _grabPoseFinder;

        private readonly PoseMeasureParameters SCORE_MODIFIER = new PoseMeasureParameters(1f);

        #region editor events
        protected virtual void Reset()
        {
            if (this.TryGetComponent(out HandGrabInteractable handGrabInteractable))
            {
                InjectAllDistanceHandGrabInteractable(handGrabInteractable.SupportedGrabTypes,
                    handGrabInteractable.Rigidbody,
                    handGrabInteractable.PinchGrabRules, handGrabInteractable.PalmGrabRules);
                InjectOptionalHandGrabPoses(new List<HandGrabPose>(handGrabInteractable.HandGrabPoses));
                InjectOptionalPointableElement(handGrabInteractable.PointableElement);
            }
            else
            {
                InjectRigidbody(this.GetComponentInParent<Rigidbody>());
                InjectOptionalPhysicsGrabbable(this.GetComponentInParent<PhysicsGrabbable>());
                InjectOptionalPointableElement(this.GetComponentInParent<Grabbable>());
            }
        }
        #endregion

        protected override void Awake()
        {
            base.Awake();
            MovementProvider = _movementProvider as IMovementProvider;
        }

        protected override void Start()
        {
            this.BeginStart(ref _started, () => base.Start());
            this.AssertField(Rigidbody, nameof(Rigidbody));
            Colliders = Rigidbody.GetComponentsInChildren<Collider>();
            this.AssertCollectionField(Colliders, nameof(Colliders),
                whyItFailed: $"The associated {nameof(Rigidbody)} must have at least one collider.");

            if (MovementProvider == null)
            {
                MoveTowardsTargetProvider movementProvider = this.gameObject.AddComponent<MoveTowardsTargetProvider>();
                InjectOptionalMovementProvider(movementProvider);
            }
            _grabPoseFinder = new GrabPoseFinder(_handGrabPoses, this.RelativeTo);
            this.EndStart(ref _started);
        }

        public IMovement GenerateMovement(in Pose from, in Pose to)
        {
            IMovement movement = MovementProvider.CreateMovement();
            movement.StopAndSetPose(from);
            movement.MoveTo(to);
            return movement;
        }

        public void ApplyVelocities(Vector3 linearVelocity, Vector3 angularVelocity)
        {
            if (_physicsGrabbable == null)
            {
                return;
            }
            _physicsGrabbable.ApplyVelocities(linearVelocity, angularVelocity);
        }

        public bool CalculateBestPose(in Pose userPose, float handScale, Handedness handedness,
            ref HandGrabResult result)
        {
            GrabPoseFinder.FindResult findResult = _grabPoseFinder.FindBestPose(userPose,
                handScale, handedness, SCORE_MODIFIER, ref result);
            if (findResult == GrabPoseFinder.FindResult.NotFound)
            {
                result.HasHandPose = false;
                result.Score = new GrabPoseScore(userPose.position, this.transform.position);
                result.RelativePose = new Pose(RelativeTo.Delta(this.transform).position, Quaternion.Inverse(RelativeTo.rotation) * userPose.rotation);
            }

            return findResult != GrabPoseFinder.FindResult.NotCompatible;
        }

        public bool UsesHandPose()
        {
            return _grabPoseFinder.UsesHandPose();
        }

        public bool SupportsHandedness(Handedness handedness)
        {
            return _grabPoseFinder.SupportsHandedness(handedness);
        }

        #region Inject

        public void InjectAllDistanceHandGrabInteractable(GrabTypeFlags supportedGrabTypes,
            Rigidbody rigidbody,
            GrabbingRule pinchGrabRules, GrabbingRule palmGrabRules)
        {
            InjectSupportedGrabTypes(supportedGrabTypes);
            InjectRigidbody(rigidbody);
            InjectPinchGrabRules(pinchGrabRules);
            InjectPalmGrabRules(palmGrabRules);
        }

        public void InjectOptionalPhysicsGrabbable(PhysicsGrabbable physicsObject)
        {
            _physicsGrabbable = physicsObject;
        }

        public void InjectSupportedGrabTypes(GrabTypeFlags supportedGrabTypes)
        {
            _supportedGrabTypes = supportedGrabTypes;
        }

        public void InjectPinchGrabRules(GrabbingRule pinchGrabRules)
        {
            _pinchGrabRules = pinchGrabRules;
        }

        public void InjectPalmGrabRules(GrabbingRule palmGrabRules)
        {
            _palmGrabRules = palmGrabRules;
        }

        public void InjectRigidbody(Rigidbody rigidbody)
        {
            _rigidbody = rigidbody;
        }

        public void InjectOptionalHandGrabPoses(List<HandGrabPose> handGrabPoses)
        {
            _handGrabPoses = handGrabPoses;
        }

        public void InjectOptionalMovementProvider(IMovementProvider provider)
        {
            _movementProvider = provider as UnityEngine.Object;
            MovementProvider = provider;
        }
        #endregion
    }
}
