using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using UnityEditor.AddressableAssets.Build.DataBuilders;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using UnityEngine.Serialization;

// ReSharper disable DelegateSubtraction

namespace UnityEditor.AddressableAssets.HostingServices
{
    /// <summary>
    /// Manages the hosting services.
    /// </summary>
    [Serializable]
    public class HostingServicesManager : ISerializationCallbackReceiver
    {
        internal const string KPrivateIpAddressKey = "PrivateIpAddress";

        internal string GetPrivateIpAddressKey(int id = 0)
        {
            if (id == 0)
                return KPrivateIpAddressKey;
            return $"{KPrivateIpAddressKey}_{id}";
        }

        [Serializable]
        internal class HostingServiceInfo
        {
            [SerializeField]
            internal string classRef;

            [SerializeField]
            internal KeyDataStore dataStore;
        }

        [FormerlySerializedAs("m_hostingServiceInfos")]
        [SerializeField]
        List<HostingServiceInfo> m_HostingServiceInfos;

        [FormerlySerializedAs("m_settings")]
        [SerializeField]
        AddressableAssetSettings m_Settings;

        [FormerlySerializedAs("m_nextInstanceId")]
        [SerializeField]
        int m_NextInstanceId;

        [FormerlySerializedAs("m_registeredServiceTypeRefs")]
        [SerializeField]
        List<string> m_RegisteredServiceTypeRefs;

        readonly Type[] m_BuiltinServiceTypes =
        {
            typeof(HttpHostingService)
        };

        Dictionary<IHostingService, HostingServiceInfo> m_HostingServiceInfoMap;
        ILogger m_Logger;
        List<Type> m_RegisteredServiceTypes;

        [SerializeField]
        int m_PingTimeoutInMilliseconds = 5000;
        /// <summary>
        /// Timeout in milliseconds for filtering ip addresses for the hosting service
        /// </summary>
        internal int PingTimeoutInMilliseconds
        {
            get { return m_PingTimeoutInMilliseconds; }
            set { m_PingTimeoutInMilliseconds = value; }
        }

        /// <summary>
        /// Key/Value pairs valid for profile variable substitution
        /// </summary>
        public Dictionary<string, string> GlobalProfileVariables { get; private set; }

        internal static readonly string k_GlobalProfileVariablesCountKey = $"com.unity.addressables.{nameof(GlobalProfileVariables)}Count";

        internal static string GetSessionStateKey(int id)
        {
            return $"com.unity.addressables.{nameof(GlobalProfileVariables)}{id}";
        }

        /// <summary>
        /// Direct logging output of all managed services
        /// </summary>
        public ILogger Logger
        {
            get { return m_Logger; }
            set
            {
                m_Logger = value ?? Debug.unityLogger;
                foreach (var svc in HostingServices)
                    svc.Logger = m_Logger;
            }
        }

        /// <summary>
        /// Static method for use in starting up the HostingServicesManager in batch mode.
        /// </summary>
        /// <param name="settings"> </param>
        public static void BatchMode(AddressableAssetSettings settings)
        {
            if (settings == null)
            {
                Debug.LogError("Could not load Addressable Assets settings - aborting.");
                return;
            }

            var manager = settings.HostingServicesManager;
            if (manager == null)
            {
                Debug.LogError("Could not load HostingServicesManager - aborting.");
                return;
            }

            manager.StartAllServices();
        }

        /// <summary>
        /// Static method for use in starting up the HostingServicesManager in batch mode. This method
        /// without parameters will find and use the default <see cref="AddressableAssetSettings"/> object.
        /// </summary>
        public static void BatchMode()
        {
            BatchMode(AddressableAssetSettingsDefaultObject.Settings);
        }

        /// <summary>
        /// Indicates whether or not this HostingServiceManager is initialized
        /// </summary>
        public bool IsInitialized
        {
            get { return m_Settings != null; }
        }

