﻿#if UNITY_3_1 || UNITY_3_2 || UNITY_3_3 || UNITY_3_4 || UNITY_3_5
#define UNITY_3
#endif

#if UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_5 || UNITY_4_6
#define UNITY_4
#endif

// Use hashtables for lower Unity versions.
#if UNITY_3 || (UNITY_4 && !UNITY_4_6)
#define USE_HASHTABLE
#endif

// Use Playtomic.
#define IDNET_PLAYTOMIC

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
#if !UNITY_EDITOR && UNITY_WEBGL
using System.Runtime.InteropServices;
#endif


/// <summary>
///     API for Idnet.
///     - Unity 4.3.0 minimum support.
/// </summary>
/// <sdk version="3.0.9">
///     - Version : v-3.0.9
///     - Package Name : IDNET-Unity3d_V3.1.0
/// </sdk>
/// <remarks>
///     --- SUGGESTIONS / FEATURE IDEAS
///     - Version number should apply to the entire API.
///     - Clean up exceptions thrown by server.
///     - Preferably use a unified (e.g. Enum ErrorCode) system. Playtomics is on the way towards that.
///     - User validation Gui to let users know that, for example, their password is too short.
/// </remarks>
public class Idnet : MonoBehaviour
{
#if !UNITY_EDITOR && UNITY_WEBGL
    [DllImport("__Internal")]
    private static extern void IdnetAutoLogin(string _appId, string _autologinUrl);
    [DllImport("__Internal")]
    private static extern void IdnetSetSessionCookie(string _appId, string _autologinUrl, string _sessionCookie);
    [DllImport("__Internal")]
    private static extern void IdnetInitExternEval(string _appId);
    [DllImport("__Internal")]
    private static extern void IdnetGetY8ToIDnetBannerStatus();
    [DllImport("__Internal")]
    private static extern void IdnetGetUserAgent();
    [DllImport("__Internal")]
    private static extern void IdnetGetRefererDomain();
    [DllImport("__Internal")]
    private static extern void IdnetGetOSDetails();
    [DllImport("__Internal")]
    private static extern void IdnetSilentSponserLogin();
    [DllImport("__Internal")]
    private static extern void IdnetY8ExternEval(string _allowGamePause);
    [DllImport("__Internal")]
    private static extern void IdnetLogs(string _log);
    [DllImport("__Internal")]
    private static extern void ShowAdsPlaceholder();
#endif

    /// <summary>
    ///     Key stored on the DB that authenticates requests. Unique to each game.
    ///     Do not share this value.
    ///     EG: a5fe2ffcea26c67d8f78c92b5f02f205
    ///     Value set in inspector.
    /// </summary>
    public static string AppId;

    /// <summary>
    ///     Unity sdk version
    /// </summary>
    public static string SdkVersion = "3.1.0";

    /// <summary>
    ///     PlayerPrefs key for all Idnet variables.
    /// </summary>
    public const string ApiPrefsKey = "IdNetApi.";

    public const string SessionPrefsKey = ApiPrefsKey + "SessionToken";
    public const string SessionCookiePrefsKey = ApiPrefsKey + "SessionCookie";
    public const string NicknamePrefsKey = ApiPrefsKey + "Nickname";
    public const string EmailPrefsKey = ApiPrefsKey + "Email";
    public const string PidPrefsKey = ApiPrefsKey + "Pid";
    public const string BestScorePrefsKey = ApiPrefsKey + "BestScore";
    public const string CurrentScorePrefsKey = ApiPrefsKey + "CurrentScore";
    public const string TrackerPrefsKey = ApiPrefsKey + "TrackerUUID";
    public const string GamenamePrefsKey = ApiPrefsKey + "GameName";
    public const string IdnetPointsPrefsKey = ApiPrefsKey + "IdnetPoints";

    /// <summary>
    ///     Server API version we are running. Changes rarely.
    ///     EG: 1
    /// </summary>
    public const string ApiVersion = "1";

    /// <summary>
    ///     Base url for all IdNet operations.
    /// </summary>
    private const string BaseApiUrl = "https://account.y8.com/api/";

    /// <summary>
    ///     Workaround: Specific url for posting scores.
    /// </summary>
    /// <remarks>
    ///     Should include a version number.
    ///     Should be based off a global object scope url.
    /// </remarks>
    private const string PostHighscoreApiUrl = BaseApiUrl;

    /// <summary>
    ///     Base url for user data.
    /// </summary>
    private const string UserDataUrl = BaseApiUrl + "user_data/";

    /// <summary>
    ///     Workaround: Specific url for getting leaderbords.
    /// </summary>
    /// <remarks>
    ///     Should include a version number.
    ///     Should be based off a global object scope url.
    /// </remarks>
    private const string LeaderboardGetUrl = BaseApiUrl + "v" + ApiVersion + "/" + "json/leaderboard?client_id=";

    /// <summary>
    ///     Url for Protection api.(Blacklisted and Sponsered domains)
    /// </summary>
    public const string ProtectionGetUrl = BaseApiUrl + "v" + ApiVersion + "/" + "json/protection-lists";

    /// <summary>
    ///     Url for getting IDnet points.
    /// </summary>
    public const string IdnetPointsGetUrl = BaseApiUrl + "v" + ApiVersion + "/" + "json/points/total";

    /// <summary>
    ///     Url for getting global time.
    /// </summary>
    public const string UnixTime = BaseApiUrl + "v" + ApiVersion + "/" + "json/time/server/unix";

    /// <summary>
    ///     Y8 Account client options.
    /// </summary>
    public const string ClientOptionsUrl = BaseApiUrl + "v" + ApiVersion + "/" + "json/client_options";

    /// <summary>
    ///     Base url for IdNet tracking operations.
    /// </summary>
    public const string TrackerApiUrl = "https://t.y8.com/log";

    /// <summary>
    ///     How long for AutoLogin to timeout.
    /// </summary>
    private const float AutoLoginTimeoutSeconds = 5;

#if IDNET_PLAYTOMIC
    public const string PlayomicHostUrl = "https://playtomic.y8.com/v1";
#endif

    public const int HighscoresPerLeaderboardPage = 10;

    /// <summary>
    ///     Static reference to the Gui.
    /// </summary>
    public IdnetGui Gui;

#if USE_HASHTABLE
     public static  Hashtable Headers = new Hashtable();
     public static  Hashtable PlaytomicHeaders = new Hashtable();
#else
    public static readonly Dictionary<string, string> Headers = new Dictionary<string, string>();
    public static readonly Dictionary<string, string> PlaytomicHeaders = new Dictionary<string, string>();
#endif

    /// <summary>
    ///     NOT ZERO BASED.
    ///     Page 1 to n.
    /// </summary>
    public int LeaderboardPage { get; set; }

    /// <summary>
    ///     Leaderboard Table Name. Example "Level1 Leaderboard" ,"Level2 Leaderboard".
    ///     Used in case of Multiple leaderboards,opens up a particular Leaderboard window.
    /// </summary>
    public string LeaderboardTable { get; set; }

    /// <summary>
    ///     (Default)  ReverseLeaderboard=false : Leaderboard scores in decreasing order(High to Low scores).
    ///     ReverseLeaderboard=true : Leaderboard scores in increasing order(Low to High scores).
    /// </summary>
    public bool ReverseLeaderboard { get; set; }

    /// <summary>
    ///     If TimerLeaderboard = true : Instead of scores,you will see Time in leaderboard with format min:sec:millisec.(Low
    ///     to High).
    ///     Make sure you pass "Milliseconds" to leaderboard's PostHighscore api and NOT "seconds" or "minutes" or "hours".
    ///     (Default) TimerLeaderboard = false : You will see scores in leaderboard window.
    /// </summary>
    public bool TimerLeaderboard { get; set; }

    /// <summary>
    ///     IsLogoClickable bool is false,if game is running on our websites(like y8.com,id.net,pog.com etc.).
    /// </summary>
    [HideInInspector] public bool IsLogoClickable = true;

    /// <summary>
    ///     isBlacklisted bool is true,if website on which the game is running,is marked as "Blacklisted" by id.net.
    /// </summary>
    [HideInInspector] public bool IsBlacklisted;

    /// <summary>
    ///     isSponsor bool is true,if website on running on y8 networks,like y8.com,id.net,pog.com etc.
    ///     If "isSponsor" variable is true,you should disale Y8's "MoreGames" button,and disable clickable-links of y8 and
    ///     idnet.
    /// </summary>
    [HideInInspector] public bool IsSponsor;

    /// <summary>
    ///     List of y8 sponsered domains,like y8.com,id.net,pog.com etc.
    /// </summary>
    [HideInInspector] public List<string> ApprovedDomainsList = new List<string>();

    /// <summary>
    ///     List of blacklisted domains,marked by id.net.
    /// </summary>
    [HideInInspector] public List<string> BlacklistedUrlsList = new List<string>();

    /// <summary>
    ///     Number of Api calls made in last 5 minutes.
    /// </summary>
    public static int ApiCallsCounter;

    /// <summary>
    ///     List containing time when api call are made in last 5 minutes
    /// </summary>
    private readonly List<float> _apiCountTimerList = new List<float>();

    /// <summary>
    ///     Game Mode,either Online Save or Local Save.
    /// </summary>
    public enum GameMode
    {
        OnlineSave,
        LocalSave
    }

    /// <summary>
    ///     Get the current game Mode,either Online Save or Local Save.
    /// </summary>
    /// <value>The current game mode</value>
    [HideInInspector] public GameMode CurrentMode;

    /// <summary>
    ///     Guest achievements id's in local storage dictionary,so they can be unlocked when user registers to idnet.
    /// </summary>
    [HideInInspector] public Dictionary<string, string> GuestAchievements = new Dictionary<string, string>();

    public LeaderboardMode LeaderboardModeValue { get; set; }

    /// <summary>
    ///     True = Create duplicates.
    ///     False = Override or reject scores if they're not the best.
    /// </summary>
    public bool AllowDuplicateLeaderboardResults { get; set; }

    /// <summary>
    ///     True = AutoLogin window is shown while AutoLogin happens.
    ///     False = AutoLogin happens in the background.
    /// </summary>
    public bool DisplayAutoLoginWindow { get; set; }

    /// <summary>
    ///     Called by <see cref="CloseLoginRegisterGui" />
    ///     which is called by the user.
    /// </summary>
    public Action<User, Exception> ClosedLoginRegister;

    /// <summary>
    ///     AsyncCache of the exception caused by the LoginRegister function.
    ///     Called by <see cref="CloseLoginRegisterGui" />.
    /// </summary>
    public Exception LoginRegisterException;

    /// <summary>
    ///     Called by <see cref="CloseProfileGui" />
    ///     which is called by the user.
    /// </summary>
    public Action<Exception> ClosedProfile;

    /// <summary>
    ///     AsyncCache of the exception caused by the Profile function.
    ///     Called by <see cref="CloseProfileGui" />.
    /// </summary>
    public Exception ProfileException;

    /// <summary>
    ///     Called by <see cref="CloseLeaderboardGui" />
    ///     which is called by the user.
    /// </summary>
    public Action<List<Highscore>, Exception> ClosedLeaderboard;

    /// <summary>
    ///     AsyncCache of the exception caused by the Leaderboard function.
    ///     Called by <see cref="CloseLeaderboardGui" />.
    /// </summary>
    public Exception LeaderboardException;

    /// <summary>
    ///     Called by <see cref="CloseAutoLoginGui" />
    ///     which is called by the user.
    /// </summary>
    public Action<User, Exception> ClosedAutoLogin;

    /// <summary>
    ///     AsyncCache of the exception caused by the AutoLogin function.
    ///     Called by <see cref="CloseAutoLoginGui" />.
    /// </summary>
    public Exception AutoLoginException;

    /// <summary>
    ///     Called by <see cref="CloseListAchievementsGui" />
    ///     which is called by the user.
    /// </summary>
    public Action<Exception> ClosedListAchievements;

    /// <summary>
    ///     AsyncCache of the exception caused by the <see cref="ListAchievements" /> function.
    ///     Called by <see cref="CloseListAchievementsGui" />.
    /// </summary>
    public Exception ListAchievementsException;

    /// <summary>
    ///     Called by <see cref="CloseUserLevelsGui" />
    ///     which is called by the user.
    /// </summary>
    public Action<Exception> ClosedUserLevels;

    /// <summary>
    ///     AsyncCache of the exception caused by the UserLevels function.
    ///     Called by <see cref="CloseUserLevelsGui" />.
    /// </summary>
    public Exception UserLevelsException;

    /// <summary>
    ///     Leaderboard data. Will be empty until <see cref="Leaderboard" /> is called.
    /// </summary>
    [HideInInspector] public List<Highscore> Highscores = new List<Highscore>();

    /// <summary>
    ///     Leaderboard data. Will be empty until <see cref="Leaderboard" /> is called.
    /// </summary>
    [HideInInspector] public List<Highscore> BestScoreList = new List<Highscore>();

    /// <summary>
    ///     User map data. Will be empty until <see cref="ListLevels" /> is called.
    /// </summary>
    [HideInInspector] public List<PlayerLevel> UserLevels = new List<PlayerLevel>();

    /// <summary>
    ///     Generated C# using: http://json2csharp.com/
    /// </summary>
    // ReSharper disable once ClassNeverInstantiated.Local
    public class LeaderboardJsonRoot
    {
        public int Score;
        public int UpdatedAt;
        public LeaderboardJsonUser User;
    }

