#region Header

// Copyright (c) 2021-2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.

#endregion

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Networking;
using UnityEngine.Rendering;
using System.Collections.Generic;
using System.Collections;

namespace BlackBox
{
    using File = System.IO.File;

    public interface ICycle
    {
        void Initialize();
        void StartSession();
        void EndSession();
        void Update(float dt, float cpu, float gpu, bool isIngameSession = true);
    }

    // These values are for the BlackBox Core
    public enum GraphicsApi
    {
        D3D11 = 2,
        NOAPI = 4,
        OPENGLES2 = 8,
        OPENGLES3 = 11,
        PS4 = 13,
        XBOXONE = 14,
        METAL = 16,
        OPENGLCORE = 17,
        D3D12 = 18,
        VULKAN = 21,
        SWITCH = 22,
        XBOXONED3D12 = 23,
    };

    public class PluginManager : ICycle
    {
        private static CommandBuffer commandBuffer_ = null;
        private static RenderTexture renderTexture_ = null;
        private const string SSLCertificate = "cacert.pem";
        private string deviceID = string.Empty;
        private static PluginAPI.GenerateDeviceIDCallback callbackDelegate;
        private const string CONFIG_TRUE = "1";
        private const string CONFIG_FALSE = "0";

        public void Initialize()
        {
            Logger.Log("SDK MODULE Startup Module");
            var logs = new StringBuilder();

            var gameName = Application.productName;
            var version = Application.unityVersion;
            var sdkVer = GetSDKVersion();
            var crashPath = GetCrashFolder();
            var helperPath = GetHelperPath();
            var issueReporterPath = GetIssueReporterPath();
            var iniPath = GetConfigPath();
            var logPath = GetLogPath();
            var gpuDeviceId = BlackBoxConfig.Instance.GraphicsApi;
            var gpuDeviceType = BlackBoxConfig.Instance.GraphicsDeviceType;
            bool isEditor;
            var helperAlternativeLogPath = GetHelperLogAlternativePath();
            (var gpu_name, var gpu_version) = GetGPUInfo();

            Logger.Log($"SDK_VERSION: {sdkVer}");
            Logger.Log($"CrashPath: {crashPath}");
            Logger.Log($"HelperPath: {helperPath}");
            Logger.Log($"IniPath: {iniPath}");
            Logger.Log($"LogPath: {logPath}");
            Logger.Log($"GPUDeviceId: {gpuDeviceId}");
            Logger.Log($"GPUDeviceType: {gpuDeviceType}");

            GatherAdditionalInfo();
            PluginAPI.blackbox_config_set_base_url(BlackBoxConfig.Instance.BaseUrl);

            var platform = BlackBoxConfig.Instance.Platform;

            // If platform is set to default, don't override and use what's the Application.isEditor provides
            if (platform == Platform.Default)
            {
                isEditor = Application.isEditor;
            }
            // Use the override value (for debugging purposes)
            else
            {
                isEditor = platform != Platform.ForceBuild;
            }

            #region Logger

            // Setup logs
            var brdigeDelegate = new PluginAPI.blackbox_log_bridge_delegate(PluginAPI.blackbox_log_callback);
            var pointerDelegate = Marshal.GetFunctionPointerForDelegate(brdigeDelegate);

            Logger.Log("Plugin api set log");
            PluginAPI.blackbox_set_log_function(pointerDelegate);

            var enableLogs = BlackBoxConfig.Instance.EnableLogs;
            Logger.Log($"BlackBoxConfig.Instance.EnableLogs:{enableLogs}");
            PluginAPI.blackbox_set_log_enabled(true);

            #endregion

            #region Command Buffer

            // Setup command buffer
            commandBuffer_ = new CommandBuffer { name = PluginAPI.PLUGIN_NAME };
            var camera = Camera.main;
            if (camera)
            {
                camera.AddCommandBuffer(CameraEvent.AfterGBuffer, commandBuffer_);
            }

#if UNITY_STANDALONE_WIN
            if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D12)
            {
                renderTexture_ = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32) { enableRandomWrite = true };
                renderTexture_.Create();

