// Copyright HTC Corporation All Rights Reserved.

using UnityEngine.XR.OpenXR;
using UnityEngine.XR.OpenXR.Features;
using UnityEngine;
using System.Runtime.InteropServices;
using System;
using System.Linq;
using UnityEngine.XR;
using System.Collections.Generic;
using AOT;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.XR.OpenXR.Features;
#endif

namespace VIVE.OpenXR.Hand
{
#if UNITY_EDITOR
    [OpenXRFeature(UiName = "VIVE XR Hand Tracking",
        BuildTargetGroups = new[] { BuildTargetGroup.Android , BuildTargetGroup.Standalone },
        Company = "HTC",
        Desc = "Support the Hand Tracking extension.",
        DocumentationLink = "..\\Documentation",
        OpenxrExtensionStrings = kOpenxrExtensionString,
        Version = "4.0.0",
        FeatureId = featureId)]
#endif
    public class ViveHandTracking : OpenXRFeature
    {
        const string LOG_TAG = "VIVE.OpenXR.Hand.ViveHandTracking";
        void DEBUG(string msg) { Debug.Log(LOG_TAG + " " + msg); }
        void WARNING(string msg) { Debug.LogWarning(LOG_TAG + " " + msg); }
        void ERROR(string msg) { Debug.LogError(LOG_TAG + " " + msg); }

        /// <summary>
        /// OpenXR specification <see href="https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_EXT_hand_tracking">12.29 XR_EXT_hand_tracking</see>.
        /// </summary>
        public const string kOpenxrExtensionString = "XR_EXT_hand_tracking";
        /// <summary>
        /// The feature id string. This is used to give the feature a well known id for reference.
        /// </summary>
        public const string featureId = "vive.openxr.feature.hand.tracking";

        #region OpenXR Life Cycle
        private bool m_XrInstanceCreated = false;
        private XrInstance m_XrInstance = 0;
        private static IntPtr xrGetInstanceProcAddr_prev;
        private static IntPtr WaitFrame_prev;
        private static XrFrameWaitInfo m_frameWaitInfo;
        private static XrFrameState m_frameState;
        protected override IntPtr HookGetInstanceProcAddr(IntPtr func)
        {
            UnityEngine.Debug.Log("EXT: registering our own xrGetInstanceProcAddr");
            xrGetInstanceProcAddr_prev = func;
            return Marshal.GetFunctionPointerForDelegate(m_intercept_xrWaitFrame_xrGetInstanceProcAddr);
        }
        [MonoPInvokeCallback(typeof(OpenXRHelper.xrGetInstanceProcAddrDelegate))]
        private static XrResult intercept_xrWaitFrame_xrGetInstanceProcAddr(XrInstance instance, string name, out IntPtr function)
        {
            if (xrGetInstanceProcAddr_prev == null || xrGetInstanceProcAddr_prev == IntPtr.Zero)
            {
                UnityEngine.Debug.LogError("xrGetInstanceProcAddr_prev is null");
                function = IntPtr.Zero;
                return XrResult.XR_ERROR_VALIDATION_FAILURE;
            }

            // Get delegate of old xrGetInstanceProcAddr.
            var xrGetProc = Marshal.GetDelegateForFunctionPointer<OpenXRHelper.xrGetInstanceProcAddrDelegate>(xrGetInstanceProcAddr_prev);
            XrResult result = xrGetProc(instance, name, out function);
            if (name == "xrWaitFrame")
            {
                WaitFrame_prev = function;
                m_intercept_xrWaitFrame = intercepted_xrWaitFrame;
                function = Marshal.GetFunctionPointerForDelegate(m_intercept_xrWaitFrame); ;
                UnityEngine.Debug.Log("Getting xrWaitFrame func");
            }

            return result;

        }
        [MonoPInvokeCallback(typeof(OpenXRHelper.xrWaitFrameDelegate))]
        private static int intercepted_xrWaitFrame(ulong session, ref XrFrameWaitInfo frameWaitInfo, ref XrFrameState frameState)
        {
            // Get delegate of prev xrWaitFrame.
            var xrWaitFrame = Marshal.GetDelegateForFunctionPointer<OpenXRHelper.xrWaitFrameDelegate>(WaitFrame_prev);
            int res = xrWaitFrame(session, ref frameWaitInfo, ref frameState);
            m_frameWaitInfo = frameWaitInfo;
            m_frameState = frameState;
            return res;
        }