    /// <summary>
    ///     Generated C# using: http://json2csharp.com/
    /// </summary>
    // ReSharper disable once ClassNeverInstantiated.Local
    public class LeaderboardJsonUser
    {
        public string Id;
        public string Nickname;
    }

    [Obsolete("Use Idnet.User.Current instead.")]
    public User CurrentUser
    {
        get { return User.Current; }
    }

    /// <summary>
    ///     Tracker UUID.
    /// </summary>
    public string TrackerUuid
    {
        set { PlayerPrefs.SetString(TrackerPrefsKey + AppId, value); }
        get { return PlayerPrefs.GetString(TrackerPrefsKey + AppId, ""); }
    }

    /// <summary>
    ///     Name of your Game.
    /// </summary>
    public string GameName { get; set; }

    /// <summary>
    ///     Current Year.
    /// </summary>
    public int CurrentYear
    {
        set { PlayerPrefs.SetInt(GamenamePrefsKey + AppId, value); }
        get { return PlayerPrefs.GetInt(GamenamePrefsKey + AppId, System.DateTime.UtcNow.Year); }
    }

    [HideInInspector] public bool EnableY8ToIDnetBanner = true;

    [HideInInspector] public bool ShowRealAdvert = true;

    [HideInInspector] public bool EnableLocalhostTest = false;

    /// <summary>
    ///     Months List(Used when registering to Y8Account).
    /// </summary>
    [HideInInspector] public List<string> MonthsList = new List<string>(12);

    /// <summary>
    ///     Last 100 Years List(Used when registering to Y8Account).
    /// </summary>
    [HideInInspector] public List<string> YearList = new List<string>(100);

    /// <summary>
    ///     Idnet User.
    /// </summary>
    [Serializable]
    public class User
    {
        /// <summary>
        ///     AsyncCache of current user.
        ///     No session key if logged out.
        ///     Do not set manually.
        /// </summary>
        public static User Current;

        /// <summary>
        ///     Achievement data. Will be empty until <see cref="Idnet.ListAchievements" /> is called.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore] public List<PlayerAchievement> Achievements = new List<PlayerAchievement>();

        /// <summary>
        ///     Preferred, social, human-readable name of the user.
        /// </summary>
        [Newtonsoft.Json.JsonProperty("nickname")]
        public string Nickname { get; set; }

        /// <summary>
        ///     Users email address.
        /// </summary>
        [Newtonsoft.Json.JsonProperty("email")]
        public string Email { get; set; }

        /// <summary>
        ///     id.net server id.
        /// </summary>
        [Newtonsoft.Json.JsonProperty("pid")]
        public string Pid { get; set; }

        /// <summary>
        ///     Sensitive login token for a player.
        ///     Empty if no user.
        /// </summary>
        public string SessionToken { get; set; }

        /// <summary>
        ///     Sensitive login Cookie for a player.
        ///     Empty if no user.
        /// </summary>
        public string SessionCookie { get; set; }

        /// <summary>
        ///     Gender of this player.
        ///     Empty if no user.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public string Gender { get; set; }

        /// <summary>
        ///     Firstname of this player.
        ///     Empty if no user.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public string Firstname { get; set; }

        /// <summary>
        ///     Birth year of this player.
        ///     Empty if no user.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public string BirthYear { get; set; }

        /// <summary>
        ///     Birth month of this player.
        ///     Empty if no user.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public string BirthMonth { get; set; }

        /// <summary>
        ///     User's Best score in Idnet leaderboard.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public int BestScore
        {
            set { PlayerPrefs.SetInt(BestScorePrefsKey + AppId, value); }
            get { return PlayerPrefs.GetInt(BestScorePrefsKey + AppId, 0); }
        }

        /// <summary>
        ///     User's Recent score.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public int CurrentScore
        {
            set { PlayerPrefs.SetInt(CurrentScorePrefsKey + AppId, value); }
            get { return PlayerPrefs.GetInt(CurrentScorePrefsKey + AppId, 0); }
        }

        /// <summary>
        ///     User's IDnet points.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public int IdnetPoints
        {
            set { PlayerPrefs.SetInt(IdnetPointsPrefsKey + AppId, value); }
            get { return PlayerPrefs.GetInt(IdnetPointsPrefsKey + AppId, 0); }
        }

        /// <summary>
        ///     Rank of user in Idnet leaderboard.
        /// </summary>
        [Newtonsoft.Json.JsonIgnore]
        public int Rank { get; set; }

        /// <summary>
        ///     Saves the user locally.
        /// </summary>
        public void SaveLocally()
        {
            PlayerPrefs.SetString(NicknamePrefsKey + AppId, Nickname);
            PlayerPrefs.SetString(EmailPrefsKey + AppId, Email);
            PlayerPrefs.SetString(SessionPrefsKey + AppId, SessionToken);
            PlayerPrefs.SetString(SessionCookiePrefsKey + AppId, SessionCookie);
            PlayerPrefs.SetString(PidPrefsKey + AppId, Pid);
            PlayerPrefs.SetInt(BestScorePrefsKey + AppId, BestScore);
            PlayerPrefs.SetInt(CurrentScorePrefsKey + AppId, CurrentScore);
            PlayerPrefs.SetInt(IdnetPointsPrefsKey + AppId, IdnetPoints);
        }

        /// <summary>
        ///     Loads the user locally.
        /// </summary>
        public void LoadLocally()
        {
            Nickname = PlayerPrefs.GetString(NicknamePrefsKey + AppId, "");
            Email = PlayerPrefs.GetString(EmailPrefsKey + AppId, "");
            SessionToken = PlayerPrefs.GetString(SessionPrefsKey + AppId, "");
            SessionCookie = PlayerPrefs.GetString(SessionCookiePrefsKey + AppId, "");
            Pid = PlayerPrefs.GetString(PidPrefsKey + AppId, "");
            BestScore = PlayerPrefs.GetInt(BestScorePrefsKey + AppId, 0);
            CurrentScore = PlayerPrefs.GetInt(CurrentScorePrefsKey + AppId, 0);
            IdnetPoints = PlayerPrefs.GetInt(IdnetPointsPrefsKey + AppId, 0);
        }

        /// <summary>
        ///     Delete local data of user.
        /// </summary>
        public void DeleteLocally()
        {
            PlayerPrefs.DeleteKey(NicknamePrefsKey + AppId);
            PlayerPrefs.DeleteKey(EmailPrefsKey + AppId);
            PlayerPrefs.DeleteKey(SessionPrefsKey + AppId);
            PlayerPrefs.DeleteKey(SessionCookiePrefsKey + AppId);
            PlayerPrefs.DeleteKey(PidPrefsKey + AppId);
            PlayerPrefs.DeleteKey(BestScorePrefsKey + AppId);
            PlayerPrefs.DeleteKey(CurrentScorePrefsKey + AppId);
            PlayerPrefs.DeleteKey(IdnetPointsPrefsKey + AppId);
        }

        public override string ToString()
        {
            return "User: " + Email + "-" + Nickname;
        }

        /// <summary>
        ///     Returns true if the specified user is logged in.
        /// </summary>
        public static bool LoggedIn(User user)
        {
            return user != null && !string.IsNullOrEmpty(user.SessionToken);
        }

        /// <summary>
        ///     Returns true if the <see cref="Current" /> user is logged in.
        /// </summary>
        public static bool LoggedIn()
        {
            return LoggedIn(Current);
        }
    }

    /// <summary>
    ///     Idnet User meta like date_of_birth & gender.
    /// </summary>
    [Serializable]
    public class UserMeta
    {
        /// <summary>
        ///     Users email address.
        /// </summary>
        [Newtonsoft.Json.JsonProperty("gender")]
        public string gender { get; set; }

        [Newtonsoft.Json.JsonProperty("date_of_birth")]
        public string dateOfBirth { get; set; }

        [Newtonsoft.Json.JsonProperty("first_name")]
        public string firstName { get; set; }
    }

    /// <summary>
    ///     Use this class to help unlock achievements.
    /// </summary>
    public class AchievementUnlocker
    {
        /// <summary>
        ///     Secret unlock key on server.
        /// </summary>
        public string Key;

        /// <summary>
        ///     Name of achievement on server.
        /// </summary>
        public string Name;

        public AchievementUnlocker(string name, string key)
        {
            Name = name;
            Key = key;
        }

        public AchievementUnlocker()
        {
        }
    }

    /// <summary>
    ///     y8 button click.
    /// </summary>
    public void Y8LogoClicked()
    {
        BrandingScript.ClickedLogo();
    }

    /// <summary>
    ///     y8 button click in new tab.
    ///     Must be called on "OnMouseDown",and never be used on "OnMouseUp".
    /// </summary>
    public void Y8LogoClicked_OnMouseDown()
    {
        BrandingScript.ClickedLogo_NewTab();
    }

    /// <summary>
    ///     y8account button click in new tab.
    ///     Must be called on "OnMouseDown",and never be used on "OnMouseUp".
    /// </summary>
    public void Y8AccountLogoClick_OnMouseDown()
    {
        BrandingScript.Y8Account_NewTab();
    }

    /// <summary>
    ///     idnet button click.
    /// </summary>
    public void IdnetLogoClick()
    {
        BrandingScript.IdnetLink();
    }

    /// <summary>
    ///     idnet button click in new tab.
    ///     Must be called on "OnMouseDown",and never be used on "OnMouseUp".
    /// </summary>
    public void IdnetLogoClick_OnMouseDown()
    {
        BrandingScript.IdnetLink_NewTab();
    }

    /// <summary>
    ///     MoreGames button click.
    /// </summary>
    public void MoreGamesClick()
    {
        BrandingScript.ClickedMoreGames();
    }

    /// <summary>
    ///     MoreGames button click in new tab.
    ///     Must be called on "OnMouseDown",and never be used on "OnMouseUp".
    /// </summary>
    public void MoreGamesClick_OnMouseDown()
    {
        BrandingScript.ClickedMoreGames_NewTab();
    }

    /// <summary>
    ///     Retrieves a value from the server.
    /// </summary>
    /// <typeparam name="T">
    ///     Type of value to save.
    ///     Can be anything that can be serialized.
    /// </typeparam>
    /// <param name="key">Field identifier.</param>
    /// <param name="callback">Called when the operation completes or fails.</param>
    public void Get<T>(string key, Action<KeyValuePair<string, T>, Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn();

            const string url = UserDataUrl + "retrieve";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken},
                {"key", key}
            };
            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                if (dataResult.ContainsKey("key"))
                    key = dataResult["key"].ToString();

                T value = default(T);
                if (dataResult.ContainsKey("jsondata"))
                {
                    var stringValue = dataResult["jsondata"].ToString();
                    stringValue = RemoveQuotes(stringValue);

                    if (typeof(T) == typeof(string))
                        value = (T)(object)stringValue;
                    else
                        value = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(stringValue);
                }

                return new KeyValuePair<string, T>(key, value);
            }, callback));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(new KeyValuePair<string, T>(key, default(T)), e);
            else throw;
        }
    }

    /// <summary>
    ///     Send's a value to the server.
    ///     Uses Newtonsoft.Json.JsonConvert as the converter.
    /// </summary>
    /// <typeparam name="T">
    ///     Type of value to save.
    ///     Can be anything that can be serialized.
    /// </typeparam>
    /// <param name="key">Field identifier.</param>
    /// <param name="value">Value to submit.</param>
    /// <param name="callback">Called when the operation completes or fails.</param>
    public void Post<T>(string key, T value, Action<T, Exception> callback = null)
    {
        if (I.CurrentMode == GameMode.LocalSave)
            return;

        try
        {
            RequireUserLoggedIn();

            const string url = UserDataUrl + "submit";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken},
                {"key", key},
                {"value", Newtonsoft.Json.JsonConvert.SerializeObject(value)}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                var status = dataResult["status"].ToString();

                if (RemoveQuotes(status) != "ok")
                    throw new ApiException("WWW Response: " + status);

                return value;
            }, callback));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(default(T), e);
            else throw;
        }
    }

    /// <summary>
    ///     Deletes a KeyValuePair from the server.
    /// </summary>
    /// <param name="key">Field identifier.</param>
    /// <param name="callback">Called when the operation completes or fails.</param>
    public void DeleteKey(string key, Action<string, Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn();

            const string url = UserDataUrl + "remove";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken},
                {"key", key}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                var status = dataResult["status"].ToString();

                if (RemoveQuotes(status) != "ok")
                    throw new ApiException("WWW Response: " + status);

                return key;
            }, callback));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(key, e);
            else throw;
        }
    }

    /// <summary>
    ///     Deletes saved progress from the server.
    /// </summary>
    /// <param name="callback">Called when the operation completes or fails.</param>
    public void DeleteAllProgress(Action<string, Exception> callback = null)
    {
        try
        {
            //key is the dummy string
            var key = "";
            RequireUserLoggedIn();

            const string url = UserDataUrl + "remove_all";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                var status = dataResult["status"].ToString();

                if (RemoveQuotes(status) != "ok")
                    throw new ApiException("WWW Response: " + status);

                return key;
            }, callback));
        }
        catch (Exception e)
        {
            var key = "";
            if (callback != null)
                callback(key, e);
            else throw;
        }
    }

