<commit_before>﻿namespace Helios.Keen
{
    using System;
    using UnityEngine;
    using System.Reflection;
    using System.Collections;
    using System.Collections.Generic;

    public partial class Client : MonoBehaviour
    {
        /// <summary>
        /// Used to hold write-specific Keen.IO project settings.
        /// </summary>
        public class Config
        {
            /// <summary>
            /// can be found in https://keen.io/project/<xxx>
            /// where <xxx> is usually obtained via Keen dashboard.
            /// </summary>
            public string ProjectId;

            /// <summary>
            /// can be found in https://keen.io/project/<xxx>
            /// after you click on "Show API Keys"
            /// </summary>
            public string WriteKey;

            /// <summary>
            /// the callback which is called after every attempt
            /// to send an event to Keen.IO. (optional)
            /// </summary>
            public Action<CallbackData> EventCallback;

            /// <summary>
            /// the interval which "cache sweeping" performs
            /// unit is in seconds (2.0f => 2 seconds)
            /// </summary>
            public float CacheSweepInterval = 15.0f;

            /// <summary>
            /// the number of cache entries which will be cleared
            /// from the cache store on every interval
            /// </summary>
            public uint CacheSweepCount = 10;

            /// <summary>
            /// Optional cache provider
            /// </summary>
            public ICacheProvider CacheInstance;
        }

        /// <summary>
        /// Status of a "sent" event to Keen.
        /// Submitted: successfully sent to Keen.
        /// Cached: failed to be sent to Keen and cached in local DB.
        /// Failed: Permanently failed.
        /// </summary>
        public enum EventStatus
        {
            Submitted,
            Cached,
            Failed,
            None
        }

        /// <summary>
        /// Type passed to ClientSettings.EventCallback after
        /// every attempt to submit events to Keen.
        /// </summary>
        public class CallbackData
        {
            public EventStatus  status;
            public string       name;
            public string       data;
        }

        private bool                        m_Validated = false;
        private Config                      m_Settings  = null;
        private List<ICacheProvider.Entry>  m_Cached    = new List<ICacheProvider.Entry>();

        public Config Settings
        {
            get { return m_Settings; }
            set
            {
                StopAllCoroutines();
                m_Validated = false;
                m_Settings = value;

                if (Settings == null)
                    Debug.LogError("[Keen] Settings object is empty.");
                else if (String.IsNullOrEmpty(Settings.ProjectId))
                    Debug.LogError("[Keen] project ID is empty.");
                else if (String.IsNullOrEmpty(Settings.WriteKey))
                    Debug.LogError("[Keen] write key is empty.");
                else if (Settings.CacheSweepInterval <= 0.5f)
                    Debug.LogError("[Keen] cache sweep interval is invalid.");
                else m_Validated = true;

                if (Settings.CacheInstance != null &&
                    Settings.CacheInstance.Ready())
                    StartCoroutine(CacheRoutineCo());
            }
        }

        /// <summary>
        /// Serializes C# objects to a JSON string using reflection.
        /// NOTE: .NET internally caches reflection info. Do not double
        /// cache it here. ONLY .NET 1.0 does not have reflection cache
        /// NOTE: NO anonymous types!
        /// </summary>
        public string Serialize<T>(T obj)
        {
            // Take in objects of type struct or class only.
            if (obj == null || !(typeof(T).IsValueType || typeof(T).IsClass))
                return string.Empty;

            string json = "{";

            foreach (FieldInfo info in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance))
            {
                string key = string.Format("\"{0:s}\"", info.Name);
                string val = "\"error\"";

                // This covers all integral types (type double in json)
                if (info.FieldType.IsPrimitive)
                {
                    if (info.FieldType == typeof(bool))
                    {
                        val = info.GetValue(obj).ToString().ToLower();
                    }
                    else
                    {
                        val = string.Format("{0:g}", info.GetValue(obj));
                    }
                }
                // this covers Enums and casts it to an integer
                else if (info.FieldType.IsEnum)
                {
                    val = string.Format("{0:g}", (int)info.GetValue(obj));
                }
                // This handles classes and struct and recurses into Serialize again
                else if (info.FieldType != typeof(string) &&
                    (info.FieldType.IsClass || info.FieldType.IsValueType))
                {
                    // This invokes our Generic method with
                    // a dynamic type info during runtime.
                    val = GetType()
                        .GetMethod(MethodInfo.GetCurrentMethod().Name)
                        .MakeGenericMethod(info.FieldType)
                        .Invoke(this, new object[] { info.GetValue(obj) })
                        .ToString();
                }
                // Handle everything else as a string
                else
                {
                    object runtime_val = info.GetValue(obj);

                    if (runtime_val == null)
                        val = "null";
                    else
                        val = string.Format("\"{0}\"", runtime_val.ToString().Replace("\"", "\\\""));
                }

                string key_val = string.Format("{0}:{1},", key, val);
                json += key_val;
            }

            json = json.TrimEnd(',');
            json += "}";

            return json;
        }

        /// <summary>
        /// Convenience method for SendEvent(string, string)
        /// </summary>
        public void SendEvent<T>(string event_name, T event_data)
        {
            SendEvent(event_name, Serialize(event_data));
        }

        /// <summary>
        /// Sends JSON string to Keen IO
        /// </summary>
        public virtual void SendEvent(string event_name, string event_data)
        {
            if (!m_Validated)
                Debug.LogError("[Keen] Client is not validated.");
            else if (String.IsNullOrEmpty(event_name))
                Debug.LogError("[Keen] event name is empty.");
            else if (String.IsNullOrEmpty(event_data))
                Debug.LogError("[Keen] event data is empty.");
            else // run if all above tests passed
                StartCoroutine(SendEventCo(event_name, event_data, Settings.EventCallback));
        }

        /// <summary>
        /// Coroutine that concurrently attempts to send events to Keen.
        /// </summary>
        IEnumerator SendEventCo(string event_name, string event_data, Action<CallbackData> callback, EventStatus status = EventStatus.None)
        {
            if (!m_Validated)
                yield break;

            var headers = new Dictionary<string, string>();
            headers.Add("Authorization", Settings.WriteKey);
            headers.Add("Content-Type", "application/json");

            WWW keen_server = new WWW(string.Format("https://api.keen.io/3.0/projects/{0}/events/{1}"
                , Settings.ProjectId, event_name),
                System.Text.Encoding.ASCII.GetBytes(event_data), headers);

            yield return keen_server;

            if (!String.IsNullOrEmpty(keen_server.error))
            {
                Debug.LogErrorFormat("[Keen]: {0}", keen_server.error);

                if (status == EventStatus.None &&
                    Settings.CacheInstance != null &&
                    Settings.CacheInstance.Ready() &&
                    Settings.CacheInstance.Write(new ICacheProvider.Entry { name = event_name, data = event_data }))
                {
                    if (callback != null)<commit_msg>don't start caching when people assign a new config, can break SessionAwareClient<commit_after>﻿namespace Helios.Keen
{
    using System;
    using UnityEngine;
    using System.Reflection;
    using System.Collections;
    using System.Collections.Generic;

    public partial class Client : MonoBehaviour
    {
        /// <summary>
        /// Used to hold write-specific Keen.IO project settings.
        /// </summary>
        public class Config
        {
            /// <summary>
            /// can be found in https://keen.io/project/<xxx>
            /// where <xxx> is usually obtained via Keen dashboard.
            /// </summary>
            public string ProjectId;

            /// <summary>
            /// can be found in https://keen.io/project/<xxx>
            /// after you click on "Show API Keys"
            /// </summary>
            public string WriteKey;

            /// <summary>
            /// the callback which is called after every attempt
            /// to send an event to Keen.IO. (optional)
            /// </summary>
            public Action<CallbackData> EventCallback;

            /// <summary>
            /// the interval which "cache sweeping" performs
            /// unit is in seconds (2.0f => 2 seconds)
            /// </summary>
            public float CacheSweepInterval = 15.0f;

            /// <summary>
            /// the number of cache entries which will be cleared
            /// from the cache store on every interval
            /// </summary>
            public uint CacheSweepCount = 10;

            /// <summary>
            /// Optional cache provider
            /// </summary>
            public ICacheProvider CacheInstance;
        }

        /// <summary>
        /// Status of a "sent" event to Keen.
        /// Submitted: successfully sent to Keen.
        /// Cached: failed to be sent to Keen and cached in local DB.
        /// Failed: Permanently failed.
        /// </summary>
        public enum EventStatus
        {
            Submitted,
            Cached,
            Failed,
            None
        }

        /// <summary>
        /// Type passed to ClientSettings.EventCallback after
        /// every attempt to submit events to Keen.
        /// </summary>
        public class CallbackData
        {
            public EventStatus  status;
            public string       name;
            public string       data;
        }

        private bool                        m_Validated = false;
        private bool                        m_Caching   = false;
        private Config                      m_Settings  = null;
        private List<ICacheProvider.Entry>  m_Cached    = new List<ICacheProvider.Entry>();

        /// <summary>
        /// Instance settings. Use this to provide your Keen project settings.
        /// </summary>
        public Config Settings
        {
            get { return m_Settings; }
            set
            {
                StopAllCoroutines();
                m_Validated = false;
                m_Caching = false;
                m_Settings = value;

                if (Settings == null)
                    Debug.LogError("[Keen] Settings object is empty.");
                else if (String.IsNullOrEmpty(Settings.ProjectId))
                    Debug.LogError("[Keen] project ID is empty.");
                else if (String.IsNullOrEmpty(Settings.WriteKey))
                    Debug.LogError("[Keen] write key is empty.");
                else if (Settings.CacheSweepInterval <= 0.5f)
                    Debug.LogError("[Keen] cache sweep interval is invalid.");
                else m_Validated = true;
            }
        }

        /// <summary>
        /// Start caching routine. You rarely need to call this. SendEvent
        /// calls this when the first time you call SendEvent.
        /// </summary>
        public void StartCaching()
        {
            if (!m_Validated)
            {
                Debug.LogWarning("[Keen] instance is not validated.");
                return;
            }

            if (m_Caching)
            {
                Debug.LogWarning("[Keen] instance is already caching.");
                return;
            }

            if (Settings.CacheInstance != null &&
                Settings.CacheInstance.Ready())
                StartCoroutine(CacheRoutineCo());

            m_Caching = true;
        }

        /// <summary>
        /// Serializes C# objects to a JSON string using reflection.
        /// NOTE: .NET internally caches reflection info. Do not double
        /// cache it here. ONLY .NET 1.0 does not have reflection cache
        /// NOTE: NO anonymous types!
        /// </summary>
        public string Serialize<T>(T obj)
        {
            // Take in objects of type struct or class only.
            if (obj == null || !(typeof(T).IsValueType || typeof(T).IsClass))
                return string.Empty;

            string json = "{";

            foreach (FieldInfo info in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance))
            {
                string key = string.Format("\"{0:s}\"", info.Name);
                string val = "\"error\"";

                // This covers all integral types (type double in json)
                if (info.FieldType.IsPrimitive)
                {
                    if (info.FieldType == typeof(bool))
                    {
                        val = info.GetValue(obj).ToString().ToLower();
                    }
                    else
                    {
                        val = string.Format("{0:g}", info.GetValue(obj));
                    }
                }
                // this covers Enums and casts it to an integer
                else if (info.FieldType.IsEnum)
                {
                    val = string.Format("{0:g}", (int)info.GetValue(obj));
                }
                // This handles classes and struct and recurses into Serialize again
                else if (info.FieldType != typeof(string) &&
                    (info.FieldType.IsClass || info.FieldType.IsValueType))
                {
                    // This invokes our Generic method with
                    // a dynamic type info during runtime.
                    val = GetType()
                        .GetMethod(MethodInfo.GetCurrentMethod().Name)
                        .MakeGenericMethod(info.FieldType)
                        .Invoke(this, new object[] { info.GetValue(obj) })
                        .ToString();
                }
                // Handle everything else as a string
                else
                {
                    object runtime_val = info.GetValue(obj);

                    if (runtime_val == null)
                        val = "null";
                    else
                        val = string.Format("\"{0}\"", runtime_val.ToString().Replace("\"", "\\\""));
                }

                string key_val = string.Format("{0}:{1},", key, val);
                json += key_val;
            }

            json = json.TrimEnd(',');
            json += "}";

            return json;
        }

        /// <summary>
        /// Convenience method for SendEvent(string, string)
        /// </summary>
        public void SendEvent<T>(string event_name, T event_data)
        {
            SendEvent(event_name, Serialize(event_data));
        }

        /// <summary>
        /// Sends JSON string to Keen IO
        /// </summary>
        public virtual void SendEvent(string event_name, string event_data)
        {
            if (!m_Validated)
                Debug.LogError("[Keen] Client is not validated.");
            else if (String.IsNullOrEmpty(event_name))
                Debug.LogError("[Keen] event name is empty.");
            else if (String.IsNullOrEmpty(event_data))
                Debug.LogError("[Keen] event data is empty.");
            else // run if all above tests passed
                StartCoroutine(SendEventCo(event_name, event_data, Settings.EventCallback));

            if (!m_Caching)
                StartCaching();
        }

        /// <summary>
        /// Coroutine that concurrently attempts to send events to Keen.
        /// </summary>
        IEnumerator SendEventCo(string event_name, string event_data, Action<CallbackData> callback, EventStatus status = EventStatus.None)
        {
            if (!m_Validated)
                yield break;

            var headers = new Dictionary<string, string>();
            headers.Add("Authorization", Settings.WriteKey);
            headers.Add("Content-Type", "application/json");

            WWW keen_server = new WWW(string.Format("https://api.keen.io/3.0/projects/{0}/events/{1}"
                , Settings.ProjectId, event_name),
                System.Text.Encoding.ASCII.GetBytes(event_data), headers);

            yield return keen_server;

            if (!String.IsNullOrEmpty(keen_server.error))
            {
                Debug.LogErrorFormat("[Keen]: {0}", keen_server.error);

                if (status == EventStatus.None &&
                    Settings.CacheInstance != null &&
                    Settings.CacheInstance.Ready() &&
                    Settings.CacheInstance.Write(new ICacheProvider.Entry { name = event_name, data = event_data }))
                {
                    if (callback != null)
{'fraction_non_alphanumeric': 0.10511252034345361, 'fraction_numerical': 0.0025815141141478197, 'mean_word_length': 1.3082901554404145, 'pattern_counts': {'":': 0, '<': 81, '<?xml version=': 0, '>': 81, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3854998', 'n_tokens_mistral': 4422, 'n_tokens_neox': 4198, 'n_words': 1199}