                if (GraphicsSettings.currentRenderPipeline != null)
                {
                    RenderPipelineManager.endContextRendering += OnEndFrameRendering;
                }
            }
#endif

            #endregion

            #region InitializeBlackBox
            // Initialize BlackBox
            if (BlackBoxConfig.Instance.ConfigOption == ConfigOption.Web)
            {
                PluginAPI.bbx_local_config_set_enable_basic_profiling("webconfig");
                PluginAPI.bbx_local_config_set_enable_crash_reporter("webconfig");
                PluginAPI.bbx_local_config_set_store_dxdiag("webconfig");
                PluginAPI.bbx_local_config_set_store_crash_video("webconfig");
                Logger.Log("Use WebConfig for BlackBox settings");
            }
            else if (BlackBoxConfig.Instance.ConfigOption == ConfigOption.Local)
            {
                PluginAPI.bbx_local_config_set_enable_basic_profiling(BlackBoxConfig.Instance.EnableBasicProfiling ? CONFIG_TRUE : CONFIG_FALSE);
                PluginAPI.bbx_local_config_set_enable_crash_reporter(BlackBoxConfig.Instance.EnableCrashReporter ? CONFIG_TRUE : CONFIG_FALSE);
                PluginAPI.bbx_local_config_set_store_dxdiag(BlackBoxConfig.Instance.StoreHardwareInformation ? CONFIG_TRUE : CONFIG_FALSE);
                PluginAPI.bbx_local_config_set_store_crash_video(BlackBoxConfig.Instance.StoreVideo ? CONFIG_TRUE : CONFIG_FALSE);
                PluginAPI.blackbox_save_local_config(Application.productName);
            }

            var config = BlackBoxConfig.Instance;
            BlackBoxConfig.LoadBlackBoxConfig(config);
            config.SaveConfigAsset();

            Logger.Log("Plugin api calling Blackbox init");
            PluginAPI.blackbox_init(gameName, version, helperPath, issueReporterPath, crashPath, logPath, iniPath, gpuDeviceId, gpuDeviceType, isEditor, helperAlternativeLogPath, gpu_name, gpu_version);
            #endregion

            var deviceIdLocal = GetDeviceId();

            // C# crash handler
#if UNITY_STANDALONE_OSX || UNITY_ANDROID || UNITY_IOS
            Logger.Log("Plugin api calling CrashHandler.Initialize()");
            CrashHandler.Initialize();
#endif

#if UNITY_ANDROID && !UNITY_EDITOR
            _ = LoadSSLCertificate();
#endif
        }

#if UNITY_STANDALONE_WIN
        private static void OnEndFrameRendering(ScriptableRenderContext context, List<Camera> cameras)
        {
            SendTextureToPlugin();
        }

        internal static void SendTextureToPlugin()
        {
            if (!renderTexture_) return;

            ScreenCapture.CaptureScreenshotIntoRenderTexture(renderTexture_);
            var texturePtr = renderTexture_.GetNativeTexturePtr();

            if (texturePtr == IntPtr.Zero) return;

            commandBuffer_.Clear();
            commandBuffer_.IssuePluginEventAndData(PluginAPI.blackbox_get_render_event_dx12_func(), 1, texturePtr);
            Graphics.ExecuteCommandBuffer(commandBuffer_);
        }
#endif


        public void StartSession()
        {
            PluginAPI.blackbox_start_session();
        }

        public void EndSession()
        {
            PluginAPI.blackbox_end_session();
            PluginAPI.blackbox_shutdown();
        }

        public void Update(float dt, float cpu, float gpu, bool isIngameSession = true)
        {
            PluginAPI.blackbox_update(dt, cpu, gpu, isIngameSession);
#if UNITY_STANDALONE_WIN
            commandBuffer_?.IssuePluginEvent(PluginAPI.blackbox_get_render_event_func(), 0);
#endif

            /*
            * Copy this function call if you want your sessions, crash logs, and reports to include information from third-party accounts.
            * Place it in the part of your game where the third-party account information has been retrieved.
            * You are responsible for obtaining your users’ third-party account information.
            */
            // Helper.BBXSetThirdPartyAccount("steam", "steam_user_id_123", "steam_username", "steamemail@example.com");
        }