#if IDNET_PLAYTOMIC
    /// <summary>
    ///     List / Show user maps.
    /// </summary>
    /// <param name="callback"></param>
    public void ListLevels(Action<Exception> callback)
    {
        try
        {
            var pPlayerLevelOptions = new PPlayerLevelOptions();

            UserLevels.Clear();

            ClosedUserLevels = exception =>
            {
                if (callback != null)
                    callback(exception);
            };

            Gui.Cfsm.SetState<IdnetGui.ListUserLevelsState>();

            Playtomic.PlayerLevels.List(pPlayerLevelOptions, (userLevels, numLevels, response) =>
            {
                // AsyncCache exception for when we close the gui.
                UserLevelsException = GeneratePlaytomicException(response);

                // Ser user levels list.
                if (userLevels != null)
                    UserLevels = userLevels;

                // Update Gui.
                if (GeneratePlaytomicException(response) != null)
                {
                    Gui.ServerException(GeneratePlaytomicException(response));
                }
            });
        }
        catch (Exception exception)
        {
            if (callback != null)
                callback(exception);
            else throw;
        }
    }

    /// <summary>
    ///     Save current map.
    /// </summary>
    /// <param name="levelName"></param>
    /// <param name="objectData"></param>
    /// <param name="callback"></param>
    public void SaveLevel(string levelName, string objectData, Action<PlayerLevel, Exception> callback)
    {
        try
        {
            RequireUserLoggedIn();

            var level = new PlayerLevel
            {
                name = levelName,
                data = objectData,
                playername = User.Current.Nickname,
                playerid = User.Current.Pid
            };

            Playtomic.PlayerLevels.Save(level, (playerLevel, response) =>
            {
                var exception = GeneratePlaytomicException(response);

                if (callback != null)
                    callback(playerLevel, exception);
                else if (exception != null)
                    throw exception;
            });
        }
        catch (Exception exception)
        {
            if (callback != null)
                callback(null, exception);
            else throw;
        }
    }

    /// <summary>
    ///     Load a map.
    /// </summary>
    /// <param name="callback"></param>
    public void LoadLevel(PlayerLevel level, Action<PlayerLevel, Exception> callback)
    {
        try
        {
            RequireUserLoggedIn();

            Playtomic.PlayerLevels.Load(level.levelid, (responseLevel, response) =>
            {
                var exception = GeneratePlaytomicException(response);

                if (callback != null)
                    callback(responseLevel, exception);
                else if (exception != null)
                    throw exception;
            });
        }
        catch (Exception exception)
        {
            if (callback != null)
                callback(null, exception);
            else throw;
        }
    }

    /// <summary>
    ///     Rate the current map.
    /// </summary>
    /// <param name="level"></param>
    /// <param name="rating"></param>
    /// <param name="callback"></param>
    public void RateLevel(PlayerLevel level, int rating, Action<PlayerLevel, int, Exception> callback)
    {
        try
        {
            if (rating < 0 || rating > 10)
                throw new ArgumentOutOfRangeException("Rating must be out of ten!");

            Playtomic.PlayerLevels.Rate(level.levelid, rating, response =>
            {
                var exception = GeneratePlaytomicException(response);

                if (callback != null)
                    callback(level, rating, exception);
                else if (exception != null)
                    throw exception;
            });
        }
        catch (Exception exception)
        {
            if (callback != null)
                callback(level, rating, exception);
            else throw;
        }
    }

    /// <summary>
    ///     Show / List all user achievements.
    /// </summary>
    /// <param name="callback"></param>
    public void ListAchievements(Action<Exception> callback = null)
    {
        try
        {
            RequireAppId();

            User.Current.Achievements.Clear();

            ClosedListAchievements = exception =>
            {
                if (callback != null)
                    callback(exception);
            };

            var achievementState = Gui.Cfsm.SetState<IdnetGui.ListAchievementsState>();

            var pAchievementOptions = new PAchievementOptions
            {
                playerid = User.Current.Pid
            };

            Playtomic.Achievements.List(pAchievementOptions, (achievements, response) =>
            {
                var exception = GeneratePlaytomicException(response);

                if (exception != null)
                {
                    Gui.ServerException(exception);
                }
                else
                {
                    User.Current.Achievements = achievements;
                    achievementState.AchievementListUpdated(achievements);
                }
            });
        }
        catch (Exception exception)
        {
            if (callback != null)
                callback(exception);
            else throw;
        }
    }

    /// <summary>
    ///     Give an achievement to a player.
    /// </summary>
    /// <remarks>
    ///     Idnet should provide a GUI popup for this to give to the user.
    /// </remarks>
    /// <param name="achievementUnlocker"></param>
    /// <param name="callback"></param>
    public void UnlockAchievement(AchievementUnlocker achievementUnlocker, Action<Exception> callback = null)
    {
        try
        {
            // RequireUserLoggedIn();

            // save achievement id's for guest user locally and return.
            if (!User.LoggedIn())
            {
                if (!GuestAchievements.ContainsKey(achievementUnlocker.Name))
                    GuestAchievements.Add(achievementUnlocker.Name, achievementUnlocker.Key);
                return;
            }

            var achievement = new PlayerAchievement
            {
                achievement = achievementUnlocker.Name,
                achievementkey = achievementUnlocker.Key,
                allowduplicates = false,
                playerid = User.Current.Pid,
                playername = User.Current.Nickname
                //fields = new Dictionary<string, object>
                //{
                //    { "Difficulty", PlayerAchievement.Difficulty.Easy }
                //}
            };

            Playtomic.Achievements.Save(achievement, response =>
            {
                var exception = GeneratePlaytomicException(response);

                if (callback != null)
                    callback(exception);
                // v-1.6.7
                //else if (exception != null)
                //throw exception;
            });
        }
        catch (Exception exception)
        {
            if (callback != null)
                callback(exception);
            else throw;
        }
    }

#endif

    /// <summary>
    ///     Shows a login / register screen and waits for it to happen.
    ///     Task will be marked as cancelled if user exits out of it.
    /// </summary>
    /// <returns>Task that waits for login / register / cancellation.</returns>
    public void LoginRegister(Action<Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn(false);

            ClosedLoginRegister = (user, exception) =>
            {
                if (callback != null)
                    callback(exception);
            };

            Gui.Cfsm.SetState<IdnetGui.LoginState>();
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(e);
            else throw;
        }
    }

    /// <summary>
    ///     Shows a login screen and waits for it to happen.
    ///     Task will be marked as cancelled if user exits out of it.
    /// </summary>
    /// <returns>Task that waits for login / register / cancellation.</returns>
    public void Login(Action<Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn(false);

            ClosedLoginRegister = (user, exception) =>
            {
                if (callback != null)
                    callback(exception);
            };

            Gui.Cfsm.SetState<IdnetGui.LoginState>();
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(e);
            else throw;
        }
    }

    /// <summary>
    ///     Shows a register screen and waits for it to happen.
    ///     Task will be marked as cancelled if user exits out of it.
    /// </summary>
    /// <returns>Task that waits for login / register / cancellation.</returns>
    public void Register(Action<Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn(false);

            ClosedLoginRegister = (user, exception) =>
            {
                if (callback != null)
                    callback(exception);
            };

            Gui.Cfsm.SetState<IdnetGui.RegisterState>();
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(e);
            else throw;
        }
    }


    /// <summary>
    ///     Actual login function. Should only be called by <see cref="IdnetGui" />.
    /// </summary>
    public void Login(string email, string password, Action<User, Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn(false);

            RequireAppId();
            Gui.Interactable = false;

            var url = UserDataUrl + "login";
            var jsonData = new Dictionary<string, object>
            {
                {"app_id", AppId},
                {"login", email},
                {"password", password}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync<User>
                (request, SetUser, (user, exception) => LoginAndRegisterGuiCallback(user, exception, callback)));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(null, e);
            else throw;
        }
    }

    /// <summary>
    ///     Allows for a manual login attempt, bypassing the Idnet's GUI.
    /// </summary>
    /// <returns>Task that will complete when the operation is over/cancelled.</returns>
    public void Register(string email, string password, string nickname, Action<User, Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn(false);

            Gui.Interactable = false;

            RequireAppId();

            const string url = UserDataUrl + "register";
            var meta = new Dictionary<string, string>
            {
                {"first_name", Idnet.User.Current.Firstname},
                {"gender", Idnet.User.Current.Gender},
                {"date_of_birth", Idnet.User.Current.BirthYear + "-" + Idnet.User.Current.BirthMonth + "-1"}
            };

            var jsonData = new Dictionary<string, object>
            {
                {"app_id", AppId},
                {"email", email},
                {"password", password},
                {"nickname", Idnet.User.Current.Nickname},
                {"hostname", Idnet.I.RefererDomain},
                {"meta", meta}
            };
            /*
            var jsonData = new Dictionary<string, object>
            {
                {"app_id", AppId},
                {"email", email},
                {"nickname", nickname},
                {"password", password}
            };
            */

            var request = GetWww(jsonData, url);


            // ReSharper disable once RedundantTypeArgumentsOfMethod
            StartCoroutine(CR_ProcessAsync<User>(request, SetUser,
                (user, exception) => LoginAndRegisterGuiCallback(user, exception, callback)));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(null, e);
            else throw;
        }
    }

    /// <summary>
    ///     Open window that contains idnet app-id.
    /// </summary>
    public void SecretWindow(Action<Exception> callback = null)
    {
        try
        {
            Gui.Cfsm.SetState<IdnetGui.SecretState>();
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(e);
            else throw;
        }
    }

    /// <summary>
    ///     Protection api(Blacklisted and Sponsered domains).
    /// </summary>
    public void Protection(Action<Exception> callback = null)
    {
        try
        {
            const string url = ProtectionGetUrl;

            var request = new WWW(url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                var blacklistedUrls =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(dataResult["blacklisted_urls"].ToString());
                var approvedUrls =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(dataResult["approved_domains"].ToString());

                foreach (var blacklistedUrl in blacklistedUrls)
                {
                    var blacklistedDomain = blacklistedUrl.StartsWith(".")
                        ? blacklistedUrl.Remove(0, 1)
                        : blacklistedUrl;

                    BlacklistedUrlsList.Add(blacklistedDomain);
                }

                foreach (var approvedUrl in approvedUrls)
                {
                    var approvedDomain = approvedUrl.StartsWith(".") ? approvedUrl.Remove(0, 1) : approvedUrl;

                    ApprovedDomainsList.Add(approvedDomain);
                }

                foreach (var blacklistedUrl in BlacklistedUrlsList)
                {
                    if (I.RefererDomain.Contains(blacklistedUrl))
                        I.IsBlacklisted = true;
                }

                foreach (var approvedUrl in ApprovedDomainsList)
                {
                    if (I.RefererDomain.Contains(approvedUrl))
                    {
                        I.IsSponsor = true;
                        I.IsLogoClickable = false;
                    }
                }

                if (I.IsBlacklisted)
                {
                    I.IsSponsor = false;
                    I.IsLogoClickable = true;
                }

                return I.RefererDomain;
            }, callback, true));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(e);
            else throw;
        }
    }

    public static DateTime UnixToDateTime(double unixTimeStamp)
    {
        // Unix timestamp is seconds past epoch.
        var dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime();
        return dateTime;
    }

    /// <summary>
    ///     Get Current Year.
    /// </summary>
    public void GetCurrentYear(Action<int, Exception> callback = null)
    {
        try
        {
            string url = UnixTime;

            var request = new WWW(url);

            int year = 1970;

            StartCoroutine(CR_ProcessAsync<int>(request, dataResult =>
            {
                var success = bool.Parse(dataResult["success"].ToString());
                if (success)
                {
                    var unixtime = int.Parse(dataResult["time"].ToString());
                    year = UnixToDateTime(unixtime).Year;
                }
                else
                {
                    year = System.DateTime.UtcNow.Year;
                }

                CurrentYear = year;

                return year;
            }, callback));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(CurrentYear, e);
            else throw;
        }
    }

//#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     Get Current Year.
    /// </summary>
    public void GetExclusiveStatus(Action<bool, Exception> callback = null)
    {
        try
        {
            string url = ClientOptionsUrl + "/" + AppId;

            var request = new WWW(url);

            bool distribution_blocked = true;

            StartCoroutine(CR_ProcessAsync<bool>(request, dataResult =>
            {
                var success = bool.Parse(dataResult["success"].ToString());
                if (success)
                {
                    var client_options = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<object, object>>(dataResult["options"].ToString());
                    distribution_blocked = bool.Parse(client_options["distribution_blocked"].ToString());
                }
                else
                {
                    distribution_blocked = true;
                }

                return distribution_blocked;
            }, callback));
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(true, e);
            else throw;
        }
    }