        /// <summary>
        /// Return an enumerable list of all configured <see cref="IHostingService"/> objects
        /// </summary>
        public ICollection<IHostingService> HostingServices
        {
            get { return m_HostingServiceInfoMap.Keys; }
        }

        /// <summary>
        /// Get an array of all <see cref="IHostingService"/> types that have been used by the manager, or are known
        /// built-in types available for use.
        /// </summary>
        /// <returns></returns>
        public Type[] RegisteredServiceTypes
        {
            get
            {
                if (m_RegisteredServiceTypes.Count == 0)
                    m_RegisteredServiceTypes.AddRange(m_BuiltinServiceTypes);

                return m_RegisteredServiceTypes.ToArray();
            }
        }

        /// <summary>
        /// The id value that will be assigned to the next <see cref="IHostingService"/> add to the manager.
        /// </summary>
        public int NextInstanceId
        {
            get { return m_NextInstanceId; }
        }

        /// <summary>
        /// Create a new <see cref="HostingServicesManager"/>
        /// </summary>
        public HostingServicesManager()
        {
            GlobalProfileVariables = new Dictionary<string, string>();
            m_HostingServiceInfos = new List<HostingServiceInfo>();
            m_HostingServiceInfoMap = new Dictionary<IHostingService, HostingServiceInfo>();
            m_RegisteredServiceTypes = new List<Type>();
            m_RegisteredServiceTypeRefs = new List<string>();
            m_Logger = Debug.unityLogger;
        }

        /// <summary>
        /// Initialize manager with the given <see cref="AddressableAssetSettings"/> object.
        /// </summary>
        /// <param name="settings"></param>
        public void Initialize(AddressableAssetSettings settings)
        {
            if (IsInitialized) return;
            m_Settings = settings;
            RefreshGlobalProfileVariables();
        }

        /// <summary>
        /// Calls <see cref="IHostingService.StopHostingService"/> on all managed <see cref="IHostingService"/> instances
        /// where <see cref="IHostingService.IsHostingServiceRunning"/> is true
        /// </summary>
        public void StopAllServices()
        {
            foreach (var svc in HostingServices)
            {
                try
                {
                    if (svc.IsHostingServiceRunning)
                        svc.StopHostingService();
                }
                catch (Exception e)
                {
                    m_Logger.LogFormat(LogType.Error, e.Message);
                }
            }
        }

        /// <summary>
        /// Calls <see cref="IHostingService.StartHostingService"/> on all managed <see cref="IHostingService"/> instances
        /// where <see cref="IHostingService.IsHostingServiceRunning"/> is false
        /// </summary>
        public void StartAllServices()
        {
            foreach (var svc in HostingServices)
            {
                try
                {
                    if (!svc.IsHostingServiceRunning)
                        svc.StartHostingService();
                }
                catch (Exception e)
                {
                    m_Logger.LogFormat(LogType.Error, e.Message);
                }
            }
        }

        /// <summary>
        /// Add a new hosting service instance of the given type. The <paramref name="serviceType"/> must implement the
        /// <see cref="IHostingService"/> interface, or an <see cref="ArgumentException"/> is thrown.
        /// </summary>
        /// <param name="serviceType">A <see cref="Type"/> object for the service. Must implement <see cref="IHostingService"/></param>
        /// <param name="name">A descriptive name for the new service instance.</param>
        /// <returns></returns>
        public IHostingService AddHostingService(Type serviceType, string name)
        {
            var svc = Activator.CreateInstance(serviceType) as IHostingService;
            if (svc == null)
                throw new ArgumentException("Provided type does not implement IHostingService", "serviceType");

            if (!m_RegisteredServiceTypes.Contains(serviceType))
                m_RegisteredServiceTypes.Add(serviceType);

            var info = new HostingServiceInfo
            {
                classRef = TypeToClassRef(serviceType),
                dataStore = new KeyDataStore()
            };

            svc.Logger = m_Logger;
            svc.DescriptiveName = name;
            svc.InstanceId = m_NextInstanceId;
            svc.HostingServiceContentRoots.AddRange(GetAllContentRoots());
            m_Settings.profileSettings.RegisterProfileStringEvaluationFunc(svc.EvaluateProfileString);

            m_HostingServiceInfoMap.Add(svc, info);
            m_Settings.SetDirty(AddressableAssetSettings.ModificationEvent.HostingServicesManagerModified, this, true, true);
            AddressableAssetUtility.OpenAssetIfUsingVCIntegration(m_Settings);

            m_NextInstanceId++;
            return svc;
        }

