// SDK Info|Utilities|90040
namespace VRTK
{
    using UnityEngine;
    using System;
    using System.Collections.Generic;
    using System.Linq;

    /// <summary>
    /// Holds all the info necessary to describe an SDK.
    /// </summary>
    [Serializable]
    public sealed class VRTK_SDKInfo : ISerializationCallbackReceiver
    {
        /// <summary>
        /// The type of the SDK.
        /// </summary>
        public Type type { get; private set; }
        /// <summary>
        /// The name of the type of which this SDK info was created from. This is only used if said type wasn't found.
        /// </summary>
        public string originalTypeNameWhenFallbackIsUsed { get; private set; }
        /// <summary>
        /// The description of the SDK.
        /// </summary>
        public SDK_DescriptionAttribute description { get; private set; }

        [SerializeField]
        private string baseTypeName;
        [SerializeField]
        private string fallbackTypeName;
        [SerializeField]
        private string typeName;
        [SerializeField]
        private int descriptionIndex;

        /// <summary>
        /// Creates new SDK infos for a type that is known at compile time.
        /// </summary>
        /// <typeparam name="BaseType">The SDK base type. Must be a subclass of SDK_Base.</typeparam>
        /// <typeparam name="FallbackType">The SDK type to fall back on if problems occur. Must be a subclass of `BaseType`.</typeparam>
        /// <typeparam name="ActualType">The SDK type to use. Must be a subclass of `BaseType`.</typeparam>
        /// <returns>Multiple newly created instances.</returns>
        public static VRTK_SDKInfo[] Create<BaseType, FallbackType, ActualType>() where BaseType : SDK_Base where FallbackType : BaseType where ActualType : BaseType
        {
            return Create<BaseType, FallbackType>(typeof(ActualType));
        }

        /// <summary>
        /// Creates new SDK infos for a type.
        /// </summary>
        /// <typeparam name="BaseType">The SDK base type. Must be a subclass of SDK_Base.</typeparam>
        /// <typeparam name="FallbackType">The SDK type to fall back on if problems occur. Must be a subclass of `BaseType.</typeparam>
        /// <param name="actualType">The SDK type to use. Must be a subclass of `BaseType.</param>
        /// <returns>Multiple newly created instances.</returns>
        public static VRTK_SDKInfo[] Create<BaseType, FallbackType>(Type actualType) where BaseType : SDK_Base where FallbackType : BaseType
        {
            string actualTypeName = actualType.FullName;

            SDK_DescriptionAttribute[] descriptions = SDK_DescriptionAttribute.GetDescriptions(actualType);
            if (descriptions.Length == 0)
            {
                VRTK_Logger.Fatal(string.Format("'{0}' doesn't specify any SDK descriptions via '{1}'.", actualTypeName, typeof(SDK_DescriptionAttribute).Name));
                return new VRTK_SDKInfo[0];
            }

            HashSet<VRTK_SDKInfo> sdkInfos = new HashSet<VRTK_SDKInfo>();
            foreach (SDK_DescriptionAttribute description in descriptions)
            {
                VRTK_SDKInfo sdkInfo = new VRTK_SDKInfo();
                sdkInfo.SetUp(typeof(BaseType), typeof(FallbackType), actualTypeName, description.index);
                sdkInfos.Add(sdkInfo);
            }

            return sdkInfos.ToArray();
        }

        private VRTK_SDKInfo()
        {
        }

        /// <summary>
        /// Creates a new SDK info by copying an existing one.
        /// </summary>
        /// <param name="infoToCopy">The SDK info to copy.</param>
        public VRTK_SDKInfo(VRTK_SDKInfo infoToCopy)
        {
            SetUp(Type.GetType(infoToCopy.baseTypeName),
                  Type.GetType(infoToCopy.fallbackTypeName),
                  infoToCopy.typeName,
                  infoToCopy.descriptionIndex);
        }