//#endif


    /// <summary>
    ///     Get IDnet points.
    /// </summary>
    public void GetIdnetPoints(Action<Exception> callback = null)
	{
		try
		{
			RequireUserLoggedIn(true);

			string url = IdnetPointsGetUrl + "/" + User.Current.Pid;

			var request = new WWW(url);

			StartCoroutine(CR_ProcessAsync(request, dataResult =>
			{
				var idnetPoints =
					Newtonsoft.Json.JsonConvert.DeserializeObject<int>(dataResult["points"].ToString());

				User.Current.IdnetPoints = idnetPoints;

				return idnetPoints;
			}, callback, true));
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(e);
			else throw;
		}
	}


	/// <summary>
	///     Causes the currently logged in user to logout.
	/// </summary>
	public void Logout(Action<Exception> callback = null)
	{
		Debug.Log(IDNET + " clearing logged-in user's cache");

		try
		{
			RequireUserLoggedIn();

			// Reset logged-in User details.
			ResetUser();

			User.Current.SaveLocally();

			Gui.StatusTextColor = Color.grey;
			Gui.StatusText = "Logged out.";
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(e);
			else throw;
		}
	}

	/// <summary>
	///     Get the best score of user from a particular idnet leaderboard.Do not call in Update() or OnGUI() obviously.
	/// </summary>
	public void GetBestScore(string leaderboardTable, Action<Exception> callback = null)
	{
		try
		{
			RequireUserLoggedIn();
			BestScoreList.Clear();

#if IDNET_PLAYTOMIC
			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}

			string table;
			if (leaderboardTable == "highscores" || leaderboardTable == "default" || leaderboardTable == "Default" ||
				string.IsNullOrEmpty(leaderboardTable))
				table = "highscores";
			else
				table = leaderboardTable;


			var leaderboardOptions = new PLeaderboardOptions
			{
				table = table,
				highest = !ReverseLeaderboard,
				playerid = User.Current.Pid
			};
			Playtomic.Leaderboards.List(leaderboardOptions, (playerScores, numScores, response) =>
			{
				var exception = GeneratePlaytomicException(response);

				//  Gui.Interactable = true;
				I.LeaderboardException = exception;

				// If there is an exception.
				if (exception != null)
				{
					Gui.ApiException(exception);
					return;
				}

				// Set highscores.
				BestScoreList = playerScores.Select(playerScore => new Highscore
				{
					Score = playerScore.points,
					Rank = playerScore.rank
				})
					.ToList();

				if (BestScoreList.Count == 0)
				{
					User.Current.BestScore = 0;
					User.Current.Rank = 0;
				}

				foreach (var highscore in BestScoreList)
				{
					User.Current.BestScore = (int)highscore.Score;
					User.Current.Rank = (int)highscore.Rank;
					break;
				}

				if (callback != null)
					callback(exception);
				else if (exception != null)
					throw exception;
			});

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(e);
			else throw;
		}
	}

	/// <summary>
	///     Get the best score of user from idnet leaderboard.Do not call in Update() or OnGUI() obviously.
	/// </summary>
	public void GetBestScore(Action<Exception> callback = null)
	{
		try
		{
			RequireUserLoggedIn();
			BestScoreList.Clear();

#if IDNET_PLAYTOMIC

			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}

			var leaderboardOptions = new PLeaderboardOptions
			{
				table = "highscores",
				highest = !ReverseLeaderboard,
				playerid = User.Current.Pid
			};
			Playtomic.Leaderboards.List(leaderboardOptions, (playerScores, numScores, response) =>
			{
				var exception = GeneratePlaytomicException(response);

				//              Gui.Interactable = true;
				I.LeaderboardException = exception;

				// If there is an exception.
				if (exception != null)
				{
					Gui.ApiException(exception);
					return;
				}

				// Set highscores.
				BestScoreList = playerScores.Select(playerScore => new Highscore
				{
					Score = playerScore.points,
					Rank = playerScore.rank
				})
					.ToList();

				if (BestScoreList.Count == 0)
				{
					User.Current.BestScore = 0;
					User.Current.Rank = 0;
				}

				foreach (var highscore in BestScoreList)
				{
					User.Current.BestScore = (int)highscore.Score;
					User.Current.Rank = (int)highscore.Rank;
					break;
				}

				if (callback != null)
					callback(exception);
				else if (exception != null)
					throw exception;
			});

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(e);
			else throw;
		}
	}


	/// <summary>
	///     Displays the leaderboard. Task ends when it closes.
	///     Only GET request in API.
	/// </summary>
	/// <returns></returns>
	/// <param name="leaderboardTable">Leaderboard table name.</param>
	/// <param name="callback">Called when async operation returns.</param>
	/// <param name="pageNumber">Page to load. If null, uses <see cref="LeaderboardPage" />.</param>
	/// <param name="mode"><see cref="LeaderboardMode" />. If null, uses <see cref="LeaderboardModeValue" />.</param>
	/// <param name="allowDuplicateResults">
	///     Allow the same user to have multiple results?
	///     If null, uses <see cref="AllowDuplicateLeaderboardResults" />.
	/// </param>
	public void Leaderboard(string leaderboardTable, Action<List<Highscore>, Exception> callback = null,
		int? pageNumber = null,
		LeaderboardMode? mode = null, bool? allowDuplicateResults = null)
	{
		try
		{
			RequireAppId();

			Gui.Interactable = false;
			Highscores.Clear();

			// AsyncCache leaderboard settings.
			if (pageNumber != null)
				LeaderboardPage = Mathf.Max((int)pageNumber, 1);

			if (mode != null)
				LeaderboardModeValue = (LeaderboardMode)mode;

			if (allowDuplicateResults != null)
				AllowDuplicateLeaderboardResults = (bool)allowDuplicateResults;

			if (leaderboardTable == "highscores" || leaderboardTable == "default" || leaderboardTable == "Default" ||
				string.IsNullOrEmpty(leaderboardTable))
				LeaderboardTable = "highscores";
			else
				LeaderboardTable = leaderboardTable;

			if (User.LoggedIn())
			{
				if (pageNumber == null)
				{
					GetBestScore(LeaderboardTable);
					LeaderboardPage = 1;
				}
			}
			else
			{
				if (pageNumber == null)
				{
					LeaderboardPage = 1;
				}
			}

			// AsyncCache the user callback so that we can call it from inside the GUI.

			if (callback != null)
				ClosedLeaderboard = callback;


			var leaderboardState = Gui.Cfsm.SetState<IdnetGui.LeaderboardState>();

#if IDNET_PLAYTOMIC // Playtomic.
			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}

			var leaderboardOptions = new PLeaderboardOptions
			{
				table = LeaderboardTable,
				page = LeaderboardPage,
				perpage = HighscoresPerLeaderboardPage,
				highest = !ReverseLeaderboard,
				allowduplicates = AllowDuplicateLeaderboardResults,
				// mode strings: "today", "last7days", "last30days"
				mode = LeaderboardModeValue.ToString().ToLowerInvariant()
			};

			Playtomic.Leaderboards.List(leaderboardOptions, (playerScores, numScores, response) =>
			{
				// Ignore the callback if we've cancelled (closed the gui).
				/*
                 * Changed in 1.3.3
                if (ClosedLeaderboard == null)
                    return;
                    */

				var exception = GeneratePlaytomicException(response);

				Gui.Interactable = true;
				I.LeaderboardException = exception;

				// If there is an exception.
				if (exception != null)
				{
					Gui.ApiException(exception);
					return;
				}

				// Set highscores.
				Highscores = playerScores.Select(playerScore => new Highscore
				{
					Nickname = playerScore.playername,
					Pid = playerScore.playerid,
					Score = playerScore.points,
					UpdatedAtDt = playerScore.updatedat,
					RDate = playerScore.rdate
				})
					.ToList();


				if (Highscores.Count != 0)
				{
					foreach (var highscore in Highscores)
					{
						if (highscore.Pid == User.Current.Pid)
						{
							User.Current.CurrentScore = User.Current.CurrentScore;
						}
					}
				}

				leaderboardState.LeaderboardsUpdated(Highscores);

				// We now wait for the user to close the GUI.
				// Closing the leaderboard will call the API user (Developer)'s callback.
			});


#else
            var url = LEADERBOARD_GET_URL + AppId;
            var www = new WWW(url, (byte[]) null);

            var guiCallback = new Action<List<Highscore>, Exception>((leaderboard, exception) =>
            {
                // Ignore the callback if we've cancelled (closed the gui).
                if (ClosedLeaderboard == null)
                    return;

                Gui.Interactable = true;
                LeaderboardException = exception;

                // If there is an exception, return.
                // The Gui already knows.
                if (exception != null) return;

                Gui.StatusTextColor = Color.grey;
                Gui.StatusText = Highscores.Count + " highscores loaded.";

                // We now wait for the user to close the GUI.
                // Closing the leaderboard will call the API user (Developer)'s callback.
            });

            Func<Dictionary<string, object>, List<Highscore>> highscoreParser = dataResult =>
            {
                // Clear leaderboard.
                Highscores.Clear();

                // Deserialize server Json data.
                var value = dataResult["data"].ToString();

                var jsonLeaderboardData =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<List<LeaderboardJsonRoot>>(value);

                // Move data from one data structure into the other.
                foreach (var lj in jsonLeaderboardData)
                {
                    Highscores.Add(
                        new Highscore
                        {
                            Pid = lj.user.id,
                            Nickname = lj.user.nickname,
                            Score = lj.score,
                            UpdatedAtDt = Highscore.UnixToDateTime(lj.updated_at)
                        });
                }

                return Highscores;
            };

            StartCoroutine(CR_ProcessAsync(www,
                highscoreParser, guiCallback));

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(null, e);
			else throw;
		}
	}

	/// <summary>
	///     Displays the leaderboard. Task ends when it closes.
	///     Only GET request in API.
	/// </summary>
	/// <returns></returns>
	/// <param name="callback">Called when async operation returns.</param>
	/// <param name="pageNumber">Page to load. If null, uses <see cref="LeaderboardPage" />.</param>
	/// <param name="mode"><see cref="LeaderboardMode" />. If null, uses <see cref="LeaderboardModeValue" />.</param>
	/// <param name="allowDuplicateResults">
	///     Allow the same user to have multiple results?
	///     If null, uses <see cref="AllowDuplicateLeaderboardResults" />.
	/// </param>
	public void Leaderboard(Action<List<Highscore>, Exception> callback = null, int? pageNumber = null,
		LeaderboardMode? mode = null, bool? allowDuplicateResults = null)
	{
		try
		{
			RequireAppId();

			Gui.Interactable = false;
			Highscores.Clear();

			// AsyncCache leaderboard settings.
			if (pageNumber != null)
				LeaderboardPage = Mathf.Max((int)pageNumber, 1);

			if (mode != null)
				LeaderboardModeValue = (LeaderboardMode)mode;

			if (allowDuplicateResults != null)
				AllowDuplicateLeaderboardResults = (bool)allowDuplicateResults;

			LeaderboardTable = "highscores";

			if (User.LoggedIn())
			{
				if (pageNumber == null)
				{
					GetBestScore(LeaderboardTable);
					LeaderboardPage = 1;
				}
			}
			else
			{
				if (pageNumber == null)
				{
					LeaderboardPage = 1;
				}
			}

			// AsyncCache the user callback so that we can call it from inside the GUI.

			if (callback != null)
				ClosedLeaderboard = callback;


			var leaderboardState = Gui.Cfsm.SetState<IdnetGui.LeaderboardState>();

#if IDNET_PLAYTOMIC // Playtomic.

			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}

			var leaderboardOptions = new PLeaderboardOptions
			{
				table = LeaderboardTable,
				page = LeaderboardPage,
				perpage = HighscoresPerLeaderboardPage,
				highest = !ReverseLeaderboard,
				allowduplicates = AllowDuplicateLeaderboardResults,
				// mode strings: "today", "last7days", "last30days"
				mode = LeaderboardModeValue.ToString().ToLowerInvariant()
			};

			Playtomic.Leaderboards.List(leaderboardOptions, (playerScores, numScores, response) =>
			{
				// Ignore the callback if we've cancelled (closed the gui).
				/*
                 * Changed in 1.3.3
                if (ClosedLeaderboard == null)
                    return;
                    */

				var exception = GeneratePlaytomicException(response);

				Gui.Interactable = true;
				I.LeaderboardException = exception;

				// If there is an exception.
				if (exception != null)
				{
					Gui.ApiException(exception);
					return;
				}

				// Set highscores.
				Highscores = playerScores.Select(playerScore => new Highscore
				{
					Nickname = playerScore.playername,
					Pid = playerScore.playerid,
					Score = playerScore.points,
					UpdatedAtDt = playerScore.updatedat,
					RDate = playerScore.rdate
				})
					.ToList();


				if (Highscores.Count != 0)
				{
					foreach (var highscore in Highscores)
					{
						if (highscore.Pid == User.Current.Pid)
						{
							User.Current.CurrentScore = User.Current.CurrentScore;
						}
					}
				}

				leaderboardState.LeaderboardsUpdated(Highscores);

				// We now wait for the user to close the GUI.
				// Closing the leaderboard will call the API user (Developer)'s callback.
			});


#else
            var url = LEADERBOARD_GET_URL + AppId;
            var www = new WWW(url, (byte[]) null);

            var guiCallback = new Action<List<Highscore>, Exception>((leaderboard, exception) =>
            {
                // Ignore the callback if we've cancelled (closed the gui).
                if (ClosedLeaderboard == null)
                    return;

                Gui.Interactable = true;
                LeaderboardException = exception;

                // If there is an exception, return.
                // The Gui already knows.
                if (exception != null) return;

                Gui.StatusTextColor = Color.grey;
                Gui.StatusText = Highscores.Count + " highscores loaded.";

                // We now wait for the user to close the GUI.
                // Closing the leaderboard will call the API user (Developer)'s callback.
            });

            Func<Dictionary<string, object>, List<Highscore>> highscoreParser = dataResult =>
            {
                // Clear leaderboard.
                Highscores.Clear();

                // Deserialize server Json data.
                var value = dataResult["data"].ToString();

                var jsonLeaderboardData =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<List<LeaderboardJsonRoot>>(value);

                // Move data from one data structure into the other.
                foreach (var lj in jsonLeaderboardData)
                {
                    Highscores.Add(
                        new Highscore
                        {
                            Pid = lj.user.id,
                            Nickname = lj.user.nickname,
                            Score = lj.score,
                            UpdatedAtDt = Highscore.UnixToDateTime(lj.updated_at)
                        });
                }

                return Highscores;
            };

            StartCoroutine(CR_ProcessAsync(www,
                highscoreParser, guiCallback));

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(null, e);
			else throw;
		}
	}


	/// <summary>
	///     Refresh the current leaderboard.
	///     No callback available at this time.
	///     Must be called while leaderboard is active.
	/// </summary>
	public void RefreshLeaderboard()
	{
		if (Gui.Cfsm.ActiveType != typeof(IdnetGui.LeaderboardState))
			throw new InvalidOperationException("RefreshLeaderboard must be called while leaderboard is active.");

		Leaderboard(LeaderboardTable, null, LeaderboardPage, LeaderboardModeValue);
	}

	/// <summary>
	///     DEPRECATED.(Use Idnet.I.Leaderboard("Default") instead.)
	/// </summary>
	public void ShowLeaderboard(string leaderboardTable = "highscores",
		Action<List<Highscore>, Exception> callback = null, int? pageNumber = null,
		LeaderboardMode? mode = null, bool? allowDuplicateResults = null)
	{
		try
		{
			RequireAppId();

			Gui.Interactable = false;
			Highscores.Clear();

			// AsyncCache leaderboard settings.
			if (pageNumber != null)
				LeaderboardPage = Mathf.Max((int)pageNumber, 1);

			if (mode != null)
				LeaderboardModeValue = (LeaderboardMode)mode;

			if (allowDuplicateResults != null)
				AllowDuplicateLeaderboardResults = (bool)allowDuplicateResults;

			if (leaderboardTable != "highscores")
				LeaderboardTable = leaderboardTable;

			if (User.LoggedIn())
			{
				if (pageNumber == null)
				{
					GetBestScore(LeaderboardTable);
					LeaderboardPage = 1;
				}
			}
			else
			{
				if (pageNumber == null)
				{
					LeaderboardPage = 1;
				}
			}

			// AsyncCache the user callback so that we can call it from inside the GUI.
			/*
             * Changed in 1.3.3
            if (callback != null)
                ClosedLeaderboard = callback;
                */

			var leaderboardState = Gui.Cfsm.SetState<IdnetGui.LeaderboardState>();


#if IDNET_PLAYTOMIC // Playtomic.

			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}

			var leaderboardOptions = new PLeaderboardOptions
			{
				table = LeaderboardTable,
				page = LeaderboardPage,
				perpage = HighscoresPerLeaderboardPage,
				highest = !ReverseLeaderboard,
				allowduplicates = AllowDuplicateLeaderboardResults,
				// mode strings: "today", "last7days", "last30days"
				mode = LeaderboardModeValue.ToString().ToLowerInvariant()
			};

			Playtomic.Leaderboards.List(leaderboardOptions, (playerScores, numScores, response) =>
			{
				// Ignore the callback if we've cancelled (closed the gui).
				/*
                 * Changed in 1.3.3
                if (ClosedLeaderboard == null)
                    return;
                    */

				var exception = GeneratePlaytomicException(response);

				Gui.Interactable = true;
				I.LeaderboardException = exception;

				// If there is an exception.
				if (exception != null)
				{
					Gui.ApiException(exception);
					return;
				}

				// Set highscores.
				Highscores = playerScores.Select(playerScore => new Highscore
				{
					Nickname = playerScore.playername,
					Pid = playerScore.playerid,
					Score = playerScore.points,
					UpdatedAtDt = playerScore.updatedat,
					RDate = playerScore.rdate
				})
					.ToList();


				if (Highscores.Count != 0)
				{
					foreach (var highscore in Highscores)
					{
						if (highscore.Pid == User.Current.Pid)
						{
							User.Current.CurrentScore = User.Current.CurrentScore;
						}
					}
				}

				leaderboardState.LeaderboardsUpdated(Highscores);

				// We now wait for the user to close the GUI.
				// Closing the leaderboard will call the API user (Developer)'s callback.
			});


#else
            var url = LEADERBOARD_GET_URL + AppId;
            var www = new WWW(url, (byte[]) null);

            var guiCallback = new Action<List<Highscore>, Exception>((leaderboard, exception) =>
            {
                // Ignore the callback if we've cancelled (closed the gui).
                if (ClosedLeaderboard == null)
                    return;

                Gui.Interactable = true;
                LeaderboardException = exception;

                // If there is an exception, return.
                // The Gui already knows.
                if (exception != null) return;

                Gui.StatusTextColor = Color.grey;
                Gui.StatusText = Highscores.Count + " highscores loaded.";

                // We now wait for the user to close the GUI.
                // Closing the leaderboard will call the API user (Developer)'s callback.
            });

            Func<Dictionary<string, object>, List<Highscore>> highscoreParser = dataResult =>
            {
                // Clear leaderboard.
                Highscores.Clear();

                // Deserialize server Json data.
                var value = dataResult["data"].ToString();

                var jsonLeaderboardData =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<List<LeaderboardJsonRoot>>(value);

                // Move data from one data structure into the other.
                foreach (var lj in jsonLeaderboardData)
                {
                    Highscores.Add(
                        new Highscore
                        {
                            Pid = lj.user.id,
                            Nickname = lj.user.nickname,
                            Score = lj.score,
                            UpdatedAtDt = Highscore.UnixToDateTime(lj.updated_at)
                        });
                }

                return Highscores;
            };

            StartCoroutine(CR_ProcessAsync(www,
                highscoreParser, guiCallback));

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(null, e);
			else throw;
		}
	}

	/// <summary>
	///     Refresh the current leaderboard.
	///     No callback available at this time.
	///     Must be called while leaderboard is active.
	/// </summary>
	public void RefreshShowLeaderboard()
	{
		if (Gui.Cfsm.ActiveType != typeof(IdnetGui.LeaderboardState))
			throw new InvalidOperationException("RefreshLeaderboard must be called while leaderboard is active.");

		ShowLeaderboard(LeaderboardTable, null, LeaderboardPage, LeaderboardModeValue);
	}

	public enum LeaderboardMode
	{
		Today,
		Last7Days,
		Last30Days,
		AllTime
	}

	/// <summary>
	///     Posts a single highscore to a particular level leaderboard.
	///     Example: Idnet.I.PostHighscore (exampleTime,"Level 1");
	/// </summary>
	/// <returns>Task that completes when operation is complete.</returns>
	public void PostHighscore(int score, string leaderboardTable, Action<long, Exception> callback = null)
	{
		try
		{
			// RequireUserLoggedIn();
			User.Current.CurrentScore = score;

			if (!User.LoggedIn())
			{
				// Debug.Log("User not logged-in to idnet,hence will not post score.");
				return;
			}

			if (leaderboardTable == "highscores" || leaderboardTable == "default" || leaderboardTable == "Default" ||
				string.IsNullOrEmpty(leaderboardTable))
				LeaderboardTable = "highscores";
			else
				LeaderboardTable = leaderboardTable;


#if IDNET_PLAYTOMIC

			if (ReverseLeaderboard != true)
			{
				// Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}


			var playerScore = new PlayerScore
			{
				playerid = User.Current.Pid,
				playername = User.Current.Nickname,
				points = score,
				table = LeaderboardTable,
				allowduplicates = false,
				highest = !ReverseLeaderboard
			};

			Playtomic.Leaderboards.Save(playerScore, response =>
			{
				var exception = GeneratePlaytomicException(response);

				if (callback != null)
					callback(playerScore.points, exception);
				else if (exception != null)
					throw exception;
			});

#else
            // Custom Post code because custom url and return info.
            const string url = POST_HIGHSCORE_API_URL + "score.json";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken},
                {"score", Newtonsoft.Json.JsonConvert.SerializeObject(score)}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                switch (request.Response.DataAsText)
                {
                    case "true":
                        // Worked. 
                        break;
                    case "false":
                        throw new ApiException("Score can only be numeric.");
                    default:
                        throw new ApiException("Post score failed.");
                }

                return score;
            }, callback));

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(score, e);
			else throw;
		}
	}

	/// <summary>
	///     Posts a single highscore.
	/// </summary>
	/// <returns>Task that completes when operation is complete.</returns>
	public void PostHighscore(int score, Action<long, Exception> callback = null)
	{
		try
		{
			// RequireUserLoggedIn();
			User.Current.CurrentScore = score;

			if (!User.LoggedIn())
			{
				// Debug.Log("User not logged-in to idnet,hence will not post score.");
				return;
			}

#if IDNET_PLAYTOMIC

			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}


			var playerScore = new PlayerScore
			{
				playerid = User.Current.Pid,
				playername = User.Current.Nickname,
				points = score,
				table = "highscores",
				allowduplicates = false,
				highest = !ReverseLeaderboard
			};

			Playtomic.Leaderboards.Save(playerScore, response =>
			{
				var exception = GeneratePlaytomicException(response);

				if (callback != null)
					callback(playerScore.points, exception);
				else if (exception != null)
					throw exception;
			});

#else
            // Custom Post code because custom url and return info.
            const string url = POST_HIGHSCORE_API_URL + "score.json";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken},
                {"score", Newtonsoft.Json.JsonConvert.SerializeObject(score)}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                switch (request.Response.DataAsText)
                {
                    case "true":
                        // Worked. 
                        break;
                    case "false":
                        throw new ApiException("Score can only be numeric.");
                    default:
                        throw new ApiException("Post score failed.");
                }

                return score;
            }, callback));

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(score, e);
			else throw;
		}
	}


	/// <summary>
	///     Submit score and hence open corresponding leaderboard window.
	/// </summary>
	public void SubmitHighScore(int highscore, string leaderboardTable = null)
	{
		if (User.LoggedIn())
		{
			// PostScore with callback,pass your OWN score below,instead of exampleScore.
			if (leaderboardTable == "highscores" || leaderboardTable == "default" || leaderboardTable == "Default" ||
				string.IsNullOrEmpty(leaderboardTable))
			{
				I.PostHighscore(highscore, (score, postHighscoreException) =>
				{
					if (postHighscoreException == null)
					{
						//Callback: Post Score succesfull here,so opening leaderboard window.
						I.Leaderboard();
					}
				});
			}
			else
			{
				I.PostHighscore(highscore, leaderboardTable, (score, postHighscoreException) =>
				{
					if (postHighscoreException == null)
					{
						//Callback: Post Score succesfull here,so opening leaderboard window.
						I.Leaderboard(leaderboardTable);
					}
				});
			}
		}
		else
		{
			// Open Login window.
			I.Login(loginRegisterException =>
			{
				if (User.LoggedIn())
				{
					/* Callback: Login/Register SUCCESS,hence posting score now. */
					Debug.Log("User logged in: " + "Nickname " + User.Current.Nickname);
					// PostScore with callback,pass your OWN score below,instead of exampleScore.
					if (leaderboardTable == "highscores" || leaderboardTable == "default" ||
						leaderboardTable == "Default" || string.IsNullOrEmpty(leaderboardTable))
					{
						I.PostHighscore(highscore, (score, postHighscoreException) =>
						{
							if (postHighscoreException == null)
							{
								//Callback: Post Score succesfull here,so opening leaderboard window.
								I.Leaderboard();
							}
						});
					}
					else
					{
						I.PostHighscore(highscore, leaderboardTable, (score, postHighscoreException) =>
						{
							if (postHighscoreException == null)
							{
								//Callback: Post Score succesfull here,so opening leaderboard window.
								I.Leaderboard(leaderboardTable);
							}
						});
					}
				}
			});
		}
	}


	/// <summary>
	///     DEPRECATED,use Idnet.I.PostHighscore(score,"YourLeaderboardName"); instead.
	/// </summary>
	public void SubmitScore(int score, string leaderboardTable, Action<long, Exception> callback = null)
	{
		try
		{
			RequireUserLoggedIn();
			User.Current.CurrentScore = score;

			if (leaderboardTable == "highscores" || leaderboardTable == "default" || leaderboardTable == "Default" ||
				string.IsNullOrEmpty(leaderboardTable))
				LeaderboardTable = "highscores";
			else
				LeaderboardTable = leaderboardTable;


#if IDNET_PLAYTOMIC

			if (ReverseLeaderboard != true)
			{
				//Make Sure if user requested Timer leaderboard,than it must be a reverse leaderboard(Low to High).
				ReverseLeaderboard = TimerLeaderboard;
			}


			var playerScore = new PlayerScore
			{
				playerid = User.Current.Pid,
				playername = User.Current.Nickname,
				points = score,
				table = LeaderboardTable,
				allowduplicates = false,
				highest = !ReverseLeaderboard
			};

			Playtomic.Leaderboards.Save(playerScore, response =>
			{
				var exception = GeneratePlaytomicException(response);

				if (callback != null)
					callback(playerScore.points, exception);
				else if (exception != null)
					throw exception;
			});

#else
            // Custom Post code because custom url and return info.
            const string url = POST_HIGHSCORE_API_URL + "score.json";
            var jsonData = new Dictionary<string, object>
            {
                {"session", User.Current.SessionToken},
                {"score", Newtonsoft.Json.JsonConvert.SerializeObject(score)}
            };

            var request = GetWww(jsonData, url);

            StartCoroutine(CR_ProcessAsync(request, dataResult =>
            {
                switch (request.Response.DataAsText)
                {
                    case "true":
                        // Worked. 
                        break;
                    case "false":
                        throw new ApiException("Score can only be numeric.");
                    default:
                        throw new ApiException("Post score failed.");
                }

                return score;
            }, callback));

#endif
		}
		catch (Exception e)
		{
			if (callback != null)
				callback(score, e);
			else throw;
		}
	}