        /// <summary>
        /// Stops the given <see cref="IHostingService"/>, unregisters callbacks, and removes it from management. This
        /// function does nothing if the service is not being managed by this <see cref="HostingServicesManager"/>
        /// </summary>
        /// <param name="svc"></param>
        public void RemoveHostingService(IHostingService svc)
        {
            if (!m_HostingServiceInfoMap.ContainsKey(svc))
                return;

            svc.StopHostingService();
            m_Settings.profileSettings.UnregisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
            m_HostingServiceInfoMap.Remove(svc);
            m_Settings.SetDirty(AddressableAssetSettings.ModificationEvent.HostingServicesManagerModified, this, true, true);
            AddressableAssetUtility.OpenAssetIfUsingVCIntegration(m_Settings);
        }

        /// <summary>
        /// Should be called by parent <see cref="ScriptableObject"/> instance Awake method
        /// </summary>
        internal void OnAwake()
        {
            RefreshGlobalProfileVariables();
        }

        /// <summary>
        /// Should be called by parent <see cref="ScriptableObject"/> instance OnEnable method
        /// </summary>
        public void OnEnable()
        {
            Debug.Assert(IsInitialized);

            m_Settings.OnModification -= OnSettingsModification;
            m_Settings.OnModification += OnSettingsModification;
            m_Settings.profileSettings.RegisterProfileStringEvaluationFunc(EvaluateGlobalProfileVariableKey);

            // GetAllContentRoots can return unpredictable results when there are no hosting services
            if (HostingServices.Count > 0)
            {
                var contentRoots = GetAllContentRoots();
                foreach (var svc in HostingServices)
                {

                    svc.Logger = m_Logger;
                    m_Settings.profileSettings.RegisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
                    var baseSvc = svc as BaseHostingService;
                    svc.HostingServiceContentRoots.Clear();
                    svc.HostingServiceContentRoots.AddRange(contentRoots);
                    baseSvc?.OnEnable();
                }
            }

            LoadSessionStateKeysIfExists();
        }

        /// <summary>
        /// Should be called by parent <see cref="ScriptableObject"/> instance OnDisable method
        /// </summary>
        public void OnDisable()
        {
            Debug.Assert(IsInitialized);

            // ReSharper disable once DelegateSubtraction
            m_Settings.OnModification -= OnSettingsModification;
            m_Settings.profileSettings.UnregisterProfileStringEvaluationFunc(EvaluateGlobalProfileVariableKey);
            foreach (var svc in HostingServices)
            {
                svc.Logger = null;
                m_Settings.profileSettings.UnregisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
                (svc as BaseHostingService)?.OnDisable();
            }

            SaveSessionStateKeys();
        }

        internal void LoadSessionStateKeysIfExists()
        {
            int numKeys = SessionState.GetInt(k_GlobalProfileVariablesCountKey, 0);
            if (numKeys > 0)
                GlobalProfileVariables.Clear();

            for (int i = 0; i < numKeys; i++)
            {
                string profileVar = SessionState.GetString(GetSessionStateKey(i), string.Empty);
                if (!string.IsNullOrEmpty(profileVar))
                    GlobalProfileVariables.Add(GetPrivateIpAddressKey(i), profileVar);
            }
        }