        /// <summary>
        /// Called when <see href="https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#xrCreateInstance">xrCreateInstance</see> is done.
        /// </summary>
        /// <param name="xrInstance">The created instance.</param>
        /// <returns>True for valid <see cref="XrInstance">XrInstance</see></returns>
        protected override bool OnInstanceCreate(ulong xrInstance)
        {
            if (!OpenXRRuntime.IsExtensionEnabled(kOpenxrExtensionString))
            {
                WARNING("OnInstanceCreate() " + kOpenxrExtensionString + " is NOT enabled.");
                return false;
            }

            m_XrInstanceCreated = true;
            m_XrInstance = xrInstance;
            DEBUG("OnInstanceCreate() " + m_XrInstance);

            return GetXrFunctionDelegates(m_XrInstance);
        }
        /// <summary>
        /// Called when <see href="https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#xrDestroyInstance">xrDestroyInstance</see> is done.
        /// </summary>
        /// <param name="xrInstance">The instance to destroy.</param>
        protected override void OnInstanceDestroy(ulong xrInstance)
        {
            m_XrInstanceCreated = false;
            m_XrInstance = 0;
            DEBUG("OnInstanceDestroy() " + xrInstance);
        }

        private XrSystemId m_XrSystemId = 0;
        /// <summary>
        /// Called when the <see cref="XrSystemId">XrSystemId</see> retrieved by <see href="https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#xrGetSystem">xrGetSystem</see> is changed.
        /// </summary>
        /// <param name="xrSystem">The system id.</param>
        protected override void OnSystemChange(ulong xrSystem)
        {
            m_XrSystemId = xrSystem;
            DEBUG("OnSystemChange() " + m_XrSystemId);
        }

        private bool m_XrSessionCreated = false;
        private XrSession m_XrSession = 0;
        private bool hasReferenceSpaceLocal = false, hasReferenceSpaceStage = false;
        private XrSpace m_ReferenceSpaceLocal = 0, m_ReferenceSpaceStage = 0;