#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     Sets the session Cookies of id.net.
    /// </summary>
    public void SetSessionCookie(Action<User, Exception> callback = null)
    {
        try
        {
            string autologinUrl = UserDataUrl + "autologin?callback=idnet_autologin&app_id=";
            IdnetSetSessionCookie(AppId, autologinUrl, User.Current.SessionCookie);
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(null, e);
            else throw;
        }
    }


    /// <summary>
    ///     Wait for 1 sec before calling silent sponser login.
    ///     This is to make sure session-cookie has been set on id.net website before we use silent login.
    /// </summary>
    public void InvokeSilentSponser()
    {
        Invoke("SilentSponserLogin", 1.2f);
    }


    /// <summary>
    ///     Silently login on sponser's websites.
    /// </summary>
    public void SilentSponserLogin()
    {
        try
        {
            // IdnetHelpers.silentLogin();
            IdnetSilentSponserLogin();
        }
        catch
        {
            Debug.Log("silent sponser-login failed.");
        }
    }
#endif


    /// <summary>
    ///     GameBreak.
    /// </summary>
    public void GameBreak(bool? allowGamePause = false)
	{
        if (Screen.fullScreen)
        {
		    Debug.Log("Game is on fullscreen, ad can't be shown");
            return;
        }

#if UNITY_EDITOR
		Debug.Log("GameBreak();");
		SetAdsPlaceholder("true");
		try
		{
			AudioListener.volume = 0;

			if (allowGamePause == true)
			{
				Time.timeScale = 0;
			}
		}
		catch
		{
			Debug.Log("failed to time scale.");
		}
#elif UNITY_WEBGL
        if (ShowRealAdvert)
        {
            try
            {
                if (allowGamePause == true)
                {
                    IdnetY8ExternEval("true");
                }
                else
                {
                    IdnetY8ExternEval("false");
                }
            }
            catch
            {
                Debug.Log("Y8ExternEval failed.");
            }
        }
        else
        {
            Debug.Log("GameBreak();");

			string lowestDomain = string.Empty;
			try
			{
				string absoluteUrl = Application.absoluteURL;
				if (!string.IsNullOrEmpty(absoluteUrl))
				{

					lowestDomain = absoluteUrl.Replace("http://", "").Replace("https://", "").Replace("file://", "");

					if (!string.IsNullOrEmpty(lowestDomain))
						lowestDomain = lowestDomain.Substring(0, lowestDomain.IndexOf("/", StringComparison.Ordinal));
				}
			}
			catch
            {
				lowestDomain = string.Empty;
                Debug.Log("failed to parse absilute url.");
            }

            if(lowestDomain.Contains("y8.com"))
            {
                try
                {
                    AudioListener.volume = 0;

                    if (allowGamePause == true)
                    {
                        Time.timeScale = 0;
                    }

                    ShowPlaceholderAd();

                }
                catch
                {
                    Debug.Log("failed to time scale.");
                }
            }
            else
            {
                try
                {
                    AudioListener.volume = 0;

                    if (allowGamePause == true)
                    {
                        Time.timeScale = 0;
                    }
                }
                catch
                {
                    Debug.Log("failed to time scale.");
                }

                SetAdsPlaceholder("true");
            }
        }

#else
        Debug.Log("GameBreak();");
#endif
    }

    /// <summary>
    ///     SetAdsPlaceholder.
    /// </summary>
    /// <param name="enable"></param>
    public void SetAdsPlaceholder(string enable)
	{
		try
		{
			Gui.AdsPlaceholder.SetActive((bool.Parse(enable)));
			if (enable.Equals("false"))
			{
				Time.timeScale = 1;
                AudioListener.volume = 1;
            }
		}
		catch
		{
			Debug.Log("AdsPlaceholder failed.");
		}
	}