#if UNITY_STANDALONE_WIN
        ~PluginManager()
        {
            if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D12)
            {
#if UNITY_2023_1_OR_NEWER
            if (GraphicsSettings.currentRenderPipeline != null)
            {
                RenderPipelineManager.endContextRendering -= OnEndFrameRendering;
            }
#endif
                commandBuffer_?.Dispose();
                renderTexture_?.Release();
            }
        }
#endif

        // TODO: +AS:20220317 Move this to utilities and add more validations

#if UNITY_ANDROID
        public static async Task<(byte[], string)> ExtractAsset(string path)
        {
            using (UnityWebRequest request = UnityWebRequest.Get(path))
            {
                var op = request.SendWebRequest();
                while (!op.isDone)
                {
                    await Task.Yield();
                }

                if (request.result != UnityWebRequest.Result.Success)
                {
                    Logger.LogError($"Failed to load file {path} Error: {request.error}");
                    return (Array.Empty<byte>(), request.error);
                }

                return (request.downloadHandler.data, request.error);
            }
        }
#endif

#if UNITY_ANDROID && !UNITY_EDITOR
        public async Task LoadSSLCertificate()
        {
            string sourcePath = Path.Combine(Application.streamingAssetsPath, SSLCertificate);
            var (data, error) = await ExtractAsset(sourcePath);
            if (data.Length > 0)
            {
#if UNITY_ANDROID
                PluginAPI.bbx_set_cert(System.Text.Encoding.UTF8.GetString(data));
#endif
            }
        }
#endif

#if UNITY_IOS || UNITY_STANDALONE_OSX
        public async Task<(byte[], string)> ExtractAsset(string path)
        {
            string text = await File.ReadAllTextAsync(path);
            byte[] bytes = Encoding.ASCII.GetBytes(text);

            return (bytes, "");
        }
#endif

#if UNITY_IOS || UNITY_STANDALONE_OSX || UNITY_ANDROID
        public async Task PreInitialize()
        {
            await ExtractBlackBoxConfig();
        }

        public async Task ExtractBlackBoxConfig()
        {
            // paths
            string sourcePath = Path.Combine(Application.streamingAssetsPath, BlackBoxConfig.BLACKBOX_DIRECTORY, BlackBoxConfig.BLACKBOX_CONFIG_FILE);
            string destPath = GetConfigPath();

            // ensure destination directory exists
            string destDir = Path.GetDirectoryName(destPath);
            if (!Directory.Exists(destDir))
            {
                Directory.CreateDirectory(destDir);
            }

            var (data, error) = await ExtractAsset(sourcePath);
            if (data.Length == 0)
            {
                Logger.Log($"Failed to load blackbox config: {error}");
            }
            else
            {
                try
                {
                    File.WriteAllBytes(destPath, data);
                    Logger.Log($"Config written to: {destPath}");
                }
                catch (Exception e)
                {
                    Logger.Log($"Failed to write blackbox config to: {destPath}");
                }
            }
            var cycle = Cycle.Instance;
            cycle.PreInitComplete();
        }
