﻿using System;
using System.Collections.Specialized;
using System.IO;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Rendering;
using IniParser;
using IniParser.Model;
using IniParser.Parser;
using UnityEngine.Assertions;
using UnityEngine.Networking;

namespace BlackBox
{
    public enum Platform
    {
        Default, // What's set in the engine
        ForceEditor,
        ForceBuild,
    }

    public enum ConfigOption
    {
        Local,
        Web,
    }

    public class BlackBoxConfig
    {
        public static string BLACKBOX_CONFIG_FILE = "BlackBox.ini";
        public static string BLACKBOX_DIRECTORY = "BlackBox";
        public const string ANDROID_UUID_FILENAME = "device_id";

        private bool enableIssueReporter_ = true;

        private bool includeGameLogInIssueReport_ = true;

        private string namespace_ = string.Empty;

        private string apiKey_ = string.Empty;

        private string gameVersionId_ = string.Empty;

        private string buildId_ = string.Empty;

        private string baseUrl_ = string.Empty;

        private int fps_ = 30;

        private int kps_ = 30;

        private bool store_video_ = true;

        private int total_seconds_ = 30;

        private HotKey issueReporterKey_;

        private static BlackBoxConfig instance_;

        private BlackBoxConfigAsset configAsset_ = null;

        public BlackBoxConfig()
        {
            issueReporterKey_ = default;
        }

        public bool AutoInitialize
        {
            get { return configAsset_.AutoInitialize; }
            set { configAsset_.AutoInitialize = value; }
        }

        public bool EnableLogs
        {
            get { return configAsset_.EnableLogs; }
            set { configAsset_.EnableLogs = value; }
        }

        public string Namespace
        {
            get { return namespace_; }
            set { namespace_ = value; }
        }

        public string ApiKey
        {
            get { return apiKey_; }
            set { apiKey_ = value; }
        }

        public string GameVersionId
        {
            get { return gameVersionId_; }
            set { gameVersionId_ = value; }
        }

        public string BuildId
        {
            get { return buildId_; }
            set { buildId_ = value; }
        }

        public string BaseUrl
        {
            get { return baseUrl_; }
            set { baseUrl_ = value; }
        }

        public bool EnableBasicProfiling
        {
            get { return configAsset_.EnableBasicProfiling; }
            set { configAsset_.EnableBasicProfiling = value; }
        }

        public bool EnableCrashReporter
        {
            get { return configAsset_.EnableCrashReporter; }
            set { configAsset_.EnableCrashReporter = value; }
        }

#if UNITY_STANDALONE
        public bool EnableIssueReporter
        {
            get { return enableIssueReporter_; }
            set { enableIssueReporter_ = value; }
        }

        public bool IncludeGameLogInIssueReport
        {
            get { return includeGameLogInIssueReport_; }
            set { includeGameLogInIssueReport_ = value; }
        }
#endif

        public bool StoreHardwareInformation
        {
            get { return configAsset_.StoreHardwareInformation; }
            set { configAsset_.StoreHardwareInformation = value; }
        }

        public Platform Platform
        {
            get { return configAsset_.Platform; }
            set { configAsset_.Platform = value; }
        }

        public ConfigOption ConfigOption
        {
            get { return configAsset_.ConfigOption; }
            set { configAsset_.ConfigOption = value; }
        }

        public int Fps
        {
            get { return fps_; }
            set { fps_ = value; }
        }

        public int Kps
        {
            get { return kps_; }
            set { kps_ = value; }
        }

        public bool StoreVideo
        {
            get { return store_video_; }
            set { store_video_ = value; }
        }

        public int TotalSeconds
        {
            get { return total_seconds_; }
            set { total_seconds_ = value; }
        }

        public int GraphicsApi
        {
            get
            {
                return configAsset_.GraphicsApi;
            }
        }

