using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Analytics;

namespace UnityEditor.Experimental.TerrainAPI
{
    /// <summary>
    /// Analytics class for collecting and sending aggregated user data
    /// </summary>
    public static class TerrainToolsAnalytics
    {
        //Event Data
        static bool s_EventRegistered = false;
        const int k_MaxEventsPerHour = 1000;
        const int k_MaxNumberOfElements = 1000;
        const string k_VendorKey = "unity.terraintools";
        const string k_EventName = "analytics.uTerrainTools";

        //Brush Analytics Data
        const float k_SignficantThreshold = .01f;

        static BrushAnalyticsData m_Data;
        static List<BrushParameterData> s_ModifiedBrushParameters = new List<BrushParameterData>();
        static List<string> s_UsedBrushShortcut = new List<string>();
        static float s_PaintingDuration;
        static bool s_ParameterChanged;

        [Serializable]
        struct BrushParameterData
        {
            public string name;
            public string value;
        }

        struct BrushAnalyticsData
        {
            public string brush_name;
            public List<string> shortcuts;
            public string[] mask_filters;
            public float strength;
            public float size;
            public float rotation;
            public float spacing;
            public float scatter;
            public float duration;
            public List<BrushParameterData> brush_parameters;
        }

        /// <summary>
        /// Array of BrushParameters used to compare against the most recent brush parameters to check if there
        /// has been a change.
        /// </summary>
        internal static IBrushParameter[] m_OriginalParameters;

        /// <summary>
        /// Interface for iterating over brush parameters of an ambiguous type
        /// </summary>
        public interface IBrushParameter
        {
            System.Type ParameterType();
            string Name { get; set; }
        }

        /// <summary>
        /// Struct containing the name and value associated to an individual brush parameter.
        /// </summary>
        /// <typeparam name="T">The variable type of the brush parameter</typeparam>
        internal struct BrushParameter<T> : IBrushParameter
        {
            public System.Type ParameterType()
            {
                return Value.GetType();
            }
            public T Value { get; set; }
            public string Name { get; set; }
        }

        /// <summary>
        /// Register the AnalyticsEvent for sending data to BigQuery
        /// </summary>
        /// <returns>EventRegisterd boolean identifying if the event was registered correctly</returns>
        static bool EnableAnalytics()
        {
            //Early out if the event has already been registered, returning bool determining 
            //if Editor Analytics are enabled
            if (s_EventRegistered)
                return EditorAnalytics.enabled;

            AnalyticsResult result = EditorAnalytics.RegisterEventWithLimit(k_EventName, k_MaxEventsPerHour,
                k_MaxNumberOfElements, k_VendorKey);

            if (result == AnalyticsResult.Ok)
                s_EventRegistered = true;

            return s_EventRegistered && EditorAnalytics.enabled;
        }

        /// <summary>
        /// Update the analytics data to be sent when a user starts painting with new parameters/settings
        /// The users time is being tracked while painting
        /// Once the user changes any brush parameters original data is sent and the new data is cached to be compared later
        /// </summary>
        /// <param name="baseBrushSettings">Brush Base class containing common brush parameters</param>
        /// <param name="brushParamFunc">Function returning brush specific parameters</param>
        internal static void UpdateAnalytics(BaseBrushUIGroup baseBrushSettings, Func<TerrainToolsAnalytics.IBrushParameter[]> brushParamFunc)
        {
            if (!EnableAnalytics())
                return;

            s_PaintingDuration += Time.deltaTime;

            if (!s_ParameterChanged)
                return;

            SendAnalytics();
            CompareBrushSettings(brushParamFunc?.Invoke());
            CacheAnalyticsData(baseBrushSettings);
        }

        /// <summary>
        /// Send the EditorAnalytics event and clear data for reuse
        /// </summary>
        static void SendAnalytics()
        {
            if(m_Data.Equals(default(BrushAnalyticsData))) 
                return;

            m_Data.duration = s_PaintingDuration;
            EditorAnalytics.SendEventWithLimit(k_EventName, m_Data);

            //Clear data
            s_ModifiedBrushParameters.Clear();
            s_UsedBrushShortcut.Clear();
            s_ParameterChanged = false;
            s_PaintingDuration = 0;
        }

