// SDK Setup|Utilities|90030
namespace VRTK
{
    using UnityEngine;
#if UNITY_EDITOR
    using UnityEditor;
    using UnityEditor.Callbacks;
#endif
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;

    /// <summary>
    /// The SDK Setup describes a list of SDKs and game objects to use.
    /// </summary>
    public sealed class VRTK_SDKSetup : MonoBehaviour
    {
        public delegate void LoadEventHandler(VRTK_SDKManager sender, VRTK_SDKSetup setup);

        [Tooltip("Determines whether the SDK object references are automatically set to the objects of the selected SDKs. If this is true populating is done whenever the selected SDKs change.")]
        public bool autoPopulateObjectReferences = true;

        [Tooltip("A reference to the GameObject that is the user's boundary or play area, most likely provided by the SDK's Camera Rig.")]
        public GameObject actualBoundaries;
        [Tooltip("A reference to the GameObject that contains the VR camera, most likely provided by the SDK's Camera Rig Headset.")]
        public GameObject actualHeadset;
        [Tooltip("A reference to the GameObject that contains the SDK Left Hand Controller.")]
        public GameObject actualLeftController;
        [Tooltip("A reference to the GameObject that contains the SDK Right Hand Controller.")]
        public GameObject actualRightController;

        [Tooltip("A reference to the GameObject that models for the Left Hand Controller.")]
        public GameObject modelAliasLeftController;
        [Tooltip("A reference to the GameObject that models for the Right Hand Controller.")]
        public GameObject modelAliasRightController;

        public event LoadEventHandler Loaded;
        public event LoadEventHandler Unloaded;

        /// <summary>
        /// The info of the SDK to use to deal with all system actions. By setting this to `null` the fallback SDK will be used.
        /// </summary>
        public VRTK_SDKInfo systemSDKInfo
        {
            get
            {
                return cachedSystemSDKInfo;
            }
            set
            {
                value = value ?? VRTK_SDKInfo.Create<SDK_BaseSystem, SDK_FallbackSystem, SDK_FallbackSystem>()[0];
                if (cachedSystemSDKInfo == value)
                {
                    return;
                }

#if UNITY_EDITOR
                DestroyImmediate(cachedSystemSDK);
#else
                Destroy(cachedSystemSDK);
#endif
                cachedSystemSDK = null;

                cachedSystemSDKInfo = new VRTK_SDKInfo(value);
                PopulateObjectReferences(false);
            }
        }

        /// <summary>
        /// The info of the SDK to use to utilize room scale boundaries. By setting this to `null` the fallback SDK will be used.
        /// </summary>
        public VRTK_SDKInfo boundariesSDKInfo
        {
            get
            {
                return cachedBoundariesSDKInfo;
            }
            set
            {
                value = value ?? VRTK_SDKInfo.Create<SDK_BaseBoundaries, SDK_FallbackBoundaries, SDK_FallbackBoundaries>()[0];
                if (cachedBoundariesSDKInfo == value)
                {
                    return;
                }

#if UNITY_EDITOR
                DestroyImmediate(cachedBoundariesSDK);
#else
                Destroy(cachedBoundariesSDK);
#endif
                cachedBoundariesSDK = null;

                cachedBoundariesSDKInfo = new VRTK_SDKInfo(value);
                PopulateObjectReferences(false);
            }
        }

        /// <summary>
        /// The info of the SDK to use to utilize the VR headset. By setting this to `null` the fallback SDK will be used.
        /// </summary>
        public VRTK_SDKInfo headsetSDKInfo
        {
            get
            {
                return cachedHeadsetSDKInfo;
            }
            set
            {
                value = value ?? VRTK_SDKInfo.Create<SDK_BaseHeadset, SDK_FallbackHeadset, SDK_FallbackHeadset>()[0];
                if (cachedHeadsetSDKInfo == value)
                {
                    return;
                }

#if UNITY_EDITOR
                DestroyImmediate(cachedHeadsetSDK);
#else
                Destroy(cachedHeadsetSDK);
#endif
                cachedHeadsetSDK = null;

                cachedHeadsetSDKInfo = new VRTK_SDKInfo(value);
                PopulateObjectReferences(false);
            }
        }