        private bool hasLeftHandTracker = false, hasRightHandTracker = false;
        private XrHandTrackerEXT leftHandTracker = 0, rightHandTracker = 0;
        /// <summary>
        /// Called when <see href="https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#xrCreateSession">xrCreateSession</see> is done.
        /// </summary>
        /// <param name="xrSession">The created session ID.</param>
        protected override void OnSessionCreate(ulong xrSession)
        {
            m_XrSession = xrSession;
            m_XrSessionCreated = true;
            DEBUG("OnSessionCreate() " + m_XrSession);

            // Enumerate supported reference space types and create the XrSpace.
            XrReferenceSpaceType[] spaces = new XrReferenceSpaceType[Enum.GetNames(typeof(XrReferenceSpaceType)).Count()];
            UInt32 spaceCountOutput;
#pragma warning disable 0618
            if (EnumerateReferenceSpaces(
                spaceCapacityInput: 0,
                spaceCountOutput: out spaceCountOutput,
                spaces: out spaces[0]) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
            {
                DEBUG("OnSessionCreate() spaceCountOutput: " + spaceCountOutput);

                Array.Resize(ref spaces, (int)spaceCountOutput);
#pragma warning disable 0618
                if (EnumerateReferenceSpaces(
                    spaceCapacityInput: spaceCountOutput,
                    spaceCountOutput: out spaceCountOutput,
                    spaces: out spaces[0]) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
                {
                    XrReferenceSpaceCreateInfo createInfo;

                    /// Create m_ReferenceSpaceLocal
                    if (IsReferenceSpaceTypeSupported(spaceCountOutput, spaces, XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_LOCAL))
                    {
                        createInfo.type = XrStructureType.XR_TYPE_REFERENCE_SPACE_CREATE_INFO;
                        createInfo.next = IntPtr.Zero;
                        createInfo.referenceSpaceType = XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_LOCAL;//referenceSpaceType;
                        createInfo.poseInReferenceSpace.orientation = new XrQuaternionf(0, 0, 0, 1);
                        createInfo.poseInReferenceSpace.position = new XrVector3f(0, 0, 0);

#pragma warning disable 0618
                        if (CreateReferenceSpace(
                            createInfo: ref createInfo,
                            space: out m_ReferenceSpaceLocal) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
                        {
                            hasReferenceSpaceLocal = true;
                            DEBUG("OnSessionCreate() CreateReferenceSpace LOCAL: " + m_ReferenceSpaceLocal);
                        }
                        else
                        {
                            ERROR("OnSessionCreate() CreateReferenceSpace LOCAL failed.");
                        }
                    }

                    /// Create m_ReferenceSpaceStage
                    if (IsReferenceSpaceTypeSupported(spaceCountOutput, spaces, XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_STAGE))
                    {
                        createInfo.type = XrStructureType.XR_TYPE_REFERENCE_SPACE_CREATE_INFO;
                        createInfo.next = IntPtr.Zero;
                        createInfo.referenceSpaceType = XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_STAGE;
                        createInfo.poseInReferenceSpace.orientation = new XrQuaternionf(0, 0, 0, 1);
                        createInfo.poseInReferenceSpace.position = new XrVector3f(0, 0, 0);

#pragma warning disable 0618
                        if (CreateReferenceSpace(
                            createInfo: ref createInfo,
                            space: out m_ReferenceSpaceStage) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
                        {
                            hasReferenceSpaceStage = true;
                            DEBUG("OnSessionCreate() CreateReferenceSpace STAGE: " + m_ReferenceSpaceStage);
                        }
                        else
                        {
                            ERROR("OnSessionCreate() CreateReferenceSpace STAGE failed.");
                        }
                    }
                }
                else
                {
                    ERROR("OnSessionCreate() EnumerateReferenceSpaces(" + spaceCountOutput + ") failed.");
                }
            }
            else
            {
                ERROR("OnSessionCreate() EnumerateReferenceSpaces(0) failed.");
            }

            { // left hand tracker
                if (CreateHandTrackers(true, out XrHandTrackerEXT value))
                {
                    hasLeftHandTracker = true;
                    leftHandTracker = value;
                    DEBUG("OnSessionCreate() leftHandTracker " + leftHandTracker);
                }
            }
            { // right hand tracker
                if (CreateHandTrackers(false, out XrHandTrackerEXT value))
                {
                    hasRightHandTracker = true;
                    rightHandTracker = value;
                    DEBUG("OnSessionCreate() rightHandTracker " + rightHandTracker);
                }
            }
        }

        /// <summary>
        /// Called when <see href="https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#xrDestroySession">xrDestroySession</see> is done.
        /// </summary>
        /// <param name="xrSession">The session ID to destroy.</param>
        protected override void OnSessionDestroy(ulong xrSession)
        {
            DEBUG("OnSessionDestroy() " + xrSession);

            // Reference Space is binding with xrSession so we destroy the xrSpace when xrSession is destroyed.
            if (hasReferenceSpaceLocal)
            {
#pragma warning disable 0618
                if (DestroySpace(m_ReferenceSpaceLocal) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
                {
                    DEBUG("OnSessionDestroy() DestroySpace LOCAL " + m_ReferenceSpaceLocal);
                    m_ReferenceSpaceLocal = 0;
                }
                else
                {
                    ERROR("OnSessionDestroy() DestroySpace LOCAL " + m_ReferenceSpaceLocal + " failed.");
                }
                hasReferenceSpaceLocal = false;
            }
            if (hasReferenceSpaceStage)
            {
#pragma warning disable 0618
                if (DestroySpace(m_ReferenceSpaceStage) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
                {
                    DEBUG("OnSessionDestroy() DestroySpace STAGE " + m_ReferenceSpaceStage);
                    m_ReferenceSpaceStage = 0;
                }
                else
                {
                    ERROR("OnSessionDestroy() DestroySpace STAGE " + m_ReferenceSpaceStage + " failed.");
                }
                hasReferenceSpaceStage = false;
            }

            // Hand Tracking is binding with xrSession so we destroy the hand trackers when xrSession is destroyed.
            if (hasLeftHandTracker)
            {
                if (DestroyHandTrackerEXT(leftHandTracker) == XrResult.XR_SUCCESS)
                {
                    DEBUG("OnSessionDestroy() Left DestroyHandTrackerEXT " + leftHandTracker);
                }
                else
                {
                    ERROR("OnSessionDestroy() Left DestroyHandTrackerEXT " + leftHandTracker + " failed.");
                }
                hasLeftHandTracker = false;
            }
            if (hasRightHandTracker)
            {
                if (DestroyHandTrackerEXT(rightHandTracker) == XrResult.XR_SUCCESS)
                {
                    DEBUG("OnSessionDestroy() Right DestroyHandTrackerEXT " + rightHandTracker);
                }
                else
                {
                    ERROR("OnSessionDestroy() Right DestroyHandTrackerEXT " + rightHandTracker + " failed.");
                }
                hasRightHandTracker = false;
            }

            if (m_XrSession == xrSession)
            {
                m_XrSession = 0;
                m_XrSessionCreated = false;
            }
        }
        #endregion

        #region OpenXR function delegates
        private static readonly OpenXRHelper.xrGetInstanceProcAddrDelegate m_intercept_xrWaitFrame_xrGetInstanceProcAddr
            = new OpenXRHelper.xrGetInstanceProcAddrDelegate(intercept_xrWaitFrame_xrGetInstanceProcAddr);
        private static OpenXRHelper.xrWaitFrameDelegate m_intercept_xrWaitFrame;
        /// xrGetInstanceProcAddr
        OpenXRHelper.xrGetInstanceProcAddrDelegate XrGetInstanceProcAddr;

        /// xrGetSystemProperties
        OpenXRHelper.xrGetSystemPropertiesDelegate xrGetSystemProperties;
        /// <summary>
        /// An application can call GetSystemProperties to retrieve information about the system such as vendor ID, system name, and graphics and tracking properties.
        /// </summary>
        /// <param name="properties">Points to an instance of the XrSystemProperties structure, that will be filled with returned information.</param>
        /// <returns>XR_SUCCESS for success.</returns>
        [Obsolete("This function will become private in next release")]
        public XrResult GetSystemProperties(ref XrSystemProperties properties)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("GetSystemProperties() XR_ERROR_SESSION_LOST.");
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("GetSystemProperties() XR_ERROR_INSTANCE_LOST.");
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            return xrGetSystemProperties(m_XrInstance, m_XrSystemId, ref properties);
        }

        /// xrEnumerateReferenceSpaces
        OpenXRHelper.xrEnumerateReferenceSpacesDelegate xrEnumerateReferenceSpaces;
        /// <summary>
        /// Enumerates the set of reference space types that this runtime supports for a given session. Runtimes must always return identical buffer contents from this enumeration for the lifetime of the session.
        /// </summary>
        /// <param name="spaceCapacityInput">The capacity of the spaces array, or 0 to indicate a request to retrieve the required capacity.</param>
        /// <param name="spaceCountOutput">A pointer to the count of spaces written, or a pointer to the required capacity in the case that spaceCapacityInput is insufficient.</param>
        /// <param name="spaces">A pointer to an application-allocated array that will be filled with the enumerant of each supported reference space. It can be NULL if spaceCapacityInput is 0.</param>
        /// <returns>XR_SUCCESS for success.</returns>
        [Obsolete("This function will become private in next release")]
        public XrResult EnumerateReferenceSpaces(UInt32 spaceCapacityInput, out UInt32 spaceCountOutput, out XrReferenceSpaceType spaces)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("EnumerateReferenceSpaces() XR_ERROR_SESSION_LOST.");
                spaceCountOutput = 0;
                spaces = XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_UNBOUNDED_MSFT;
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("EnumerateReferenceSpaces() XR_ERROR_SESSION_LOST.");
                spaceCountOutput = 0;
                spaces = XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_UNBOUNDED_MSFT;
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            return xrEnumerateReferenceSpaces(m_XrSession, spaceCapacityInput, out spaceCountOutput, out spaces);
        }

        /// xrCreateReferenceSpace
        OpenXRHelper.xrCreateReferenceSpaceDelegate xrCreateReferenceSpace;
        /// <summary>
        /// Creates an <see cref="XrSpace">XrSpace</see> handle based on a chosen reference space. Application can provide an <see cref="XrPosef">XrPosef</see> to define the position and orientation of the new space’s origin within the natural reference frame of the reference space.
        /// </summary>
        /// <param name="createInfo">The XrReferenceSpaceCreateInfo used to specify the space.</param>
        /// <param name="space">The returned XrSpace handle.</param>
        /// <returns>XR_SUCCESS for success.</returns>
        [Obsolete("This function will become private in next release")]
        public XrResult CreateReferenceSpace(ref XrReferenceSpaceCreateInfo createInfo, out XrSpace space)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("CreateReferenceSpace() XR_ERROR_SESSION_LOST.");
                space = 0;
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("CreateReferenceSpace() XR_ERROR_INSTANCE_LOST.");
                space = 0;
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            return xrCreateReferenceSpace(m_XrSession, ref createInfo, out space);
        }

        /// xrDestroySpace
        OpenXRHelper.xrDestroySpaceDelegate xrDestroySpace;
        /// <summary>
        /// <see cref="XrSpace">XrSpace</see> handles are destroyed using DestroySpace. The runtime may still use this space if there are active dependencies (e.g, compositions in progress).
        /// </summary>
        /// <param name="space">Must be a valid XrSpace handle.</param>
        /// <returns>XR_SUCCESS for success.</returns>
        [Obsolete("This function will become private in next release")]
        public XrResult DestroySpace(XrSpace space)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("DestroySpace() XR_ERROR_SESSION_LOST.");
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("DestroySpace() XR_ERROR_INSTANCE_LOST.");
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            return xrDestroySpace(space);
        }

        /// xrCreateHandTrackerEXT
        ViveHandTrackingHelper.xrCreateHandTrackerEXTDelegate xrCreateHandTrackerEXT;
        /// <summary>
        /// An application can create an <see cref="XrHandTrackerEXT">XrHandTrackerEXT</see> handle using CreateHandTrackerEXT function.
        /// </summary>
        /// <param name="createInfo">The XrHandTrackerCreateInfoEXT used to specify the hand tracker.</param>
        /// <param name="handTracker">The returned XrHandTrackerEXT handle.</param>
        /// <returns>XR_SUCCESS for success.</returns>
        public XrResult CreateHandTrackerEXT(ref XrHandTrackerCreateInfoEXT createInfo, out XrHandTrackerEXT handTracker)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("CreateHandTrackerEXT() XR_ERROR_SESSION_LOST.");
                handTracker = 0;
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("CreateHandTrackerEXT() XR_ERROR_INSTANCE_LOST.");
                handTracker = 0;
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            if (createInfo.hand == XrHandEXT.XR_HAND_LEFT_EXT && hasLeftHandTracker)
            {
                DEBUG("CreateHandTrackerEXT() Left tracker " + leftHandTracker + " already created.");
                handTracker = leftHandTracker;
                return XrResult.XR_SUCCESS;
            }
            if (createInfo.hand == XrHandEXT.XR_HAND_RIGHT_EXT && hasRightHandTracker)
            {
                DEBUG("CreateHandTrackerEXT() Right tracker " + rightHandTracker + " already created.");
                handTracker = rightHandTracker;
                return XrResult.XR_SUCCESS;
            }

            return xrCreateHandTrackerEXT(m_XrSession, ref createInfo, out handTracker);
        }

        /// xrDestroyHandTrackerEXT
        ViveHandTrackingHelper.xrDestroyHandTrackerEXTDelegate xrDestroyHandTrackerEXT;
        /// <summary>
        /// Releases the handTracker and the underlying resources when finished with hand tracking experiences.
        /// </summary>
        /// <param name="handTracker">An XrHandTrackerEXT previously created by <see cref="CreateHandTrackerEXT">CreateHandTrackerEXT</see>.</param>
        /// <returns>XR_SUCCESS for success.</returns>
        public XrResult DestroyHandTrackerEXT(XrHandTrackerEXT handTracker)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("DestroyHandTrackerEXT() XR_ERROR_SESSION_LOST.");
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("DestroyHandTrackerEXT() XR_ERROR_INSTANCE_LOST.");
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            return xrDestroyHandTrackerEXT(handTracker);
        }