#endif

        public void OnValidateBlackBox()
        {
            Assert.IsTrue(IsBlackBoxConfigExist(), $"[BlackBoxManaged] ERROR! BlackBox.ini file is missing!");
        }
        public bool IsBlackBoxConfigExist()
        {
            return File.Exists(GetConfigPath());
        }

        #region BlackBox Info

        public void GatherAdditionalInfo()
        {
#if UNITY_ANDROID || UNITY_IOS
            var userPath = Application.persistentDataPath;
            var unityFolder = Path.Combine(userPath, BlackBoxConfig.BLACKBOX_DIRECTORY, "unity");
#else
            var userPath        = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
            var unityFolder     = Path.Combine(userPath, ".blackbox", "unity");
#endif
            var applicationInfo = Path.Combine(unityFolder, "application.json");
            var json = new AdditionalInfo();
            var txt = json.ToJsonStr();

            // create folder
            if (!Directory.Exists(unityFolder))
            {
                Directory.CreateDirectory(unityFolder);
            }

            File.WriteAllText(applicationInfo, txt);
        }

        public static string GetSDKVersion()
        {
            return Marshal.PtrToStringAnsi(PluginAPI.blackbox_info_get_version());
        }

        public static string GetCrashFolder()
        {
#if UNITY_ANDROID || UNITY_IOS
            return Path.Combine(Application.persistentDataPath, BlackBoxConfig.BLACKBOX_DIRECTORY, "Minidump");
#else

            // Get the folder path where crash reports are stored
            var crashReportPath = UnityEngine.Windows.CrashReporting.crashReportFolder;

            // Log the path to the console
            Logger.Log("Crash reports are stored in: " + crashReportPath);

            // Check if the folder exists, and if it does, list crash reports
            if (Directory.Exists(crashReportPath))
            {
                var crashReports = Directory.GetFiles(crashReportPath);
                foreach (var report in crashReports)
                {
                    Logger.Log("Found crash report: " + report);
                }
                return crashReportPath;
            }
            else
            {
                var userProfileFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
                var crashFolder = System.IO.Path.Combine(userProfileFolder, ".blackbox", "crash");
                System.IO.Directory.CreateDirectory(crashFolder);

                Logger.Log("Crash report folder does not exist. Create new one" + crashFolder);
                return crashFolder;
            }
#endif
        }

        public static string GetHelperPath()
        {
            return Path.Combine(Application.streamingAssetsPath, BlackBoxConfig.BLACKBOX_DIRECTORY, "helper", "blackbox_helper.exe");
        }

        public static string GetIssueReporterPath()
        {
            return Path.Combine(Application.streamingAssetsPath, BlackBoxConfig.BLACKBOX_DIRECTORY, "issue_reporter", "blackbox_issue_reporter.exe");
        }

        public static string GetConfigPath()
        {
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS)
            return Path.Combine(Application.persistentDataPath, BlackBoxConfig.BLACKBOX_DIRECTORY, BlackBoxConfig.BLACKBOX_CONFIG_FILE);
#else
            return Path.Combine(Application.streamingAssetsPath, BlackBoxConfig.BLACKBOX_DIRECTORY, BlackBoxConfig.BLACKBOX_CONFIG_FILE);
#endif
        }

        public static string GetLogPath()
        {
#if UNITY_EDITOR
            var localDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            var blackboxLogPath = Path.Combine(localDataPath, "Unity", "Editor", "Editor.log");
            return blackboxLogPath;
#else
            var blackboxLogPath = Path.Combine(Application.persistentDataPath, "Player.log");

            return blackboxLogPath;
#endif
        }

        public static string GetHelperLogAlternativePath()
        {
            return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "/BlackBoxHelper", "/helper_1.log");
        }

        public static (string, string) GetGPUInfo()
        {
            var gpu_name = SystemInfo.graphicsDeviceName;
            var gpu_version = SystemInfo.graphicsDeviceVersion;
            return (gpu_name, gpu_version);
        }
        #endregion

        private string GetDeviceId()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            string android_uuid_file_path = Path.Combine(Application.persistentDataPath, BlackBoxConfig.BLACKBOX_DIRECTORY, BlackBoxConfig.ANDROID_UUID_FILENAME);
            bool is_path_valid = PluginAPI.bbx_init_android_device_id(android_uuid_file_path);
            if (is_path_valid == false) {
                Logger.LogWarning("Invalid Android device_id path!");
                return deviceID;
            }
#endif
            // DeviceID by Unity
            callbackDelegate = Helper.BBXGenerateDeviceIDCallback;
            PluginAPI.bbx_generate_device_id("unity", callbackDelegate);

            deviceID = Marshal.PtrToStringAnsi(PluginAPI.bbx_config_get_device_id());
            if (String.IsNullOrEmpty(deviceID))
            {
                // Device ID by Default BlackBox
                PluginAPI.bbx_generate_device_id("", callbackDelegate);
                deviceID = Marshal.PtrToStringAnsi(PluginAPI.bbx_config_get_device_id());
            }

            Logger.Log($"DeviceID: [{deviceID}]");
            return deviceID;
        }
    }
}