        /// <summary>
        /// The info of the SDK to use to utilize the input devices. By setting this to `null` the fallback SDK will be used.
        /// </summary>
        public VRTK_SDKInfo controllerSDKInfo
        {
            get
            {
                return cachedControllerSDKInfo;
            }
            set
            {
                value = value ?? VRTK_SDKInfo.Create<SDK_BaseController, SDK_FallbackController, SDK_FallbackController>()[0];
                if (cachedControllerSDKInfo == value)
                {
                    return;
                }

#if UNITY_EDITOR
                DestroyImmediate(cachedControllerSDK);
#else
                Destroy(cachedControllerSDK);
#endif
                cachedControllerSDK = null;

                cachedControllerSDKInfo = new VRTK_SDKInfo(value);
                PopulateObjectReferences(false);
            }
        }

        /// <summary>
        /// The selected system SDK.
        /// </summary>
        /// <returns>The currently selected system SDK.</returns>
        public SDK_BaseSystem systemSDK
        {
            get
            {
                if (cachedSystemSDK == null)
                {
                    HandleSDKGetter<SDK_BaseSystem>("System", systemSDKInfo, VRTK_SDKManager.InstalledSystemSDKInfos);
                    cachedSystemSDK = (SDK_BaseSystem)ScriptableObject.CreateInstance(systemSDKInfo.type);
                }

                return cachedSystemSDK;
            }
        }

        /// <summary>
        /// The selected boundaries SDK.
        /// </summary>
        /// <returns>The currently selected boundaries SDK.</returns>
        public SDK_BaseBoundaries boundariesSDK
        {
            get
            {
                if (cachedBoundariesSDK == null)
                {
                    HandleSDKGetter<SDK_BaseBoundaries>("Boundaries", boundariesSDKInfo, VRTK_SDKManager.InstalledBoundariesSDKInfos);
                    cachedBoundariesSDK = (SDK_BaseBoundaries)ScriptableObject.CreateInstance(boundariesSDKInfo.type);
                }

                return cachedBoundariesSDK;
            }
        }

        /// <summary>
        /// The selected headset SDK.
        /// </summary>
        /// <returns>The currently selected headset SDK.</returns>
        public SDK_BaseHeadset headsetSDK
        {
            get
            {
                if (cachedHeadsetSDK == null)
                {
                    HandleSDKGetter<SDK_BaseHeadset>("Headset", headsetSDKInfo, VRTK_SDKManager.InstalledHeadsetSDKInfos);
                    cachedHeadsetSDK = (SDK_BaseHeadset)ScriptableObject.CreateInstance(headsetSDKInfo.type);
                }

                return cachedHeadsetSDK;
            }
        }

        /// <summary>
        /// The selected controller SDK.
        /// </summary>
        /// <returns>The currently selected controller SDK.</returns>
        public SDK_BaseController controllerSDK
        {
            get
            {
                if (cachedControllerSDK == null)
                {
                    HandleSDKGetter<SDK_BaseController>("Controller", controllerSDKInfo, VRTK_SDKManager.InstalledControllerSDKInfos);
                    cachedControllerSDK = (SDK_BaseController)ScriptableObject.CreateInstance(controllerSDKInfo.type);
                }

                return cachedControllerSDK;
            }
        }

        /// <summary>
        /// The VR device names used by the currently selected SDKs.
        /// </summary>
        public string[] usedVRDeviceNames
        {
            get
            {
                VRTK_SDKInfo[] infos = { systemSDKInfo, boundariesSDKInfo, headsetSDKInfo, controllerSDKInfo };
                return infos.Select(info => info.description.vrDeviceName)
                            .Distinct()
                            .ToArray();
            }
        }

        /// <summary>
        /// Whether it's possible to use the Setup. See `GetSimplifiedErrorDescriptions` for more info.
        /// </summary>
        public bool isValid
        {
            get
            {
                return GetSimplifiedErrorDescriptions().Length == 0;
            }
        }