        internal void SaveSessionStateKeys()
        {
            int prevNumKeys = SessionState.GetInt(k_GlobalProfileVariablesCountKey, 0);
            SessionState.SetInt(k_GlobalProfileVariablesCountKey, GlobalProfileVariables.Count);

            int profileVarIdx = 0;
            foreach (KeyValuePair<string, string> pair in GlobalProfileVariables)
            {
                SessionState.SetString(GetSessionStateKey(profileVarIdx), pair.Value);
                profileVarIdx++;
            }

            EraseSessionStateKeys(profileVarIdx, prevNumKeys);
        }

        internal static void EraseSessionStateKeys()
        {
            int numKeys = SessionState.GetInt(k_GlobalProfileVariablesCountKey, 0);
            EraseSessionStateKeys(0, numKeys);
            SessionState.EraseInt(k_GlobalProfileVariablesCountKey);
        }

        static void EraseSessionStateKeys(int min, int max)
        {
            for (int i = min; i < max; i++)
            {
                SessionState.EraseString(GetSessionStateKey(i));
            }
        }

        /// <summary> Ensure object is ready for serialization, and calls <see cref="IHostingService.OnBeforeSerialize"/> methods
        /// on all managed <see cref="IHostingService"/> instances
        /// </summary>
        public void OnBeforeSerialize()
        {
            // https://docs.unity3d.com/ScriptReference/EditorWindow.OnInspectorUpdate.html
            // Because the manager is a serialized field in the Addressables settings, this method is called
            // at 10 frames per second when the settings are opened in the inspector...
            // Be careful what you put in there...

            m_HostingServiceInfos.Clear();
            foreach (var svc in HostingServices)
            {
                var info = m_HostingServiceInfoMap[svc];
                m_HostingServiceInfos.Add(info);
                svc.OnBeforeSerialize(info.dataStore);
            }

            m_RegisteredServiceTypeRefs.Clear();
            foreach (var type in m_RegisteredServiceTypes)
                m_RegisteredServiceTypeRefs.Add(TypeToClassRef(type));
        }

        /// <summary> Ensure object is ready for serialization, and calls <see cref="IHostingService.OnBeforeSerialize"/> methods
        /// on all managed <see cref="IHostingService"/> instances
        /// </summary>
        public void OnAfterDeserialize()
        {
            m_HostingServiceInfoMap = new Dictionary<IHostingService, HostingServiceInfo>();
            foreach (var svcInfo in m_HostingServiceInfos)
            {
                IHostingService svc = CreateHostingServiceInstance(svcInfo.classRef);

                if (svc == null) continue;
                svc.OnAfterDeserialize(svcInfo.dataStore);
                m_HostingServiceInfoMap.Add(svc, svcInfo);
            }

            m_RegisteredServiceTypes = new List<Type>();
            foreach (var typeRef in m_RegisteredServiceTypeRefs)
            {
                var type = Type.GetType(typeRef, false);
                if (type == null) continue;
                m_RegisteredServiceTypes.Add(type);
            }
        }

        /// <summary>
        /// Refresh values in the global profile variables table.
        /// </summary>
        public void RefreshGlobalProfileVariables()
        {
            var vars = GlobalProfileVariables;
            vars.Clear();

            var ipAddressList = FilterValidIPAddresses(NetworkInterface.GetAllNetworkInterfaces()
                .Where(n => n.NetworkInterfaceType != NetworkInterfaceType.Loopback && n.OperationalStatus == OperationalStatus.Up)
                .SelectMany(n => n.GetIPProperties().UnicastAddresses)
                .Where(a => a.Address.AddressFamily == AddressFamily.InterNetwork)
                .Select(a => a.Address).ToList());

            if (ipAddressList.Count > 0)
            {
                vars.Add(KPrivateIpAddressKey, ipAddressList[0].ToString());

                if (ipAddressList.Count > 1)
                {
                    for (var i = 1; i < ipAddressList.Count; i++)
                        vars.Add(KPrivateIpAddressKey + "_" + i, ipAddressList[i].ToString());
                }
            }
        }

        // Internal for unit tests
        internal string EvaluateGlobalProfileVariableKey(string key)
        {
            string retVal;
            GlobalProfileVariables.TryGetValue(key, out retVal);
            return retVal;
        }