#if !UNITY_EDITOR && UNITY_WEBGL
    private void ForceToShowRealAd(string enable)
    {
        try
        {
            ShowRealAdvert = bool.Parse(enable);
        }
        catch
        {
            Debug.Log("ForceToShowRealAd failed.");
        }
    }
#endif


    /// <summary>
    ///     Attempts to login automatically using https://account.y8.com cookies.
    ///     Success if:
    ///     -User is logged in to https://account.y8.com on the same browser that they are playing the game on.
    ///     Failure (exception) if:
    ///     -Not logged in at https://account.y8.com.
    ///     -Playing in the editor.
    ///     -Playing on mobile.
    /// </summary>
    public void AutoLogin(Action<User, Exception> callback = null, bool? displayAutoLoginWindow = null)
    {
        try
        {
#if UNITY_EDITOR
            throw new NotSupportedException("AutoLogin does not work in the editor!");
#else
#if !UNITY_WEBGL
            throw new NotSupportedException("AutoLogin only works on WebGL.");
#endif
            StartCoroutine(CR_AutoLoginTimeout());

            ClosedAutoLogin = callback;

            if (displayAutoLoginWindow != null)
            {
                DisplayAutoLoginWindow = (bool) displayAutoLoginWindow;
            }

            if (DisplayAutoLoginWindow)
                Gui.Cfsm.SetState<IdnetGui.AutoLoginState>();

            // WebGL cannot use WWW to retrieve cookie data.
            // Use ExternalEval() with a timeout.

            string autologinUrl = UserDataUrl + "autologin?callback=idnet_autologin&app_id=";
            IdnetAutoLogin(AppId, autologinUrl);
#endif
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(null, e);
            else throw;
        }
    }


    /// <summary>
    ///     Shows the profile screen if logged in.
    /// </summary>
    /// <param name="callback"></param>
    public void Profile(Action<Exception> callback = null)
    {
        try
        {
            RequireUserLoggedIn();

            ClosedProfile = callback;
            Gui.Cfsm.SetState<IdnetGui.ProfileState>();
        }
        catch (Exception e)
        {
            if (callback != null)
                callback(e);
            else throw;
        }
    }

    private const string IDNET = "IDNET(Idnet.cs)";


    /// <summary>
    ///     AutoLogin UNITY_WEBGL response.
    /// </summary>
    /// <param name="response"></param>
    private void AutoLoginResult(string response)
    {
        try
        {
            AutoLoginRequestCallback(
                AutoLoginParser(Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(response)),
                null);
        }
        catch (Exception e)
        {
            AutoLoginRequestCallback(null, e);
        }
    }

    /// <summary>
    ///     SetTimeScale.
    /// </summary>
    /// <param name="response"></param>
    private void SetTimeScale(string response)
    {
        try
        {
            Time.timeScale = int.Parse(response);
        }
        catch
        {
            Debug.Log("Time Scaling failed.");
        }
    }

    /// <summary>
    ///     SetAudio.
    /// </summary>
    /// <param name="response"></param>
    private void SetAudio(string response)
    {
        try
        {
            AudioListener.volume = int.Parse(response);
        }
        catch
        {
            Debug.Log("Audio Sound variation failed.");
        }
    }

    /// <summary>
    ///     SetY8ToIDnetBanner.
    /// </summary>
    /// <param name="response"></param>
    private void SetY8ToIDnetBanner(string response)
    {
        try
        {
            EnableY8ToIDnetBanner = bool.Parse(response);
        }
        catch
        {
            Debug.Log("SetY8ToIDnetBanner failed.");
            EnableY8ToIDnetBanner = false;
        }
    }

#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     GetY8ToIDnetBannerStatus.
    /// </summary>
    private void GetY8ToIDnetBannerStatus()
    {
        try
        {
            IdnetGetY8ToIDnetBannerStatus();
        }
        catch
        {
            Debug.Log("GetY8ToIDnetBannerStatus failed.");
        }
    }

    /// <summary>
    ///     Initialise Extern Eval.
    /// </summary>
    private void InitExternEval()
    {
        try
        {
            IdnetInitExternEval(AppId);
        }
        catch
        {
            Debug.Log("InitExternEval failed.");
        }
    }

    /// <summary>
    ///     Ads placeholder with javascript.
    /// </summary>
    private void ShowPlaceholderAd()
    {
        try
        {
            ShowAdsPlaceholder();
        }
        catch
        {
            Debug.Log("ShowPlaceholderAd failed.");
        }
    }

    public void EnableExclusiveMenu()
    {
        Gui.ExclusiveMenu.SetActive(true);
        Crash(0);
    }

    // crash unity to prevent redirection hack
    private void Crash(int i)
    {
        i = i + 1;
        Debug.LogError("Dear developer, please test the game on storage.y8.com " + i);
        Crash(i++);
    }

    /// <summary>
    ///     Lock webgl games to y8 domains only, prevent stealing of webgl files.
    /// </summary>
    private void LockDomains()
    {
        try
        {
            bool isAllowed = false;
            string lowestDomain = string.Empty;
            string absoluteUrl = Application.absoluteURL;
            if (!string.IsNullOrEmpty(absoluteUrl))
            {

                lowestDomain = absoluteUrl.Replace("http://", "").Replace("https://", "").Replace("file://", "");

                if (!string.IsNullOrEmpty(lowestDomain))
                    lowestDomain = lowestDomain.Substring(0, lowestDomain.IndexOf("/", StringComparison.Ordinal));
            }

            if (!string.IsNullOrEmpty(lowestDomain))
            {
                lowestDomain = lowestDomain.ToLower();
                string stringToCheck = lowestDomain;
                string[] allowedDomainsArray = { ".y8.com", ".pog.com", ".gamepost.com", ".dollmania.com", ".4fun.com",
                    ".ready2play.net", ".wgcloud.cz", ".videotime.com", ".id.net"};
                string[] localhostArray = { "localhost" };

                foreach (string x in allowedDomainsArray)
                {
                    if (stringToCheck.Contains(x))
                    {
                        isAllowed = true;
                        break;
                    }
                }

                if (EnableLocalhostTest)
                {
                    foreach (string x in localhostArray)
                    {
                        if (stringToCheck.Contains(x))
                        {
                            isAllowed = true;
                            break;
                        }
                    }
                }
            }
            else
            {
                if (EnableLocalhostTest)
                {
                    isAllowed = true;
                }
            }

            if (!isAllowed)
            {
                EnableExclusiveMenu();
            }
        }
        catch
        {
            Debug.Log("LockDomain failed.");
        }
    }

    /// <summary>
    ///     Block blacklisted sites hardcodedly, prevent stealing of webgl files.
    /// </summary>
    private void PreventStealing()
    {
        try
        {
            string lowestDomain = string.Empty;
            string absoluteUrl = Application.absoluteURL;
            if (!string.IsNullOrEmpty(absoluteUrl))
            {

                lowestDomain = absoluteUrl.Replace("http://", "").Replace("https://", "").Replace("file://", "");

                if (!string.IsNullOrEmpty(lowestDomain))
                    lowestDomain = lowestDomain.Substring(0, lowestDomain.IndexOf("/", StringComparison.Ordinal));
            }

            if (!string.IsNullOrEmpty(lowestDomain))
            {
                lowestDomain = lowestDomain.ToLower();
                string stringToCheck = lowestDomain;
                string[] hackingSitesArray = { "g55", "kbhgames", "vseigru", "49644913", "4399", "7k7k", "i-gamer", "itch", "facebook" };

                foreach (string x in hackingSitesArray)
                {
                    if (stringToCheck.Contains(x))
                    {
                        EnableExclusiveMenu();
                    }
                }
            }
        }
        catch
        {
            Debug.Log("PreventStealing failed.");
        }
    }