        [SerializeField]
        private VRTK_SDKInfo cachedSystemSDKInfo = VRTK_SDKInfo.Create<SDK_BaseSystem, SDK_FallbackSystem, SDK_FallbackSystem>()[0];
        [SerializeField]
        private VRTK_SDKInfo cachedBoundariesSDKInfo = VRTK_SDKInfo.Create<SDK_BaseBoundaries, SDK_FallbackBoundaries, SDK_FallbackBoundaries>()[0];
        [SerializeField]
        private VRTK_SDKInfo cachedHeadsetSDKInfo = VRTK_SDKInfo.Create<SDK_BaseHeadset, SDK_FallbackHeadset, SDK_FallbackHeadset>()[0];
        [SerializeField]
        private VRTK_SDKInfo cachedControllerSDKInfo = VRTK_SDKInfo.Create<SDK_BaseController, SDK_FallbackController, SDK_FallbackController>()[0];

        private SDK_BaseSystem cachedSystemSDK;
        private SDK_BaseBoundaries cachedBoundariesSDK;
        private SDK_BaseHeadset cachedHeadsetSDK;
        private SDK_BaseController cachedControllerSDK;

        /// <summary>
        /// The PopulateObjectReferences method populates the object references by using the currently set SDKs.
        /// </summary>
        /// <param name="force">Whether to ignore `autoPopulateObjectReferences` while deciding to populate.</param>
        public void PopulateObjectReferences(bool force)
        {
            if (!(force || autoPopulateObjectReferences))
            {
                return;
            }

#if UNITY_EDITOR
            if (!EditorApplication.isPlaying && VRTK_SDKManager.ValidInstance())
            {
                VRTK_SDKManager.instance.SetLoadedSDKSetupToPopulateObjectReferences(this);
            }
#endif
            VRTK_SDK_Bridge.InvalidateCaches();

#if UNITY_EDITOR
            Undo.RecordObject(this, "Populate Object References");
#endif

            actualBoundaries = null;
            actualHeadset = null;
            actualLeftController = null;
            actualRightController = null;
            modelAliasLeftController = null;
            modelAliasRightController = null;

            Transform playAreaTransform = boundariesSDK.GetPlayArea();
            Transform headsetTransform = headsetSDK.GetHeadset();

            actualBoundaries = playAreaTransform == null ? null : playAreaTransform.gameObject;
            actualHeadset = headsetTransform == null ? null : headsetTransform.gameObject;
            actualLeftController = controllerSDK.GetControllerLeftHand(true);
            actualRightController = controllerSDK.GetControllerRightHand(true);
            modelAliasLeftController = controllerSDK.GetControllerModel(SDK_BaseController.ControllerHand.Left);
            modelAliasRightController = controllerSDK.GetControllerModel(SDK_BaseController.ControllerHand.Right);
        }

        /// <summary>
        /// The GetSimplifiedErrorDescriptions method checks the setup for errors and creates an array of error descriptions.
        /// </summary>
        /// <remarks>
        /// The returned error descriptions handle the following cases for the current SDK infos:
        /// <list type="bullet">
        /// <item> <description>Its type doesn't exist anymore.</description> </item>
        /// <item> <description>It's a fallback SDK.</description> </item>
        /// <item> <description>It doesn't have its scripting define symbols added.</description> </item>
        /// <item> <description>It's missing its vendor SDK.</description> </item>
        /// </list>
        /// Additionally the current SDK infos are checked whether they use multiple VR Devices.
        /// </remarks>
        /// <returns>An array of all the error descriptions. Returns an empty array if no errors are found.</returns>
        public string[] GetSimplifiedErrorDescriptions()
        {
            List<string> sdkErrorDescriptions = new List<string>();

            ReadOnlyCollection<VRTK_SDKInfo>[] installedSDKInfosList = {
                VRTK_SDKManager.InstalledSystemSDKInfos,
                VRTK_SDKManager.InstalledBoundariesSDKInfos,
                VRTK_SDKManager.InstalledHeadsetSDKInfos,
                VRTK_SDKManager.InstalledControllerSDKInfos
            };
            VRTK_SDKInfo[] currentSDKInfos = { systemSDKInfo, boundariesSDKInfo, headsetSDKInfo, controllerSDKInfo };

            for (int index = 0; index < installedSDKInfosList.Length; index++)
            {
                ReadOnlyCollection<VRTK_SDKInfo> installedSDKInfos = installedSDKInfosList[index];
                VRTK_SDKInfo currentSDKInfo = currentSDKInfos[index];

                Type baseType = VRTK_SharedMethods.GetBaseType(currentSDKInfo.type);
                if (baseType == null)
                {
                    continue;
                }

                if (currentSDKInfo.originalTypeNameWhenFallbackIsUsed != null)
                {
                    sdkErrorDescriptions.Add(string.Format("The SDK '{0}' doesn't exist anymore.", currentSDKInfo.originalTypeNameWhenFallbackIsUsed));
                }
                else if (currentSDKInfo.description.describesFallbackSDK)
                {
                    sdkErrorDescriptions.Add("A fallback SDK is used. Make sure to set a real SDK.");
                }
                else if (!installedSDKInfos.Contains(currentSDKInfo))
                {
                    sdkErrorDescriptions.Add(string.Format("The vendor SDK for '{0}' is not installed.", currentSDKInfo.description.prettyName));
                }
            }

            if (usedVRDeviceNames.Except(new[] { "None" }).Count() > 1)
            {
                sdkErrorDescriptions.Add("The current SDK selection uses multiple VR Devices. It's not possible to use more than one VR Device at the same time.");
            }

            return sdkErrorDescriptions.Distinct().ToArray();
        }