        void OnSettingsModification(AddressableAssetSettings s, AddressableAssetSettings.ModificationEvent evt, object obj)
        {
            switch (evt)
            {
                case AddressableAssetSettings.ModificationEvent.GroupAdded:
                case AddressableAssetSettings.ModificationEvent.GroupRemoved:
                case AddressableAssetSettings.ModificationEvent.GroupSchemaAdded:
                case AddressableAssetSettings.ModificationEvent.GroupSchemaRemoved:
                case AddressableAssetSettings.ModificationEvent.GroupSchemaModified:
                case AddressableAssetSettings.ModificationEvent.ActiveProfileSet:
                case AddressableAssetSettings.ModificationEvent.BuildSettingsChanged:
                case AddressableAssetSettings.ModificationEvent.ProfileModified:
                    var profileRemoteBuildPath = m_Settings.profileSettings.GetValueByName(m_Settings.activeProfileId, AddressableAssetSettings.kRemoteBuildPath);
                    if (profileRemoteBuildPath != null && (profileRemoteBuildPath.Contains('[') || !CurrentContentRootsContain(profileRemoteBuildPath)))
                        ConfigureAllHostingServices();
                    break;
                case AddressableAssetSettings.ModificationEvent.BatchModification:
                    ConfigureAllHostingServices();
                    break;
            }
        }

        bool CurrentContentRootsContain(string root)
        {
            foreach (var svc in HostingServices)
            {
                if (!svc.HostingServiceContentRoots.Contains(root))
                    return false;
            }

            return true;
        }

        void ConfigureAllHostingServices()
        {
            if (HostingServices.Count > 0)
            {
                var contentRoots = GetAllContentRoots();

                foreach (var svc in HostingServices)
                {
                    svc.HostingServiceContentRoots.Clear();
                    svc.HostingServiceContentRoots.AddRange(contentRoots);
                }
            }
        }

        string[] GetAllContentRoots()
        {
            Debug.Assert(IsInitialized);

            var contentRoots = new List<string>();
            foreach (var group in m_Settings.groups)
            {
                if (group != null)
                {
                    foreach (var schema in group.Schemas)
                    {
                        var configProvider = schema as IHostingServiceConfigurationProvider;
                        if (configProvider != null)
                        {
                            var groupRoot = configProvider.HostingServicesContentRoot;
                            if (groupRoot != null && !contentRoots.Contains(groupRoot))
                                contentRoots.Add(groupRoot);
                        }
                    }
                }
            }

            return contentRoots.ToArray();
        }

        IHostingService CreateHostingServiceInstance(string classRef)
        {
            try
            {
                var objType = Type.GetType(classRef, true);
                var svc = (IHostingService)Activator.CreateInstance(objType);
                return svc;
            }
            catch (Exception e)
            {
                m_Logger.LogFormat(LogType.Error, "Could not create IHostingService from class ref '{0}'", classRef);
                m_Logger.LogFormat(LogType.Error, e.Message);
            }

            return null;
        }

        static string TypeToClassRef(Type t)
        {
            return string.Format("{0}, {1}", t.FullName, t.Assembly.GetName().Name);
        }

        // For unit tests
        internal AddressableAssetSettings Settings
        {
            get { return m_Settings; }
        }

        private List<IPAddress> FilterValidIPAddresses(List<IPAddress> ipAddresses)
        {
            List<IPAddress> validIpList = new List<IPAddress>();
            if (PingTimeoutInMilliseconds < 0)
            {
                m_Logger.LogFormat(LogType.Error, "Cannot filter IP addresses. Timeout must be a non-negative integer.");
                return validIpList;
            }

            foreach (IPAddress address in ipAddresses)
            {
                var sender = new System.Net.NetworkInformation.Ping();
                var reply = sender.Send(address.ToString(), PingTimeoutInMilliseconds);
                if (reply.Status == IPStatus.Success)
                {
                    validIpList.Add(address);
                }
            }

            return validIpList;
        }
    }
}