        public int GraphicsDeviceType
        {
            get
            {
                return configAsset_.GraphicsDeviceType;
            }
        }

#if UNITY_STANDALONE
        public ref HotKey IssueReporterKey
        {
            get
            {
                return ref issueReporterKey_;
            }
        }
#endif
        public static BlackBoxConfig Instance
        {
            get
            {
                if (instance_ == null)
                {
                    Logger.Log("Create config instance");
                    instance_ = new BlackBoxConfig();
                    bool success = LoadBlackBoxConfig(instance_);
                    if (!success)
                    {
                        Debug.Log("Failed to load configuration");
                        instance_ = null;
                    }
                }
                return instance_;
            }
            set { instance_ = value; }
        }

#if UNITY_ANDROID && !UNITY_EDITOR
        public string ExtractAssetText(string path, System.Text.Encoding encoding = null)
        {
            // Extract asset text using java api as using UnityWebRequest may cause stuck if called from non main thread
            using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
            using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
            using (var assetManager = activity.Call<AndroidJavaObject>("getAssets"))
            using (var inputStream = assetManager.Call<AndroidJavaObject>("open", path))
            {
                IntPtr javaBuffer = AndroidJNI.NewByteArray(2048);
                IntPtr inputStreamPtr = inputStream.GetRawObject();
                IntPtr inputStreamClass = AndroidJNI.GetObjectClass(inputStreamPtr);
                IntPtr readMethodId = AndroidJNI.GetMethodID(inputStreamClass, "read", "([B)I");

                try
                {
                    using (var ms = new MemoryStream())
                    {
                        while (true)
                        {
                            int readBytes = AndroidJNI.CallIntMethod(inputStreamPtr, readMethodId, new jvalue[]
                            {
                                new jvalue() { l = javaBuffer }
                            });

                            if (readBytes == -1)
                            {
                                // End of file
                                break;
                            }

                            byte[] tmpBuffer = AndroidJNI.FromByteArray(javaBuffer);
                            ms.Write(tmpBuffer, 0, readBytes);
                        }

                        ms.Position = 0;
                        encoding ??= System.Text.Encoding.UTF8; // Default UTF-8
                        using (var reader = new StreamReader(ms, encoding, detectEncodingFromByteOrderMarks: true))
                        {
                            return reader.ReadToEnd();
                        }
                    }
                }
                finally
                {
                    AndroidJNI.DeleteLocalRef(javaBuffer);
                    AndroidJNI.DeleteLocalRef(inputStreamClass);

                    // Close Java InputStream
                    inputStream.Call("close");
                }
            }
        }
#endif
        public static bool LoadBlackBoxConfig(BlackBoxConfig config)
        {
            IniData configData;
            config.configAsset_ = BlackBoxConfigAsset.LoadBlackBoxConfigAsset();
#if UNITY_ANDROID && !UNITY_EDITOR
            Logger.Log($"Reading for android");
            var blackboxIniConfig = Path.Combine(BLACKBOX_DIRECTORY, BLACKBOX_CONFIG_FILE);

            string configStr = config.ExtractAssetText(blackboxIniConfig);
            if (configStr.Length <= 0)
            {
                Logger.Log($"Config Android not found");
                return false;
            }

            IniDataParser iniParser = new IniDataParser();
            configData = iniParser.Parse(configStr);
#else
            var blackboxIniConfig = Path.Combine(Application.streamingAssetsPath, BLACKBOX_DIRECTORY, BLACKBOX_CONFIG_FILE);
            Logger.Log($"Try read config from '{blackboxIniConfig}' file");
            if (File.Exists(blackboxIniConfig))
            {
                var parser = new FileIniDataParser();
                configData = parser.ReadFile(blackboxIniConfig);
            }
            else
            {
                return false;
            }
#endif

            // BlackBoxSettings
            config.Namespace = configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.NAMESPACE_FIELD];
            config.ApiKey = configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.API_KEY_FIELD];
            config.BuildId = configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.BUILD_ID_FIELD];
            config.GameVersionId = configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.GAME_VERSION_ID_FIELD];
            string baseUrl = configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.BASE_URL_FIELD];
            config.EnableBasicProfiling = bool.Parse(configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.ENABLE_BASIC_PROFILING]);
            config.EnableCrashReporter = bool.Parse(configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.ENABLE_CRASH_REPORTER]);
            config.StoreHardwareInformation = bool.Parse(configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.STORE_HARDWARE_INFO]);
            config.EnableLogs = bool.Parse(configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.ENABLE_LOGS]);
            config.AutoInitialize = bool.Parse(configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.IS_AUTO_INITIALIZE]);

#if UNITY_EDITOR
            // Read base url with priorities:
            // 1. BlackBox.ini
            // 2. <user_folder>/.blackbox/.config.ini
            // 3. Default url only if both config above empty
            if (String.IsNullOrEmpty(baseUrl) && String.IsNullOrEmpty(config.BaseUrl))
            {
                var blackboxCliUserConfig =
                    Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
                        ".blackbox", "config.ini");
                if (File.Exists(blackboxCliUserConfig))
                {
                    IniData configDataCli = new IniData();
                    var parser = new FileIniDataParser();
                    configDataCli = parser.ReadFile(blackboxCliUserConfig);

                    baseUrl = configDataCli["url"]["base_url"];

                    Logger.Log($"Read config from '{blackboxCliUserConfig}' file");
                }
            }