        /// xrLocateHandJointsEXT
        ViveHandTrackingHelper.xrLocateHandJointsEXTDelegate xrLocateHandJointsEXT;
        /// <summary>
        /// The LocateHandJointsEXT function locates an array of hand joints to a base space at given time.
        /// </summary>
        /// <param name="handTracker">An <see cref="XrHandTrackerEXT">XrHandTrackerEXT</see> previously created by <see cref="ViveHandTracking.CreateHandTrackerEXT(ref XrHandTrackerCreateInfoEXT, out XrHandTrackerEXT)">CreateHandTrackerEXT</see>.</param>
        /// <param name="locateInfo">A pointer to <see cref="XrHandJointsLocateInfoEXT">XrHandJointsLocateInfoEXT</see> describing information to locate hand joints.</param>
        /// <param name="locations">A pointer to <see cref="XrHandJointLocationsEXT">XrHandJointLocationsEXT</see> receiving the returned hand joint locations.</param>
        /// <returns></returns>
        public XrResult LocateHandJointsEXT(XrHandTrackerEXT handTracker, XrHandJointsLocateInfoEXT locateInfo, ref XrHandJointLocationsEXT locations)
        {
            if (!m_XrSessionCreated)
            {
                ERROR("LocateHandJointsEXT() XR_ERROR_SESSION_LOST.");
                return XrResult.XR_ERROR_SESSION_LOST;
            }
            if (!m_XrInstanceCreated)
            {
                ERROR("LocateHandJointsEXT() XR_ERROR_INSTANCE_LOST.");
                return XrResult.XR_ERROR_INSTANCE_LOST;
            }

            return xrLocateHandJointsEXT(handTracker, locateInfo, ref locations);
        }