        private void SetUp(Type baseType, Type fallbackType, string actualTypeName, int descriptionIndex)
        {
            if (baseType == null || fallbackType == null)
                return;
            if (!VRTK_SharedMethods.IsTypeSubclassOf(baseType, typeof(SDK_Base)))
            {
                VRTK_Logger.Fatal(new ArgumentOutOfRangeException("baseType", baseType, string.Format("'{0}' is not a subclass of the SDK base type '{1}'.", baseType.Name, typeof(SDK_Base).Name)));
                return;
            }

            if (!VRTK_SharedMethods.IsTypeSubclassOf(fallbackType, baseType))
            {
                VRTK_Logger.Fatal(new ArgumentOutOfRangeException("fallbackType", fallbackType, string.Format("'{0}' is not a subclass of the SDK base type '{1}'.", fallbackType.Name, baseType.Name)));
                return;
            }

            baseTypeName = baseType.FullName;
            fallbackTypeName = fallbackType.FullName;
            typeName = actualTypeName;

            if (string.IsNullOrEmpty(actualTypeName))
            {
                type = fallbackType;
                originalTypeNameWhenFallbackIsUsed = null;
                this.descriptionIndex = -1;
                description = new SDK_DescriptionAttribute(typeof(SDK_FallbackSystem));

                return;
            }

            Type actualType = Type.GetType(actualTypeName);
            if (actualType == null)
            {
                type = fallbackType;
                originalTypeNameWhenFallbackIsUsed = actualTypeName;
                this.descriptionIndex = -1;
                description = new SDK_DescriptionAttribute(typeof(SDK_FallbackSystem));

                VRTK_Logger.Error(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.SDK_NOT_FOUND, actualTypeName, fallbackType.Name));

                return;
            }

            if (!VRTK_SharedMethods.IsTypeSubclassOf(actualType, baseType))
            {
                VRTK_Logger.Fatal(new ArgumentOutOfRangeException("actualTypeName", actualTypeName, string.Format("'{0}' is not a subclass of the SDK base type '{1}'.", actualTypeName, baseType.Name)));
                return;
            }

            SDK_DescriptionAttribute[] descriptions = SDK_DescriptionAttribute.GetDescriptions(actualType);
            if (descriptions.Length <= descriptionIndex)
            {
                VRTK_Logger.Fatal(new ArgumentOutOfRangeException("descriptionIndex", descriptionIndex, string.Format("'{0}' has no '{1}' at that index.", actualTypeName, typeof(SDK_DescriptionAttribute).Name)));
                return;
            }

            type = actualType;
            originalTypeNameWhenFallbackIsUsed = null;
            this.descriptionIndex = descriptionIndex;
            description = descriptions[descriptionIndex];
        }

        #region ISerializationCallbackReceiver

        public void OnBeforeSerialize()
        {
        }

        public void OnAfterDeserialize()
        {
            SetUp(Type.GetType(baseTypeName), Type.GetType(fallbackTypeName), typeName, descriptionIndex);
        }

        #endregion

        #region Equality via type and descriptionIndex

        public override bool Equals(object obj)
        {
            VRTK_SDKInfo other = obj as VRTK_SDKInfo;
            if ((object)other == null)
            {
                return false;
            }

            return this == other;
        }

        public bool Equals(VRTK_SDKInfo other)
        {
            return this == other;
        }

        public override int GetHashCode()
        {
            return type.GetHashCode();
        }

        public static bool operator ==(VRTK_SDKInfo x, VRTK_SDKInfo y)
        {
            if (ReferenceEquals(x, y))
            {
                return true;
            }

            if ((object)x == null || (object)y == null)
            {
                return false;
            }

            return x.type == y.type && x.descriptionIndex == y.descriptionIndex;
        }

        public static bool operator !=(VRTK_SDKInfo x, VRTK_SDKInfo y)
        {
            return !(x == y);
        }

        #endregion
    }
}