        /// <summary>
        /// Checks whether the brush parameters have changed between
        /// the original and current state of a brush.
        /// </summary>
        /// <param name="parameters">Array of brushparameter structs which identify the name and value of the brushes
        /// parameters. </param>
        static void CompareBrushSettings(IBrushParameter[] parameters)
        {
            if (parameters == null)
                return;

            for (int i = 0; (i < parameters.Length && i < m_OriginalParameters.Length); i++)
            {
                if (parameters[i].Equals(m_OriginalParameters[i]))
                    continue;

                System.Type type = parameters[i].ParameterType();
                TypeCode typecode = Type.GetTypeCode(type);
                switch (typecode)
                {
                    case TypeCode.Int32:
                    case TypeCode.Int64:
                        {
                            int currentValue = ((BrushParameter<int>)parameters[i]).Value;
                            int originalValue = ((BrushParameter<int>)m_OriginalParameters[i]).Value;
                            CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
                            break;
                        }

                    case TypeCode.Single:
                        {
                            float currentValue = ((BrushParameter<float>)parameters[i]).Value;
                            float originalValue = ((BrushParameter<float>)m_OriginalParameters[i]).Value;

                            //Check if the user made a significant enough change to the parameter
                            if (CompareSignificance(currentValue, originalValue) == originalValue)
                            {
                                break;
                            }
                            CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
                            break;
                        }

                    case TypeCode.Boolean:
                        {
                            bool currentValue = ((BrushParameter<bool>)parameters[i]).Value;
                            bool originalValue = ((BrushParameter<bool>)m_OriginalParameters[i]).Value;
                            CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
                            break;
                        }

                    case TypeCode.String:
                        {
                            string currentValue = ((BrushParameter<string>)parameters[i]).Value;
                            string originalValue = ((BrushParameter<string>)m_OriginalParameters[i]).Value;
                            CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
                            break;
                        }
                    case TypeCode.Object:
                        {
                            if (type == typeof(Vector3))
                            {
                                Vector3 currentValue = ((BrushParameter<Vector3>)parameters[i]).Value;
                                Vector3 originalValue = ((BrushParameter<Vector3>)m_OriginalParameters[i]).Value;
                                CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
                            }
                            else if (type == typeof(Vector4))
                            {
                                Vector4 currentValue = ((BrushParameter<Vector4>)parameters[i]).Value;
                                Vector4 originalValue = ((BrushParameter<Vector4>)m_OriginalParameters[i]).Value;
                                CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
                            }
                            else if (type == typeof(Keyframe[]))
                            {
                                Keyframe[] currentValue = ((BrushParameter<Keyframe[]>)parameters[i]).Value;
                                Keyframe[] originalValue = ((BrushParameter<Keyframe[]>)m_OriginalParameters[i]).Value;
                                for (int k = 0; k < currentValue.Length; k++)
                                {
                                    //Check if there's a change between the original and current keyframe values
                                    //Cache the change if there's a difference
                                    if(!currentValue[k].Equals(originalValue[k]))
                                    {
                                        CacheChangedParamter($"{parameters[i].Name}", currentValue[k], originalValue[k]);
                                        break;
                                    }
                                }
                            }
                            break;
                        }
                    default:
                        Debug.LogWarning($"The parameter of type {type} isn't able to be tracked by Analytics");
                        break;
                }
            }

            m_OriginalParameters = parameters;
        }

        static void CacheAnalyticsData(BaseBrushUIGroup brush)
        {
            m_Data.brush_name = brush.brushName;
            m_Data.shortcuts = s_UsedBrushShortcut;
            m_Data.mask_filters = brush.brushMaskFilterStack.filters?.
                Where(x => x.enabled).
                Select(x => x.GetType().Name).ToArray();
            m_Data.strength = CompareSignificance(brush.brushStrength, m_Data.strength);
            m_Data.size = CompareSignificance(brush.brushSize, m_Data.size);
            m_Data.rotation = CompareSignificance(brush.brushRotation, m_Data.rotation);
            m_Data.spacing = CompareSignificance(brush.brushSpacing, m_Data.spacing);
            m_Data.scatter = CompareSignificance(brush.brushScatter, m_Data.scatter);
            m_Data.brush_parameters = s_ModifiedBrushParameters;
        }

        #region Helper Methods
        /// <summary>
        /// Caches the shortcutId on keyRelease to be sent as analytics data if the 
        /// shortcut key hasn't been cached already
        /// </summary>
        /// <param name="shortcutId">ID of the shortcut </param>
        internal static void OnShortcutKeyRelease(string shortcutId)
        {
            if(!s_UsedBrushShortcut.Contains(shortcutId))
                s_UsedBrushShortcut.Add(shortcutId);
        }

        /// <summary>
        /// Flags the parameter changed boolean notifying that the user has changed brush parameters
        /// and the old data needs to be sent while the new data needs to be cached.
        /// </summary>
        internal static void OnParameterChange() => s_ParameterChanged = true;

        /// <summary>
        /// Cache the parameter that has been changed to be sent as analytics data
        /// </summary>
        /// <param name="name">The name of the changed setting Ex: "Brush Strength"</param>
        /// <param name="currentSetting">Setting of the brush before the user starts painting</param>
        /// <param name="originalSetting">Setting of the brush which the user starts painting with</param>
        /// <returns>Returns a boolean indicating if the parameter was changed</returns>
        static void CacheChangedParamter<T>(string name, T currentSetting, T originalSetting)
        {
            s_ModifiedBrushParameters.Add(new BrushParameterData
            {
                name = name,
                value = currentSetting.ToString()
            });
        }

        /// <summary>
        /// Check whether the difference between the initial and current brush parameters
        /// is significant enough to send the data for analyzing. 
        /// </summary>
        /// <param name="currentValue">The latest brush parameter</param>
        /// <param name="originalValue">The starting brush parameter</param>
        /// <returns></returns>
        static float CompareSignificance(float currentValue, float originalValue)
        {
            //Determine if value A has significantly changed from value B
            if (Mathf.Abs(currentValue - originalValue) >= k_SignficantThreshold)
                return currentValue;
            else
                return originalValue;
        }

        #endregion
    }
}