        private bool GetXrFunctionDelegates(XrInstance xrInstance)
        {
            /// xrGetInstanceProcAddr
            if (xrGetInstanceProcAddr != null && xrGetInstanceProcAddr != IntPtr.Zero)
            {
                DEBUG("Get function pointer of xrGetInstanceProcAddr.");
                XrGetInstanceProcAddr = Marshal.GetDelegateForFunctionPointer(
                    xrGetInstanceProcAddr,
                    typeof(OpenXRHelper.xrGetInstanceProcAddrDelegate)) as OpenXRHelper.xrGetInstanceProcAddrDelegate;
            }
            else
            {
                ERROR("xrGetInstanceProcAddr");
                return false;
            }

            IntPtr funcPtr = IntPtr.Zero;
            /// xrGetSystemProperties
            if (XrGetInstanceProcAddr(xrInstance, "xrGetSystemProperties", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrGetSystemProperties.");
                    xrGetSystemProperties = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(OpenXRHelper.xrGetSystemPropertiesDelegate)) as OpenXRHelper.xrGetSystemPropertiesDelegate;
                }
            }
            else
            {
                ERROR("xrGetSystemProperties");
                return false;
            }
            /// xrEnumerateReferenceSpaces
            if (XrGetInstanceProcAddr(xrInstance, "xrEnumerateReferenceSpaces", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrEnumerateReferenceSpaces.");
                    xrEnumerateReferenceSpaces = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(OpenXRHelper.xrEnumerateReferenceSpacesDelegate)) as OpenXRHelper.xrEnumerateReferenceSpacesDelegate;
                }
            }
            else
            {
                ERROR("xrEnumerateReferenceSpaces");
                return false;
            }
            /// xrCreateReferenceSpace
            if (XrGetInstanceProcAddr(xrInstance, "xrCreateReferenceSpace", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrCreateReferenceSpace.");
                    xrCreateReferenceSpace = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(OpenXRHelper.xrCreateReferenceSpaceDelegate)) as OpenXRHelper.xrCreateReferenceSpaceDelegate;
                }
            }
            else
            {
                ERROR("xrCreateReferenceSpace");
                return false;
            }
            /// xrDestroySpace
            if (XrGetInstanceProcAddr(xrInstance, "xrDestroySpace", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrDestroySpace.");
                    xrDestroySpace = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(OpenXRHelper.xrDestroySpaceDelegate)) as OpenXRHelper.xrDestroySpaceDelegate;
                }
            }
            else
            {
                ERROR("xrDestroySpace");
                return false;
            }

            /// xrCreateHandTrackerEXT
            if (XrGetInstanceProcAddr(xrInstance, "xrCreateHandTrackerEXT", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrCreateHandTrackerEXT.");
                    xrCreateHandTrackerEXT = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(ViveHandTrackingHelper.xrCreateHandTrackerEXTDelegate)) as ViveHandTrackingHelper.xrCreateHandTrackerEXTDelegate;
                }
            }
            else
            {
                ERROR("xrCreateHandTrackerEXT");
                return false;
            }
            /// xrDestroyHandTrackerEXT
            if (XrGetInstanceProcAddr(xrInstance, "xrDestroyHandTrackerEXT", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrDestroyHandTrackerEXT.");
                    xrDestroyHandTrackerEXT = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(ViveHandTrackingHelper.xrDestroyHandTrackerEXTDelegate)) as ViveHandTrackingHelper.xrDestroyHandTrackerEXTDelegate;
                }
            }
            else
            {
                ERROR("xrDestroyHandTrackerEXT");
                return false;
            }
            /// xrLocateHandJointsEXT
            if (XrGetInstanceProcAddr(xrInstance, "xrLocateHandJointsEXT", out funcPtr) == XrResult.XR_SUCCESS)
            {
                if (funcPtr != IntPtr.Zero)
                {
                    DEBUG("Get function pointer of xrLocateHandJointsEXT.");
                    xrLocateHandJointsEXT = Marshal.GetDelegateForFunctionPointer(
                        funcPtr,
                        typeof(ViveHandTrackingHelper.xrLocateHandJointsEXTDelegate)) as ViveHandTrackingHelper.xrLocateHandJointsEXTDelegate;
                }
            }
            else
            {
                ERROR("xrLocateHandJointsEXT");
                return false;
            }

            return true;
        }
        #endregion

        static List<XRInputSubsystem> s_InputSubsystems = new List<XRInputSubsystem>();
        /// <summary>
        /// Retrieves the current tracking origin in Unity XR.
        /// </summary>
        /// <returns>The tracking origin in <see href="https://docs.unity3d.com/ScriptReference/XR.TrackingOriginModeFlags.html">TrackingOriginModeFlags</see></returns>
        public TrackingOriginModeFlags GetTrackingOriginMode()
        {
            XRInputSubsystem subsystem = null;

            SubsystemManager.GetInstances(s_InputSubsystems);
            if (s_InputSubsystems.Count > 0)
            {
                subsystem = s_InputSubsystems[0];
            }

            if (subsystem != null)
            {
                return subsystem.GetTrackingOriginMode();
            }

            return TrackingOriginModeFlags.Unknown;
        }
        private bool IsReferenceSpaceTypeSupported(UInt32 spaceCountOutput, XrReferenceSpaceType[] spaces, XrReferenceSpaceType space)
        {
            bool support = false;
            for (int i = 0; i < spaceCountOutput; i++)
            {
                DEBUG("IsReferenceSpaceTypeSupported() supported space[" + i + "]: " + spaces[i]);
                if (spaces[i] == space) { support = true; }
            }

            return support;
        }

        XrSystemHandTrackingPropertiesEXT handTrackingSystemProperties;
        XrSystemProperties systemProperties;
        private bool IsHandTrackingSupported()
        {
            bool ret = false;
            if (!m_XrSessionCreated)
            {
                ERROR("IsHandTrackingSupported() session is not created.");
                return ret;
            }

            handTrackingSystemProperties.type = XrStructureType.XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT;
            systemProperties.type = XrStructureType.XR_TYPE_SYSTEM_PROPERTIES;
            systemProperties.next = Marshal.AllocHGlobal(Marshal.SizeOf(handTrackingSystemProperties));

            long offset = 0;
            if (IntPtr.Size == 4)
                offset = systemProperties.next.ToInt32();
            else
                offset = systemProperties.next.ToInt64();

            IntPtr sys_hand_tracking_prop_ptr = new IntPtr(offset);
            Marshal.StructureToPtr(handTrackingSystemProperties, sys_hand_tracking_prop_ptr, false);

#pragma warning disable 0618
            if (GetSystemProperties(ref systemProperties) == XrResult.XR_SUCCESS)
#pragma warning restore 0618
            {
                if (IntPtr.Size == 4)
                    offset = systemProperties.next.ToInt32();
                else
                    offset = systemProperties.next.ToInt64();

                sys_hand_tracking_prop_ptr = new IntPtr(offset);
                handTrackingSystemProperties = (XrSystemHandTrackingPropertiesEXT)Marshal.PtrToStructure(sys_hand_tracking_prop_ptr, typeof(XrSystemHandTrackingPropertiesEXT));

                DEBUG("IsHandTrackingSupported() XrSystemHandTrackingPropertiesEXT.supportsHandTracking: " + handTrackingSystemProperties.supportsHandTracking);
                ret = handTrackingSystemProperties.supportsHandTracking > 0;
            }
            else
            {
                ERROR("IsHandTrackingSupported() GetSystemProperties failed.");
            }

            Marshal.FreeHGlobal(systemProperties.next);
            return ret;
        }

        private bool CreateHandTrackers(bool isLeft, out XrHandTrackerEXT handTracker)
        {
            if (!IsHandTrackingSupported())
            {
                ERROR("CreateHandTrackers() " + (isLeft ? "Left" : "Right") + " hand tracking is NOT supported.");
                handTracker = 0;
                return false;
            }

            XrHandTrackerCreateInfoEXT createInfo;
            createInfo.type = XrStructureType.XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT;
            createInfo.next = IntPtr.Zero;
            createInfo.hand = isLeft ? XrHandEXT.XR_HAND_LEFT_EXT : XrHandEXT.XR_HAND_RIGHT_EXT;
            createInfo.handJointSet = XrHandJointSetEXT.XR_HAND_JOINT_SET_DEFAULT_EXT;

            var ret = CreateHandTrackerEXT(ref createInfo, out handTracker);
            DEBUG("CreateHandTrackers() " + (isLeft ? "Left" : "Right") + " CreateHandTrackerEXT = " + ret);

            return ret == XrResult.XR_SUCCESS;
        }

        private XrHandJointLocationEXT[] jointLocationsL = new XrHandJointLocationEXT[(int)XrHandJointEXT.XR_HAND_JOINT_MAX_ENUM_EXT];
        private XrHandJointLocationEXT[] jointLocationsR = new XrHandJointLocationEXT[(int)XrHandJointEXT.XR_HAND_JOINT_MAX_ENUM_EXT];
        private XrHandJointLocationsEXT locations = new XrHandJointLocationsEXT(XrStructureType.XR_TYPE_HAND_JOINT_LOCATIONS_EXT, IntPtr.Zero, false, 0, IntPtr.Zero);

        public bool GetHandTrackingSpace(out XrSpace space)
        {
            space = 0;

            TrackingOriginModeFlags origin = GetTrackingOriginMode();
            if (origin == TrackingOriginModeFlags.Unknown || origin == TrackingOriginModeFlags.Unbounded) { return false; }

            space = (origin == TrackingOriginModeFlags.Device ? m_ReferenceSpaceLocal : m_ReferenceSpaceStage);
            return true;
        }

        /// <summary>
        /// Retrieves the <see cref="XrHandJointLocationEXT"> XrHandJointLocationEXT </see> data.
        /// </summary>
        /// <param name="isLeft">Left or right hand.</param>
        /// <param name="handJointLocation">Output parameter to retrieve <see cref="XrHandJointLocationEXT"> XrHandJointLocationEXT </see> data.</param>
        /// <returns>True for valid data.</returns>
        public bool GetJointLocations(bool isLeft, out XrHandJointLocationEXT[] handJointLocation)
        {
            bool ret = false;
            handJointLocation = isLeft ? jointLocationsL : jointLocationsR;

            if (isLeft && !hasLeftHandTracker) { return ret; }
            if (!isLeft && !hasRightHandTracker) { return ret; }

            TrackingOriginModeFlags origin = GetTrackingOriginMode();
            if (origin == TrackingOriginModeFlags.Unknown || origin == TrackingOriginModeFlags.Unbounded) { return ret; }
            XrSpace baseSpace = (origin == TrackingOriginModeFlags.Device ? m_ReferenceSpaceLocal : m_ReferenceSpaceStage);

            /// Configures XrHandJointsLocateInfoEXT
            XrHandJointsLocateInfoEXT locateInfo = new XrHandJointsLocateInfoEXT(
                in_type: XrStructureType.XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT,
                in_next: IntPtr.Zero,
                in_baseSpace: baseSpace,
                in_time: m_frameState.predictedDisplayTime);

            /// Configures XrHandJointLocationsEXT
            locations.type = XrStructureType.XR_TYPE_HAND_JOINT_LOCATIONS_EXT;
            locations.next = IntPtr.Zero;
            locations.isActive = false;
            locations.jointCount = (uint)(isLeft ? jointLocationsL.Length : jointLocationsR.Length);

            XrHandJointLocationEXT joint_location_ext_type = default(XrHandJointLocationEXT);
            int jointLocationsLength = isLeft ? jointLocationsL.Length : jointLocationsR.Length;
            locations.jointLocations = Marshal.AllocHGlobal(Marshal.SizeOf(joint_location_ext_type) * jointLocationsLength);

            long offset = 0;
            /*if (IntPtr.Size == 4)
                offset = locations.jointLocations.ToInt32();
            else
                offset = locations.jointLocations.ToInt64();

            for (int i = 0; i < jointLocationsLength; i++)
            {
                IntPtr joint_location_ext_ptr = new IntPtr(offset);

                if (isLeft)
                    Marshal.StructureToPtr(jointLocationsL[i], joint_location_ext_ptr, false);
                else
                    Marshal.StructureToPtr(jointLocationsR[i], joint_location_ext_ptr, false);

                offset += Marshal.SizeOf(joint_location_ext_type);
            }*/

            if (LocateHandJointsEXT(
                handTracker: (isLeft ? leftHandTracker : rightHandTracker),
                locateInfo: locateInfo,
                locations: ref locations) == XrResult.XR_SUCCESS)
            {
                if (locations.isActive)
                {
                    if (IntPtr.Size == 4)
                        offset = locations.jointLocations.ToInt32();
                    else
                        offset = locations.jointLocations.ToInt64();

                    for (int i = 0; i < locations.jointCount; i++)
                    {
                        IntPtr joint_location_ext_ptr = new IntPtr(offset);

                        if (isLeft)
                            jointLocationsL[i] = (XrHandJointLocationEXT)Marshal.PtrToStructure(joint_location_ext_ptr, typeof(XrHandJointLocationEXT));
                        else
                            jointLocationsR[i] = (XrHandJointLocationEXT)Marshal.PtrToStructure(joint_location_ext_ptr, typeof(XrHandJointLocationEXT));

                        offset += Marshal.SizeOf(joint_location_ext_type);
                    }

                    // ToDo: locationFlags?
                    handJointLocation = isLeft ? jointLocationsL : jointLocationsR;

                    ret = true;
                }
            }

            Marshal.FreeHGlobal(locations.jointLocations);
            return ret;
        }
    }
}