#endif
            if (!String.IsNullOrEmpty(baseUrl))
            {
                config.BaseUrl = baseUrl;
#if UNITY_EDITOR
                config.SaveConfigAsset();
#endif
            }
            else if (String.IsNullOrEmpty(config.BaseUrl))
            {
                config.BaseUrl = BlackBoxConfigAsset.DEFAULT_URL;
#if UNITY_EDITOR
                config.SaveConfigAsset();
#endif
            }

            Func<string, string, bool, bool> GetBoolConfigValue = (section, subsection, defaultValue) =>
            {
                string configString = configData[section][subsection];
                return configString is { Length: > 0 } ? (configString.ToUpper().Equals("TRUE") || configString.ToUpper().Equals("1")) : defaultValue;
            };
#if UNITY_STANDALONE_WIN
            config.EnableIssueReporter = GetBoolConfigValue(ConfigIniTemplate.BLACKBOX_SETTINGS, ConfigIniTemplate.ENABLE_ISSUE_REPORTER_FIELD, config.EnableIssueReporter);
            string issueReporterHotKey = configData[ConfigIniTemplate.BLACKBOX_SETTINGS][ConfigIniTemplate.ISSUE_REPORTER_KEY_FIELD];
            if (!String.IsNullOrEmpty(issueReporterHotKey))
            {
                config.IssueReporterKey = HotKey.Deserialize(issueReporterHotKey);
            }

            config.IncludeGameLogInIssueReport = GetBoolConfigValue(ConfigIniTemplate.BLACKBOX_SETTINGS,
                ConfigIniTemplate.INCLUDE_GAME_LOG_IN_ISSUE_REPORT_FIELD, config.IncludeGameLogInIssueReport);
#endif
            Func<string, string, int, int> GetIntConfigValue = (section, subsection, defaultValue) =>
            {
                string configString = configData[section][subsection];
                int result;
                bool success = Int32.TryParse(configString, out result);
                return success ? result : defaultValue;
            };

            config.Fps = GetIntConfigValue(ConfigIniTemplate.RECORDER_SETTINGS, ConfigIniTemplate.FPS_FIELD, config.Fps);
            config.Kps = GetIntConfigValue(ConfigIniTemplate.RECORDER_SETTINGS, ConfigIniTemplate.KPS_FIELD, config.Kps);
            config.StoreVideo = GetBoolConfigValue(ConfigIniTemplate.RECORDER_SETTINGS, ConfigIniTemplate.STORE_VIDEO_FIELD, config.StoreVideo);
            config.TotalSeconds = GetIntConfigValue(ConfigIniTemplate.RECORDER_SETTINGS, ConfigIniTemplate.KPS_FIELD, config.TotalSeconds);
            Logger.Log($"Read config from '{blackboxIniConfig}' file");
            return true;
        }

        public void SaveConfigAsset()
        {
            configAsset_.Save();
        }

        public void OnValidate()
        {
            Debug.Log("Validate Blackbox Config");

            const string BLACKBOX_CONFIG_ASSET = "BlackBoxConfigAsset";

            var isBlackboxConfigAssetExists = File.Exists(Path.Combine(Application.dataPath, "Resources", BLACKBOX_DIRECTORY, $"{BLACKBOX_CONFIG_ASSET}.asset"));

            if (!isBlackboxConfigAssetExists)
            {
                Logger.LogWarning("Blackbox Config Asset is not exists. Please fill the BlackBox Properties first!");
                return;
            }

            Assert.IsTrue(namespace_.IsSafeString(), $"BlackBox.ini contains invalid Namespace:{namespace_}");
            Assert.IsTrue(apiKey_.IsSafeString(), $"BlackBox.ini contains invalid ApiKey:{apiKey_}");
            Assert.IsTrue(gameVersionId_.IsSafeString(), $"BlackBox.ini contains invalid GameVersionId:{gameVersionId_}");
            Assert.IsTrue(buildId_.IsSafeString(), $"BlackBox.ini contains invalid BuildId:{buildId_}");
            Assert.IsTrue(BaseUrl.IsSafeString(), $"BlackBox.ini contains invalid BaseUrl:{BaseUrl}");
        }
    }
}