        /// <summary>
        /// The OnLoaded method determines when an SDK Setup has been loaded.
        /// </summary>
        /// <param name="sender">The SDK Manager that has loaded the SDK Setup.</param>
        public void OnLoaded(VRTK_SDKManager sender)
        {
            List<SDK_Base> sdkBases = new SDK_Base[] { systemSDK, boundariesSDK, headsetSDK, controllerSDK }.ToList();
            sdkBases.ForEach(sdkBase => sdkBase.OnBeforeSetupLoad(this));

            gameObject.SetActive(true);
            VRTK_SDK_Bridge.InvalidateCaches();
            SetupHeadset();
            SetupControllers();
            boundariesSDK.InitBoundaries();

            sdkBases.ForEach(sdkBase => sdkBase.OnAfterSetupLoad(this));

            LoadEventHandler handler = Loaded;
            if (handler != null)
            {
                handler(sender, this);
            }
        }

        /// <summary>
        /// The OnUnloaded method determines when an SDK Setup has been unloaded.
        /// </summary>
        /// <param name="sender">The SDK Manager that has unloaded the SDK Setup.</param>
        public void OnUnloaded(VRTK_SDKManager sender)
        {
            List<SDK_Base> sdkBases = new SDK_Base[] { systemSDK, boundariesSDK, headsetSDK, controllerSDK }.ToList();
            sdkBases.ForEach(sdkBase => sdkBase.OnBeforeSetupUnload(this));

            gameObject.SetActive(false);

            sdkBases.ForEach(sdkBase => sdkBase.OnAfterSetupUnload(this));

            LoadEventHandler handler = Unloaded;
            if (handler != null)
            {
                handler(sender, this);
            }
        }

        private void OnEnable()
        {
#pragma warning disable 618
            if (VRTK_SDKManager.ValidInstance() && !VRTK_SDKManager.instance.persistOnLoad)
#pragma warning restore 618
            {
                PopulateObjectReferences(false);
            }
        }

#if UNITY_EDITOR
        static VRTK_SDKSetup()
        {
            //call AutoPopulateObjectReferences when the currently active scene changes
#if UNITY_2018_1_OR_NEWER
            EditorApplication.hierarchyChanged += AutoPopulateObjectReferences;
#else
            EditorApplication.hierarchyWindowChanged += AutoPopulateObjectReferences;
#endif
        }

        [DidReloadScripts(2)]
        private static void AutoPopulateObjectReferences()
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                return;
            }

            foreach (VRTK_SDKSetup setup in VRTK_SharedMethods.FindEvenInactiveComponents<VRTK_SDKSetup>(true))
            {
                setup.PopulateObjectReferences(false);
            }
        }