#endif


    /// <summary>
    ///     Called after WWW request returns or after timeout.
    /// </summary>
    /// <param name="user"></param>
    /// <param name="exception"></param>
    private void AutoLoginRequestCallback(User user, Exception exception)
    {
        //silent sponser login after autologin is successfull.
#if !UNITY_EDITOR && UNITY_WEBGL
        I.InvokeSilentSponser();
        I.GetY8ToIDnetBannerStatus();
#endif

        // Ignore the callback if we've cancelled (closed the gui).
        if (ClosedAutoLogin == null)
            return;

        AutoLoginException = exception;

        // Update Gui.
        if (exception != null)
        {
            var serverException = exception as ServerException;

            if (serverException != null)
                Gui.ServerException(serverException);
            else
                Gui.ApiException(exception);
        }
        else
        {
            Gui.StatusTextColor = Color.grey;
            Gui.StatusText = "Logged in!";
        }

        if (Gui.Cfsm.ActiveType == typeof(IdnetGui.AutoLoginState))
        {
            Gui.Cfsm.SetState<IdnetGui.ClosingState>();
        }
        else
        {
            CloseAutoLoginGui();
        }
    }

    private IEnumerator CR_AutoLoginTimeout()
    {
        yield return new WaitForSeconds(AutoLoginTimeoutSeconds);

        // If we're still in this state...
        if (Gui.Cfsm.ActiveType == typeof(IdnetGui.AutoLoginState))
        {
            AutoLoginRequestCallback(null, new TimeoutException("AutoLogin timed out."));
        }
    }

    private User AutoLoginParser(Dictionary<string, object> jsonData)
    {
        var autoLoginUser = SetUser(jsonData);

        // Only set the current user to the autoLoginUser if autoLoginUser has found a profile.
        if (User.LoggedIn(autoLoginUser))
        {
            User.Current = SetUser(jsonData);
            User.Current.SaveLocally();
        }

        return User.Current;
    }

    /// <summary>
    ///     Returns true if IDNET Gui is being displayed on screen.
    /// </summary>
    public bool IsShowingGui()
    {
        return Gui.useGUILayout;
    }

    /// <summary>
    ///     Processer for all async actions.
    /// </summary>
    /// <typeparam name="T">Type to return.</typeparam>
    /// <param name="www">Request.</param>
    /// <param name="parser">Func used to [de]serialize the values.</param>
    /// <param name="callback">Called when async operation completes or fails.</param>
    /// <returns></returns>
    public IEnumerator CR_ProcessAsync<T>(WWW www, Func<Dictionary<string, object>, T> parser,
        Action<T, Exception> callback)
    {
        //Block IDnet api calls if api count is above 150 in last 5 minutes.
        if (ApiCallsCounter >= 150)
        {
            CheckApiCount();
            Gui.Interactable = true;
            I.CloseGui();
            yield break;
        }

        // Wait for WWW.
        yield return www;

        Exception finalException = null;
        var value = default(T);

        try
        {
            // Increment ApiCount
            AddApiCount();

            // WWW (connectivity) error handling.
            if (!string.IsNullOrEmpty(www.error))
                throw new ServerException(www.error);

            // Get dictionary values.
            var data = ParseData<T>(www.text);

            // Debugging.
#if IDNET_DEBUG
            Debug.Log(IDNET + "CR_ProcessAsync - Success WWW Data: " + www.text + "\n\n" +
                      www.url + "\n\n" +
                      data.Aggregate("Parse Data:\n",
                          (current, next) => current + next.Key + " : " + next.Value + "\n"));
#endif

            // API (parsing && user) error handling.
            if (data.ContainsKey("error"))
            {
                if (data.ContainsKey("key"))
                    value = parser(data);
                throw new ApiException(data["error"].ToString());
            }

            // Handle lots of errors.
            if (data.ContainsKey("errors"))
            {
                var errors =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object[]>>(
                        data["errors"].ToString());
                foreach (var error in errors)
                {
                    throw new ApiException(error.Key + " " + error.Value[0]);
                }
            }

            value = parser(data);
        }
        catch (ServerException serverException)
        {
            // Debug.Log(IDNET + serverException);

            finalException = serverException;
            Gui.ServerException(serverException);
        }
        catch (Exception exception)
        {
            // Debug.Log(IDNET + exception);

            finalException = exception;
            Gui.ApiException(exception);
        }

        // Send value (and possibly exception) back.
        if (callback != null)
            callback(value, finalException);

        // Or throw exception to make sure developers know it's failing.
        else if (finalException != null)
            throw finalException;
    }


    /// <summary>
    ///     Processer for all async actions.
    /// </summary>
    /// <typeparam name="T">Type to return.</typeparam>
    /// <param name="www">Request.</param>
    /// <param name="parser">Func used to [de]serialize the values.</param>
    /// <param name="callback">Called when async operation completes or fails.</param>
    /// <param name="waitForReferrer">Wait until we get RefererDomain.</param>
    /// <returns></returns>
    public IEnumerator CR_ProcessAsync<T>(WWW www, Func<Dictionary<string, object>, T> parser,
        Action<Exception> callback, bool waitForReferrer)
    {
        //Block IDnet api calls if api count is above 150 in last 5 minutes.
        if (ApiCallsCounter >= 150)
        {
            CheckApiCount();
            Gui.Interactable = true;
            I.CloseGui();
            yield break;
        }

        // Wait for WWW.
        yield return www;

#if !UNITY_EDITOR && UNITY_WEBGL
        if (waitForReferrer)
        {
            while (string.IsNullOrEmpty(I.RefererDomain))
                yield return null;
        }
#else
        I.RefererDomain = I.DomainName = "Unity_Editor";
#endif


        Exception finalException = null;

        try
        {
            // Increment ApiCount
            AddApiCount();

            // WWW (connectivity) error handling.
            if (!string.IsNullOrEmpty(www.error))
                throw new ServerException(www.error);

            // Get dictionary values.
            var data = ParseData<T>(www.text);

            // Debugging.
#if UNITY_EDITOR || IDNET_DEBUG
            if (!waitForReferrer)
            {
                Debug.Log(IDNET + "CR_ProcessAsync - Success WWW Data: " + www.text + "\n\n" +
                          www.url + "\n\n" +
                          data.Aggregate("Parse Data:\n",
                              (current, next) => current + next.Key + " : " + next.Value + "\n"));
            }
#endif

            // API (parsing && user) error handling.
            if (data.ContainsKey("error"))
            {
                throw new ApiException(data["error"].ToString());
            }

            // Handle lots of errors.
            if (data.ContainsKey("errors"))
            {
                var errors =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object[]>>(
                        data["errors"].ToString());
                foreach (var error in errors)
                {
                    throw new ApiException(error.Key + " " + error.Value[0]);
                }
            }

            parser(data);
        }
        catch (ServerException serverException)
        {
            Debug.Log(IDNET + serverException);

            finalException = serverException;
            Gui.ServerException(serverException);
        }
        catch (Exception exception)
        {
            Debug.Log(IDNET + exception);

            finalException = exception;
            Gui.ApiException(exception);
        }

        // Send value (and possibly exception) back.
        if (callback != null)
            callback(finalException);

        // Or throw exception to make sure developers know it's failing.
        else if (finalException != null)
            throw finalException;
    }

    /// <summary>
    ///     Parses out the user after a login/register/autologin.
    /// </summary>
    /// <param name="jsonData"></param>
    /// <returns></returns>
    private User SetUser(Dictionary<string, object> jsonData)
    {
        // Login uses "user".
        // Register uses "data".
        var user = Newtonsoft.Json.JsonConvert.DeserializeObject<User>(
            jsonData[jsonData.ContainsKey("user") ? "user" : "data"].ToString());

        if (jsonData.ContainsKey("sessionKey"))
        {
            user.SessionToken = jsonData["sessionKey"].ToString();
        }

        if (jsonData.ContainsKey("sessionCookie"))
        {
            user.SessionCookie = jsonData["sessionCookie"].ToString();
        }

        if (jsonData.ContainsKey("meta"))
        {
            if (jsonData["meta"] != null)
            {
                var meta = Newtonsoft.Json.JsonConvert.DeserializeObject<UserMeta>(jsonData["meta"].ToString());

                if (!string.IsNullOrEmpty(meta.dateOfBirth))
                {
                    string[] str = meta.dateOfBirth.Split('-');
                    if (str.Length >= 2)
                    {
                        user.BirthYear = str[0];
                        user.BirthMonth = str[1];
                    }
                }

                if (!string.IsNullOrEmpty(meta.gender))
                    user.Gender = meta.gender;

                if (!string.IsNullOrEmpty(meta.firstName))
                    user.Firstname = meta.firstName;
                else
                    user.Firstname = User.Current.Firstname;
            }
        }

        return user;
    }

    /// <summary>
    ///     Server callback for logging in/registering.
    /// </summary>
    /// <param name="user"></param>
    /// <param name="exception"></param>
    /// <param name="callback"></param>
    private void LoginAndRegisterGuiCallback(User user, Exception exception, Action<User, Exception> callback)
    {
        if (ClosedLoginRegister == null) return;

        LoginRegisterException = exception;

        // If there is an exception, return.
        // GUI already knows.
        if (exception != null)
        {
            if (Gui.Cfsm.ActiveType == typeof(IdnetGui.RegisterState))
            {
                int currentFlowState = 0;

                if ((exception.Message.Contains("nickname") || exception.Message.Contains("firstname")))
                {
                    currentFlowState = 0;
                }
                else if ((exception.Message.Contains("email") || exception.Message.Contains("password")))
                {
                    currentFlowState = 1;
                }
                else if ((exception.Message.Contains("gender")))
                {
                    currentFlowState = 2;
                }
                else if ((exception.Message.Contains("birth")))
                {
                    currentFlowState = 3;
                }

                Gui.SetCurrentFlowState(currentFlowState, exception);
            }

            if (Gui.Cfsm.ActiveType == typeof(IdnetGui.LoginState))
            {
                Gui.SetCurrentFlowState(0, exception);
            }

            return;
        }

        User.Current = user;

        // The only time a user will not have a session key is if they just registered.
        if (!User.LoggedIn())
        {
            // Login.
            // We can just call Login ourselves.
            Login(User.Current.Email, Gui.PasswordField, (user1, exception1) =>
                LoginAndRegisterGuiCallback(user1, exception1, callback));
        }
        // Else, we want to auto-close the GUI if the user has logged in.
        else
        {
            // Calling Tracker API when registration gets sucessfull "reg"
#if !UNITY_EDITOR && UNITY_WEBGL
            if (Gui.Cfsm.ActiveType == typeof (IdnetGui.RegisterState))
            {
                I.StartCoroutine(IdnetTracker.CR_SendTrackerStats("reg"));
                I.SetSessionCookie();
                I.InvokeSilentSponser();
            }
            if (Gui.Cfsm.ActiveType == typeof (IdnetGui.LoginState))
            {
                I.SetSessionCookie();
                I.InvokeSilentSponser();
            }
#endif

            // unlock locally saved achievements when user login/registers.
            if (GuestAchievements.Count > 0)
            {
                foreach (var achievement in GuestAchievements)
                {
                    var au = new AchievementUnlocker(achievement.Key, achievement.Value);
                    I.UnlockAchievement(au);
                }

                GuestAchievements.Clear();
            }

            // Set the user to the one sent back by the server.
            Gui.StatusTextColor = Color.grey;
            Gui.StatusText = "Login successful.";
            Gui.Interactable = true;

            // Close the GUI using the callback to call the developers custom callback.
            CloseLoginRegisterGui();
            Gui.Cfsm.SetState<IdnetGui.ClosingState>();
        }
    }

    /// <summary>
    ///     Throws a <see cref="InvalidOperationException" /> if:
    ///     -The operation requires the user to be logged in but they are not.
    ///     -The operation requires the user to be logged out but they are not.
    /// </summary>
    public void RequireUserLoggedIn(bool yes = true)
    {
        // If user is null equals required to log in, throw exception.
        if (User.LoggedIn() != yes)
            throw new InvalidOperationException("Operation requires user to be " +
                                                (yes ? "Logged in" : "Logged out") +
                                                ".");
    }

    /// <summary>
    ///     Throws a <see cref="InvalidOperationException" /> if:
    ///     -The AppId is null or empty.
    /// </summary>
    public void RequireAppId()
    {
        if (string.IsNullOrEmpty(AppId))
            throw new InvalidOperationException(
                "AppId is null or empty,please Initialise Idnet with your App-ID in Awake()");
    }

    /// <summary>
    ///     Helper function to remove the quotes from the string object.
    ///     Required because the server returns quotes around strings.
    /// </summary>
    /// <param name="stringWithQuotes"></param>
    /// <returns></returns>
    public static string RemoveQuotes(string stringWithQuotes)
    {
        // If the string does not start with any of the the quote chars,
        // Safe to assume it's not wrapped in quotes.
        var startChar = stringWithQuotes[0];
        if (startChar == 34)
            return stringWithQuotes.Substring(1, stringWithQuotes.Length - 2);

        return stringWithQuotes;
    }

    /// <summary>
    ///     Helper function to add quotes to the string object.
    /// </summary>
    /// <param name="stringWithoutQuotes"></param>
    /// <returns></returns>
    public static string AddQuotes(string stringWithoutQuotes)
    {
        const string quote = "\"";

        var stringBuilder = new StringBuilder(stringWithoutQuotes);

        if (!stringWithoutQuotes.StartsWith(quote))
            stringBuilder.Insert(0, quote);

        if (!stringWithoutQuotes.EndsWith(quote))
            stringBuilder.Append(quote);

        return stringBuilder.ToString();
    }

    /// <summary>
    ///     Returns whether or not a field is safe to be passed into the url.
    ///     Use this on any string that the user is allowed to input, that will be sent as data.
    /// </summary>
    /// <param name="fieldValue">String value to check.</param>
    /// <returns>True if safe, false if not safe.</returns>
    public bool IsValidFieldString(string fieldValue)
    {
        if (string.IsNullOrEmpty(fieldValue))
            return false;

        var r = new Regex("^[a-zA-Z0-9]*$");

        return r.IsMatch(fieldValue);
    }

    /// <summary>
    ///     Closes the Gui.
    /// </summary>
    public void CloseGui()
    {
        CloseLoginRegisterGui();
        CloseProfileGui();
        CloseAutoLoginGui();
        CloseLeaderboardGui();
        CloseListAchievementsGui();
        CloseUserLevelsGui();

        if (Gui.Cfsm.ActiveType != typeof(IdnetGui.ClosedState))
            Gui.Cfsm.SetState<IdnetGui.ClosedState>();
    }

    /// <summary>
    ///     If we are attempting to login or register, calling this will cancel that attempt.
    /// </summary>
    private void CloseLoginRegisterGui()
    {
        if (ClosedLoginRegister == null)
            return;

        ClosedLoginRegister(User.Current, LoginRegisterException);

        ClosedLoginRegister = null;
    }

    /// <summary>
    ///     If we are attempting to login or register, calling this will cancel that attempt.
    /// </summary>
    private void CloseProfileGui()
    {
        if (ClosedProfile == null)
            return;

        ClosedProfile(ProfileException);

        ClosedProfile = null;
    }

    /// <summary>
    ///     If we are attempting to AutoLogin, calling this will cancel that attempt.
    /// </summary>
    private void CloseAutoLoginGui()
    {
        if (ClosedAutoLogin == null)
            return;

        ClosedAutoLogin(User.Current, AutoLoginException);

        ClosedAutoLogin = null;
    }

    /// <summary>
    ///     If we are attempting to AutoLogin, calling this will cancel that attempt.
    /// </summary>
    private void CloseListAchievementsGui()
    {
        if (ClosedListAchievements == null)
            return;

        ClosedListAchievements(ListAchievementsException);

        ClosedListAchievements = null;
    }

    /// <summary>
    ///     If we are attempting to AutoLogin, calling this will cancel that attempt.
    /// </summary>
    private void CloseUserLevelsGui()
    {
        if (ClosedUserLevels == null)
            return;

        ClosedUserLevels(UserLevelsException);

        ClosedUserLevels = null;
    }

    /// <summary>
    ///     If we are showing the leaderboard, calling this will close it.
    /// </summary>
    public void CloseLeaderboardGui()
    {
        if (ClosedLeaderboard == null)
            return;

        ClosedLeaderboard(Highscores, LeaderboardException);
        ClosedLeaderboard = null;
        Gui.Cfsm.SetState<IdnetGui.ClosedState>();
    }