#endif

        /// <summary>
        /// Handles the various SDK getters by logging potential errors.
        /// </summary>
        /// <typeparam name="BaseType">The SDK base type of which to handle the getter for. Must be a subclass of SDK_Base.</typeparam>
        /// <param name="prettyName">The pretty name of the base SDK to use when logging errors.</param>
        /// <param name="info">The SDK info of which the SDK getter was called.</param>
        /// <param name="installedInfos">The installed SDK infos of which the SDK getter was called.</param>
        private static void HandleSDKGetter<BaseType>(string prettyName, VRTK_SDKInfo info, IEnumerable<VRTK_SDKInfo> installedInfos) where BaseType : SDK_Base
        {
            if (VRTK_SharedMethods.IsEditTime())
            {
                return;
            }

            string sdkErrorDescription = GetSDKErrorDescription<BaseType>(prettyName, info, installedInfos);
            if (!string.IsNullOrEmpty(sdkErrorDescription))
            {
                VRTK_Logger.Error(sdkErrorDescription);
            }
        }

        /// <summary>
        /// Returns an error description in case any of these are true for the current SDK info:
        /// <list type="bullet">
        /// <item> <description>Its type doesn't exist anymore.</description> </item>
        /// <item> <description>It's a fallback SDK.</description> </item>
        /// <item> <description>It doesn't have its scripting define symbols added.</description> </item>
        /// <item> <description>It's missing its vendor SDK.</description> </item>
        /// </list>
        /// </summary>
        /// <typeparam name="BaseType">The SDK base type of which to return the error description for. Must be a subclass of SDK_Base.</typeparam>
        /// <param name="prettyName">The pretty name of the base SDK to use when returning error descriptions.</param>
        /// <param name="info">The SDK info of which to return the error description for.</param>
        /// <param name="installedInfos">The installed SDK infos.</param>
        /// <returns>An error description if there is one, else `null`.</returns>
        private static string GetSDKErrorDescription<BaseType>(string prettyName, VRTK_SDKInfo info, IEnumerable<VRTK_SDKInfo> installedInfos) where BaseType : SDK_Base
        {
            Type selectedType = info.type;
            Type baseType = typeof(BaseType);
            Type fallbackType = VRTK_SDKManager.SDKFallbackTypesByBaseType[baseType];

            if (selectedType == fallbackType)
            {
                return string.Format("The fallback {0} SDK is being used because there is no other {0} SDK set in the SDK Setup.", prettyName);
            }

            if (!VRTK_SharedMethods.IsTypeAssignableFrom(baseType, selectedType) || VRTK_SharedMethods.IsTypeAssignableFrom(fallbackType, selectedType))
            {
                string description = string.Format("The fallback {0} SDK is being used despite being set to '{1}'.", prettyName, selectedType.Name);

                if (installedInfos.Select(installedInfo => installedInfo.type).Contains(selectedType))
                {
                    return description + " Its needed scripting define symbols are not added. You can click the GameObject with the `VRTK_SDKManager` script attached to it in Edit Mode and choose to automatically let the manager handle the scripting define symbols." + VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.SCRIPTING_DEFINE_SYMBOLS_NOT_FOUND);
                }

                return description + " The needed vendor SDK isn't installed.";
            }

            return null;
        }

        private void SetupHeadset()
        {
            if (actualHeadset != null && !actualHeadset.GetComponent<VRTK_TrackedHeadset>())
            {
                actualHeadset.AddComponent<VRTK_TrackedHeadset>();
            }
        }

        private void SetupControllers()
        {
            Action<GameObject, GameObject> setParent = (scriptAliasGameObject, actualGameObject) =>
            {
                if (scriptAliasGameObject == null)
                {
                    return;
                }

                Transform scriptAliasTransform = scriptAliasGameObject.transform;
                Transform actualTransform = actualGameObject.transform;

                if (scriptAliasTransform.parent != actualTransform)
                {
                    Vector3 previousScale = scriptAliasTransform.localScale;
                    scriptAliasTransform.SetParent(actualTransform);
                    scriptAliasTransform.localScale = previousScale;
                }

                scriptAliasTransform.localPosition = Vector3.zero;
                scriptAliasTransform.localRotation = Quaternion.identity;
            };

            if (actualLeftController != null && VRTK_SDKManager.ValidInstance())
            {
                setParent(VRTK_SDKManager.instance.scriptAliasLeftController, actualLeftController);

                if (actualLeftController.GetComponent<VRTK_TrackedController>() == null)
                {
                    actualLeftController.AddComponent<VRTK_TrackedController>();
                }
            }

            if (actualRightController != null && VRTK_SDKManager.ValidInstance())
            {
                setParent(VRTK_SDKManager.instance.scriptAliasRightController, actualRightController);

                if (actualRightController.GetComponent<VRTK_TrackedController>() == null)
                {
                    actualRightController.AddComponent<VRTK_TrackedController>();
                }
            }
        }
    }
}