#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     Get Navigator useragent.
    /// </summary>
    public void GetUserAgent()
    {
        try
        {
            IdnetGetUserAgent();
        }
        catch
        {
        }
    }
#endif

#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     Get the host name,on which game is running,like "static2.y8.com" and not the unity's absolute url that provide the
    ///     full url of game.
    /// </summary>
    public void GetRefererDomain()
    {
        try
        {
            IdnetGetRefererDomain();
        }
        catch
        {
        }
    }
#endif

#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     Get the operating system type.
    /// </summary>
    public void GetOSDetails()
    {
        try
        {
            IdnetGetOSDetails();
        }
        catch
        {
        }
    }
#endif

#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     js console logs.
    /// </summary>
    public void Log(string log)
    {
        try
        {
            IdnetLogs(log);
        }
        catch
        {
            Debug.Log("js console logs failed.");
        }
    }
#endif


    /// <summary>
    ///     RefererDomain is Host,on which game is running,the full top parent domain.
    /// </summary>
    public string RefererDomain { get; set; }

    /// <summary>
    ///     Domain Url is Host,on which game is running,like "y8.com" and not the unity's absolute url that provide the full
    ///     url of game.
    /// </summary>
    public string DomainName { get; set; }

    /// <summary>
    ///     UserAgent,used by tracker.
    /// </summary>
    public string UserAgent { get; set; }

    /// <summary>
    ///     Referrer Domain callback.
    /// </summary>
    /// <param name="response"></param>
    public string RefererDomainCallback(string response)
    {
        try
        {
            RefererDomain = response;
            // Remove http:// 
            DomainName = response.Replace("http://", "").Replace("https://", "").Replace("file://", "");

            // Get substring. (like media2.y8.com and not the full url)
			if(!string.IsNullOrEmpty(DomainName))
                DomainName = DomainName.Substring(0, DomainName.IndexOf("/", StringComparison.Ordinal));

            return RefererDomain;
        }
        catch (Exception e)
        {
            Debug.Log(this + " exception, failed to Get DomainURL " + e);
            Debug.LogException(e);
            return "";
        }
    }

    /// <summary>
    ///     Operating System type,either 64bit or 32bit
    /// </summary>
    public string OSType { get; set; }

    /// <summary>
    ///     Detect user's Operating type.
    /// </summary>
    /// <param name="response"></param>
    public string OSDetailsCallback(string response)
    {
        try
        {
            OSType = response;
            if (!string.IsNullOrEmpty(OSType))
                IdnetTracker.CustomEvent("OS", OSType);

            return OSType;
        }
        catch (Exception e)
        {
            Debug.Log(this + " exception, failed to Get OS " + e);
            Debug.LogException(e);
            return "";
        }
    }

    /// <summary>
    ///     UserAgent UNITY_WEBGL response.
    /// </summary>
    /// <param name="response"></param>
    public void UserAgentCallback(string response)
    {
        try
        {
            UserAgent = response;
        }
        catch (Exception e)
        {
            Debug.Log(this + " exception, failed to Get UserAgent " + e);
        }
    }


    /// <summary>
    ///     Reset logged-in User details.
    /// </summary>
    public void ResetUser()
    {
        Gui.PasswordField = "";

        if (User.LoggedIn())
        {
            User.Current.DeleteLocally();       
            User.Current.SessionToken = "";
            User.Current.SessionCookie = "";
            User.Current.Nickname = "";
            User.Current.Email = "";
            User.Current.Pid = "";
            User.Current.BirthMonth = "";
            User.Current.BirthYear = "";
            User.Current.Gender = "";
            User.Current.Firstname = "";
            User.Current.CurrentScore = 0;
            User.Current.BestScore = 0;
            User.Current.IdnetPoints = 0;
            User.Current.SaveLocally();
        }
    }

    /// <summary>
    ///     DOM Injector WebGL.
    /// </summary>
    public void OpenLink(string url)
    {
        if (url.Contains("account.y8.com"))
        {
            OpenUrls.OpenLink(url);
            //Track OBL
            IdnetTracker.CustomEvent("Y8Account_OBL", DomainName);
        }
        else if (url.Contains("y8"))
        {
            OpenUrls.OpenLink(url);
            //Track OBL
            IdnetTracker.CustomEvent("Y8_OBL", DomainName);
        }
        else if (url.Contains("play.google.com"))
        {
            OpenUrls.OpenLink(url);
            //Track OBL
            IdnetTracker.CustomEvent("GooglePlay_OBL", DomainName);
        }
        else if (url.Contains("itunes.apple.com"))
        {
            OpenUrls.OpenLink(url);
            //Track OBL
            IdnetTracker.CustomEvent("iTunes_OBL", DomainName);
        }
        else
        {
            //force outbound links to y8 to prevent link redirect hacks
            url = url.Replace(url, "http://www.y8.com");

            OpenUrls.OpenLink(url);
            //Track OBL
            IdnetTracker.CustomEvent("Y8_OBL", DomainName);
        }
    }

#if !UNITY_EDITOR && UNITY_WEBGL
    /// <summary>
    ///     Referrer Domain,if its not yet retrieved.
    /// </summary>
    public void GetDomain()
    {
        if (string.IsNullOrEmpty(I.RefererDomain))
        {
            I.GetRefererDomain();
        }
        I.InitExternEval();
    }
#endif

    /// <summary>
    ///     Adds the API count.
    /// </summary>
    public void AddApiCount()
    {
        _apiCountTimerList.Add(Time.time);
        foreach (var time in _apiCountTimerList.ToList())
        {
            if (Time.time - time > 300)
            {
                _apiCountTimerList.Remove(time);
            }
        }

        ApiCallsCounter = _apiCountTimerList.Count;
        if (ApiCallsCounter >= 85)
        {
            Debug.LogWarning("Warning: Too many IDnet Api calls in last 5 minutes(over 75),please reduce them.");
        }
    }

    /// <summary>
    ///     Checks the API count.
    /// </summary>
    public void CheckApiCount()
    {
        foreach (var time in _apiCountTimerList.ToList())
        {
            if (Time.time - time > 300)
            {
                _apiCountTimerList.Remove(time);
            }
        }

        ApiCallsCounter = _apiCountTimerList.Count;
        Debug.LogError("Alert: Too many api calls in last 5 min(over 150),so blocking this Api call.");
    }


    /// <summary>
    ///     Returns a formatted WWW class with the data serialized.
    /// </summary>
    /// <param name="jsonData">Data to send.</param>
    /// <param name="url">Url to send the data to.</param>
    /// <returns>
    ///     <see cref="WWW" />
    /// </returns>
    public static WWW GetWww(Dictionary<string, object> jsonData, string url)
    {
#if IDNET_DEBUG
        Debug.Log(IDNET + "GetWww - WWW Data Success: " + url
                  + "\n\n" + jsonData.Aggregate("Parse Data:\n",
                      (current, next) => current + next.Key + " : " +
                                         (next.Key.Contains("password") ? "REDACTED" : next.Value) + "\n"));
#endif

        return new WWW(url, EncodeWwwData(jsonData), Headers);
    }

    /// <summary>
    ///     Encodes <see cref="WWW" /> data.
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    public static byte[] EncodeWwwData(Dictionary<string, object> data)
    {
        return Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(data));
    }

    /// <summary>
    ///     Handles the multitude of return data strings given by the server.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="text"></param>
    /// <returns></returns>
    private static Dictionary<string, object> ParseData<T>(string text)
    {
        // Expected format.
        Dictionary<string, object> data;

        // JSON.
        if (text.StartsWith("{"))
        {
            data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(text);
        }
        // AutoLogin returns idnet. 
        else if (text.StartsWith("idnet"))
        {
            data = ParseData<T>(text.Substring(16, text.Length - 17));
        }
        // PostHighscores returns just true, so handle that.
        else
        {
            // Attempt to deserialize the string as a single object.
            object deserializeObject;

            try
            {
                deserializeObject = Newtonsoft.Json.JsonConvert.DeserializeObject(text);
            }
            catch (Exception)
            {
                // If it fails, just leave it as string.
                deserializeObject = text;
            }

            data = new Dictionary<string, object>
            {
                {"status", deserializeObject}
            };
        }

        return data;
    }


#if IDNET_PLAYTOMIC

    private static PlaytomicException GeneratePlaytomicException(PResponse response)
    {
        PlaytomicException playtomicException = null;

        if (!response.success)
        {
            // submission failed because of response.errormessage with response.errorcode
            playtomicException =
                new PlaytomicException(response.errorcode + ": " + response.errormessage);
        }

        return playtomicException;
    }

#endif

    private void Start()
    {
        if (string.IsNullOrEmpty(AppId))
            throw new ApiException(
                IDNET + "App-Id is empty!" +
                "Make sure you initialize IDNET using the Initialize function.");
    }

    private void OnApplicationQuit()
    {
        User.Current.SaveLocally();
    }

    /// <summary>
    ///     Call before any other API functions.
    ///     You only need to call this once per game.
    ///     Each repeat call will be ignored.
    ///     You do not need to call this once per scene.
    ///     Playtomics will also be initialized here.
    ///     Playtomics Initialize is async.
    ///     Please wait until it is complete to use the Idnet API.
    /// </summary>
    /// <param name="appId">
    ///     App Unique Identifier.
    ///     Find yours here: https://account.y8.com/applications
    /// </param>
    /// <param name="showPreloaderAd">
    ///     Idnet has a preloader ad that it can display.
    ///     True = Display. It'll last 10 seconds.
    /// </param>
    public void Initialize(string appId, bool showPreloaderAd = false)
    {
        // Check for duplicates.
        if (!string.IsNullOrEmpty(AppId))
        {
            Debug.Log(IDNET + "App ID already initialized. Ignoring.");

            // If they are different, tell the developer because there could be bugs.
            if (AppId != appId)
            {
                throw new Exception(IDNET + "You have two or more App ID's!\n" +
                                    "Current (In use) App-Id: " + AppId + "\n" +
                                    "Other:   " + appId);
            }

            return;
        }

        // AsyncCache AppId.
        AppId = appId;

        gameObject.AddComponent<BrandingScript>();

        LeaderboardModeValue = LeaderboardMode.AllTime;
        LeaderboardPage = 1;
        AllowDuplicateLeaderboardResults = false;

        // Make sure we have correct headers.
        if (Headers.Count == 0)
        {
            Headers.Add("Content-Type", "application/json");

            PlaytomicHeaders.Add("Content-Type", "text/plain");
        }

        // Load User.
        if (User.Current == null)
        {
            User.Current = new User();
            User.Current.LoadLocally();
        }

        // Reset logged-in User details.
#if !UNITY_EDITOR
        ResetUser();
#endif

        Debug.Log(IDNET + "IsInitialized"
#if UNITY_EDITOR
            + " with AppId: " + appId
#endif
            + (!string.IsNullOrEmpty(User.Current.SessionToken) ? ".<color=cyan>"
            + " Logged in from cache, with User.Current = "
            + User.Current + "</color>. "
            + "<color=magenta> Use ('Shift' + 'L') key in editor play mode to logout.</color>" : string.Empty)
        );


#if UNITY_WEBGL && !UNITY_EDITOR
		// Calling Tracker "play" event when sdk is initialised
        Idnet.I.StartCoroutine (IdnetTracker.CR_SendTrackerStats ("play"));
		Application.runInBackground = true;
#endif

#if IDNET_PLAYTOMIC
        Playtomic.Initialize(AppId, PlayomicHostUrl, _i.gameObject);
#endif

        Idnet.I.GetCurrentYear((year, exception) =>
        {
            if (exception != null)
            {
                Idnet.I.CurrentYear = System.DateTime.UtcNow.Year;
            }

            Gui.SetDOBDropDowns();
        });

#if !UNITY_EDITOR && UNITY_WEBGL
        LockDomains();
        Invoke("PreventStealing", 6f);
#endif

        /*
        if(Application.absoluteURL.Contains("/y8-studio"))
        {
            ShowRealAdvert = true;
        }
        */

    }

    /// <summary>
    ///     Instance.
    ///     Will find or create it.
    ///     Singleton.
    /// </summary>
    public static Idnet I
    {
        get
        {
            if (_i == null)
            {
                _i = FindObjectOfType<Idnet>();

                if (_i == null)
                {
                    _i = Instantiate(Resources.Load<Idnet>("Idnet"));

                    if (_i == null)
                    {
                        throw new ApiException("Idnet Prefab not in a resources folder.");
                    }

                    DontDestroyOnLoad(_i.gameObject);

                    _i.name = IDNET;
                }
            }

            return _i;
        }
    }

    private static Idnet _i;
}