using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Rendering;
using static UnityEngine.GUILayout;
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PrimeTween.Internal")]

namespace PrimeTween {
    internal class PrimeTweenInstaller : ScriptableObject {
        [SerializeField] internal SceneAsset demoScene;
        [SerializeField] internal SceneAsset demoSceneUrp;
        [SerializeField] internal Color uninstallButtonColor;

        [ContextMenu(nameof(ResetReviewRequest))] void ResetReviewRequest() => ReviewRequest.ResetReviewRequest();
        [ContextMenu(nameof(DebugReviewRequest))] void DebugReviewRequest() => ReviewRequest.DebugReviewRequest();
    }

    [CustomEditor(typeof(PrimeTweenInstaller), false)]
    internal class InstallerInspector : Editor {
        internal const string pluginName = "PrimeTween";
        internal const string pluginPackageId = "com.kyrylokuzyk.primetween";
        internal const string tgzPath = "Assets/Plugins/PrimeTween/internal/com.kyrylokuzyk.primetween.tgz";
        internal const string newTgzPath = "Assets/Plugins/PrimeTween/internal/com.kyrylokuzyk.primetween-" + version + ".tgz";
        const string documentationUrl = "https://github.com/KyryloKuzyk/PrimeTween";
        bool isInstalled;
        bool hasNewTgz;
        GUIStyle boldButtonStyle;
        GUIStyle uninstallButtonStyle;
        GUIStyle wordWrapLabelStyle;

        void OnEnable() {
            isInstalled = CheckPluginInstalled();
            hasNewTgz = File.Exists(newTgzPath);
        }

        /// Use Package Manager because Unity 2018 doesn't support version defines
        static bool CheckPluginInstalled() {
            var listRequest = Client.List(true);
            while (!listRequest.IsCompleted) {
            }
            return listRequest.Result.Any(_ => _.name == pluginPackageId);
        }

        public override void OnInspectorGUI() {
            if (boldButtonStyle == null) {
                boldButtonStyle = new GUIStyle(GUI.skin.button) { fontStyle = FontStyle.Bold };
            }
            var installer = (PrimeTweenInstaller)target;
            if (uninstallButtonStyle == null) {
                uninstallButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = installer.uninstallButtonColor } };
            }
            if (wordWrapLabelStyle == null) {
                wordWrapLabelStyle = new GUIStyle(GUI.skin.label) { wordWrap = true, richText = true, margin = new RectOffset(4, 4, 8, 8) };
            }
            EditorGUI.indentLevel = 5;
            Space(8);
            Label(pluginName, EditorStyles.boldLabel);
            Space(4);
            if (!isInstalled) {
                if (Button("Install " + pluginName)) {
                    installPlugin();
                }
                return;
            }
            if (hasNewTgz) {
                if (Button($"Update to {version}", boldButtonStyle)) {
                    ReviewRequest.OnPackageUpdate();
                }
                Space(8);
            }
            if (Button("Documentation", boldButtonStyle)) {
                Application.OpenURL(documentationUrl);
            }

            Space(8);
            if (Button("Open Demo", boldButtonStyle)) {
                var rpAsset = GraphicsSettings.
                    #if UNITY_2019_3_OR_NEWER
                    defaultRenderPipeline;
                    #else
                    renderPipelineAsset;
                    #endif
                bool isUrp = rpAsset != null && rpAsset.GetType().Name.Contains("Universal");
                var demoScene = isUrp ? installer.demoSceneUrp : installer.demoScene;
                if (demoScene == null) {
                    Debug.LogError("Please re-import the plugin from Asset Store and import the 'Demo' folder.\n");
                    return;
                }
                var path = AssetDatabase.GetAssetPath(demoScene);
                EditorSceneManager.OpenScene(path);
            }
            #if UNITY_2019_4_OR_NEWER
            if (Button("Import Basic Examples")) {
                EditorUtility.DisplayDialog(pluginName, $"Please select the '{pluginName}' package in 'Package Manager', then press the 'Samples/Import' button at the bottom of the plugin's description.", "Ok");
                UnityEditor.PackageManager.UI.Window.Open(pluginPackageId);
            }
            #endif
            if (Button("Support")) {
                Application.OpenURL("https://github.com/KyryloKuzyk/PrimeTween#support");
            }

            Space(8);
            if (Button("Uninstall", uninstallButtonStyle)) {
                Client.Remove(pluginPackageId);
                isInstalled = false;
                var msg = $"Please remove the folder manually to uninstall {pluginName} completely: 'Assets/Plugins/{pluginName}'";
                EditorUtility.DisplayDialog(pluginName, msg, "Ok");
                Debug.Log(msg);
            }

            if (EditorPrefs.GetBool(InsertCallbackBug.showInsertCallbackBugUi, false)) {
                Space(24);
                Label("Updating from PrimeTween [1.1.10 - 1.1.22]", EditorStyles.boldLabel);
                Label("The behaviour of 'Sequence.ChainCallback()' and 'InsertCallback()' was fixed in PrimeTween 1.2.0 so the code written with older versions may work differently in some cases.", wordWrapLabelStyle);
                if (Button("Find potential issues")) {
                    InsertCallbackBug.Find();
                }
                BeginHorizontal();
                if (Button("More info")) {
                    Application.OpenURL(InsertCallbackBug.moreInfoUrl);
                }
                if (Button("Download version 1.1.22")) {
                    Application.OpenURL("https://github.com/KyryloKuzyk/PrimeTween/blob/545dcc52769d52841e282c772e98c8984bfeb243/Benchmarks/Packages/com.kyrylokuzyk.primetween.tgz");
                }
                EndHorizontal();
            }

            Space(24);
            Label("Enjoying PrimeTween?", EditorStyles.boldLabel);
            Label("Consider leaving an <b>honest review</b> and starring PrimeTween on GitHub!\n\n" +
                  "Honest reviews make PrimeTween better and help other developers discover it.", wordWrapLabelStyle);
            if (Button("Leave review!", GUI.skin.button)) {
                ReviewRequest.DisableReviewRequest();
                ReviewRequest.OpenReviewsURL();
            }
        }

        static void installPlugin() {
            if (File.Exists(newTgzPath)) {
                MoveAndRenameTgzArchive();
            }

            ReviewRequest.OnBeforeInstall();
            var path = $"file:../{tgzPath}";
            var addRequest = Client.Add(path);
            while (!addRequest.IsCompleted) {
            }
            if (addRequest.Status == StatusCode.Success) {
                Debug.Log($"{pluginName} installed successfully.\n" +
                          $"Offline documentation is located at Packages/{pluginName}/Documentation.md.\n" +
                          $"Online documentation: {documentationUrl}\n");
            } else {
                Debug.LogError($"Please re-import the plugin from the Asset Store and check that the file exists: [{path}].\n\n{addRequest.Error?.message}\n");
            }
        }

        internal static void MoveAndRenameTgzArchive() {
            Assert.IsTrue(File.Exists(newTgzPath));
            Assert.IsTrue(File.Exists(newTgzPath + ".meta"));
            File.Delete(tgzPath);
            File.Delete(tgzPath + ".meta");
            File.Move(newTgzPath, tgzPath);
            File.Move(newTgzPath + ".meta", tgzPath + ".meta");
            RevertTgzMeta();
        }

        static void RevertTgzMeta() {
            const string path = tgzPath + ".meta";
            Assert.IsTrue(File.Exists(path), path);
            File.WriteAllText(path, @"fileFormatVersion: 2
guid: cdd0c4b9889044d73bc958a922ada300
DefaultImporter:
  externalObjects: {}
  userData: 
  assetBundleName: 
  assetBundleVariant: 
");
        }

        [InitializeOnLoadMethod]
        static void InitOnLoad() {
            AssetDatabase.importPackageCompleted += name => {
                if (name.Contains(pluginName)) {
                    if (CheckPluginInstalled()) {
                        ReviewRequest.OnPackageUpdate();
                    } else {
                        var installer = AssetDatabase.LoadAssetAtPath<PrimeTweenInstaller>("Assets/Plugins/PrimeTween/PrimeTweenInstaller.asset");
                        EditorUtility.FocusProjectWindow(); // this is important to show the installer object in the Project window
                        Selection.activeObject = installer;
                        EditorGUIUtility.PingObject(installer);
                        EditorApplication.update += InstallAndUnsubscribeFromUpdate;
                        void InstallAndUnsubscribeFromUpdate() {
                            EditorApplication.update -= InstallAndUnsubscribeFromUpdate;
                            installPlugin();
                        }
                    }
                }
            };
        }

        internal const string version = "1.3.7";
    }

    internal static class FixedUpdateParameterMigration {
        internal static string[] FindLocalScriptGuids() {
            var listRequest = Client.List(true);
            while (!listRequest.IsCompleted) {
            }
            Assert.AreEqual(StatusCode.Success, listRequest.Status);
            string[] folders = listRequest.Result
                .Where(x => x.source == PackageSource.Embedded || x.source == PackageSource.Local)
                .Where(x => x.name != InstallerInspector.pluginPackageId)
                .Select(x => x.assetPath)
                .Append("Assets")
                .ToArray();
            return AssetDatabase.FindAssets("t:Script", folders);
        }

        internal static bool Process(string[] scripts, bool? fixAutomatically = null) {
            var logSb = new StringBuilder();
            var fileSb = new StringBuilder();
            foreach (string guid in scripts)
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                var textAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
                string text = textAsset.text;
                if (!Regex.IsMatch(text, @"using PrimeTween\s*;")) {
                    continue;
                }
                var parameterMatches = Regex.Matches(text, @"useFixedUpdate\s*:");
                if (parameterMatches.Count > 0) {
                    if (!fixAutomatically.HasValue) {
                        return true;
                    }
                    logSb.Clear();
                    if (fixAutomatically.Value) {
                        fileSb.Clear();
                        fileSb.Append(text);
                        for (int i = parameterMatches.Count - 1; i >= 0; i--) {
                            var paramMatch = parameterMatches[i];
                            fileSb.Remove(paramMatch.Index, paramMatch.Length);
                            fileSb.Insert(paramMatch.Index, "updateType:");
                        }
                        File.WriteAllText(path, fileSb.ToString());
                        logSb.Append($"PrimeTween automatically renamed ({parameterMatches.Count}) occurrences of 'useFixedUpdate' to 'updateType' in file '{textAsset.name}.cs':\n");
                    } else {
                        logSb.Append($"PrimeTween: please rename ({parameterMatches.Count}) occurrences of 'useFixedUpdate' to 'updateType' in file '{textAsset.name}.cs':\n");
                    }
                    var lineMatches = Regex.Matches(text, @"(?m)^.*useFixedUpdate\s*:.*$", RegexOptions.Multiline);
                    foreach (Match match in lineMatches) {
                        logSb.Append($"{match.Value.Trim().Replace("useFixedUpdate", "<b>useFixedUpdate</b>")}\n");
                    }
                    Debug.unityLogger.Log(fixAutomatically.Value ? LogType.Warning : LogType.Error, logSb.ToString());
                }
            }
            // no need to call AssetDatabase.Refresh() here because MoveAndRenameTgzArchive() already does that
            return false;
        }
    }

    internal static class ReviewRequest {
        const string version = InstallerInspector.version;
        const string canAskKey = "PrimeTween.canAskForReview";
        const string versionKey = "PrimeTween.version";

        internal static void OnPackageUpdate() {
            log("OnPackageUpdate");
            if (!File.Exists(InstallerInspector.newTgzPath)) {
                Debug.LogError($"The installation archive is missing: '{InstallerInspector.newTgzPath}'. Please re-import PrimeTween from Asset Store.");
                return;
            }
            bool shouldAskForReview = true;
            var scriptGuids = FixedUpdateParameterMigration.FindLocalScriptGuids();
            if (FixedUpdateParameterMigration.Process(scriptGuids)) {
                shouldAskForReview = false;
                const string msg = "'bool useFixedUpdate' parameter was changed to 'UpdateType updateType' in version 1.3.0, which will cause breaking changes in your current project.\n" +
                                   "PrimeTween can fix the breaking changes automatically, or you can fix them manually after the update.\n";
                Debug.LogWarning($"PrimeTween: the {msg}");
                int response = EditorUtility.DisplayDialogComplex($"{InstallerInspector.pluginName} {version}",
                    $"The {msg}",
                    "Fix automatically",
                    "Cancel",
                    "Fix manually");
                string cancelMessage = $"PrimeTween: update to {version} was cancelled. You can trigger update manually from 'Assets/Plugins/PrimeTween/PrimeTweenInstaller'.";
                if (response == 1) {
                    Debug.LogWarning(cancelMessage);
                    return;
                }
                if (!EditorUtility.DisplayDialog($"{InstallerInspector.pluginName} {version}", "Please back up your project before proceeding.", "OK", "Cancel")) {
                    Debug.LogWarning(cancelMessage);
                    return;
                }
                bool fixAutomatically = response == 0;
                FixedUpdateParameterMigration.Process(scriptGuids, fixAutomatically);
            }
            InstallerInspector.MoveAndRenameTgzArchive();
            if (UNITY_2018) {
                var removeRequest = Client.Remove(InstallerInspector.pluginPackageId);
                while (!removeRequest.IsCompleted) {
                }
                string path = $"file:../{InstallerInspector.tgzPath}";
                Client.Add(path);
            } else {
                EditorApplication.ExecuteMenuItem("Assets/Refresh"); // AssetDatabase.Refresh() refreshes the project only partially
            }

            string prevVersion = savedVersion;
            if (savedVersion == version) {
                log($"same version {version}");
                return;
            }
            savedVersion = version;

            if (InsertCallbackBug.IsUpdatingFromVersionWithBug(prevVersion)) {
                InsertCallbackBug.Find();
                EditorPrefs.SetBool(InsertCallbackBug.showInsertCallbackBugUi, true);
                shouldAskForReview = false;
            } else {
                EditorPrefs.SetBool(InsertCallbackBug.showInsertCallbackBugUi, false);
            }
            log($"updated from version {prevVersion} to {version}, {nameof(shouldAskForReview)}: {shouldAskForReview}");
            if (shouldAskForReview) {
                TryAskForReview();
            }
        }

        static bool UNITY_2018 {
            get {
                #if UNITY_2018
                    return true;
                #else
                    return false;
                #endif
            }
        }

        static void TryAskForReview() {
            if (!EditorPrefs.GetBool(canAskKey, true)) {
                log("can't ask");
                return;
            }
            DisableReviewRequest();
            var response = EditorUtility.DisplayDialogComplex("Enjoying PrimeTween?",
                "Would you mind to leave an honest review on Asset store? Honest reviews make PrimeTween better and help other developers discover it.",
                "Sure, leave a review!",
                "Never ask again",
                "");
            if (response == 0) {
                OpenReviewsURL();
            }
        }

        internal static void OnBeforeInstall() {
            log($"OnBeforeInstall {version}");
            if (string.IsNullOrEmpty(savedVersion)) {
                savedVersion = version;
            }
        }

        static string savedVersion {
            get => EditorPrefs.GetString(versionKey);
            set => EditorPrefs.SetString(versionKey, value);
        }

        internal static void DisableReviewRequest() => EditorPrefs.SetBool(canAskKey, false);
        internal static void OpenReviewsURL() => Application.OpenURL("https://assetstore.unity.com/packages/slug/252960#reviews");

        internal static void ResetReviewRequest() {
            Debug.Log(nameof(ResetReviewRequest));
            EditorPrefs.DeleteKey(versionKey);
            EditorPrefs.DeleteKey(canAskKey);
        }

        internal static void DebugReviewRequest() {
            Debug.Log(nameof(DebugReviewRequest));
            savedVersion = "1.1.22";
            EditorPrefs.SetBool(canAskKey, false);
            // TryAskForReview();
        }

        [System.Diagnostics.Conditional("_")]
        static void log(string msg) {
            Debug.Log($"ReviewRequest: {msg}");
        }
    }

    internal static class InsertCallbackBug {
        internal const string moreInfoUrl = "https://github.com/KyryloKuzyk/PrimeTween/discussions/112";
        internal const string showInsertCallbackBugUi = "PrimeTween.showInsertCallbackBugUi";
        static Dictionary<short, OpCode> OpCodeDict;
        static MethodInfo[] methodsWithBug;
        static MethodInfo[] groupMethods;

        internal static bool IsUpdatingFromVersionWithBug(string prevVersionString) {
            if (Version.TryParse(prevVersionString, out var prevVersion)
                && new Version(1, 1, 10) <= prevVersion
                && prevVersion <= new Version(1, 1, 22)
                ) {
                return true;
            }
            return false;
        }

        internal static void Find() {
            OpCodeDict = typeof(OpCodes)
                .GetFields(BindingFlags.Public | BindingFlags.Static)
                .Select(x => (OpCode)x.GetValue(null))
                .ToDictionary(x => x.Value, x => x);
            #if PRIME_TWEEN_INSTALLED
            methodsWithBug = typeof(Sequence).GetMethods(BindingFlags.Public | BindingFlags.Instance)
                .Where(methodInfo => methodInfo.Name == nameof(Sequence.ChainCallback) || methodInfo.Name == nameof(Sequence.InsertCallback))
                .Select(methodInfo => methodInfo.IsGenericMethod ? methodInfo.GetGenericMethodDefinition() : methodInfo)
                .ToArray();
            Assert.AreEqual(4, methodsWithBug.Length);
            groupMethods = typeof(Sequence).GetMethods(BindingFlags.Public | BindingFlags.Instance)
                .Where(methodInfo => methodInfo.Name == nameof(Sequence.Group))
                .ToArray();
            #endif
            Assert.AreEqual(2, groupMethods.Length);

            string methodAssemblyName = methodsWithBug[0].Module.Assembly.FullName;
            const BindingFlags findAll = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
            int numPotentialIssues = AppDomain.CurrentDomain.GetAssemblies()
                .Where(assembly => assembly.GetReferencedAssemblies().Any(dependency => dependency.FullName == methodAssemblyName))
                .Where(assembly => !assembly.GetName().Name.StartsWith("PrimeTween.", StringComparison.Ordinal))
                .SelectMany(assembly => assembly.GetTypes())
                .SelectMany(type => type.GetMethods(findAll).Cast<MethodBase>().Union(type.GetConstructors(findAll)))
                .Count(method => FindInMethod(method));
            if (numPotentialIssues == 0) {
                Debug.Log($"PrimeTween updated to version {InstallerInspector.version}: no potential issues found in ChainCallback() and InsertCallback() usages.\n" +
                          $"More info: {moreInfoUrl}\n");
            }

            int updateResponse = EditorUtility.DisplayDialogComplex($"{InstallerInspector.pluginName} {InstallerInspector.version}",
                "PrimeTween 1.2.0 fixed a bug in ChainCallback() and InsertCallback() methods.\n" +
                "This fix may introduce breaking changes in the existing projects. Please see the Console output for more details.",
                "More info",
                "Close",
                "");
            if (updateResponse == 0) {
                Application.OpenURL(moreInfoUrl);
            }
        }

        /// https://stackoverflow.com/a/33034906/1951038
        static bool FindInMethod(MethodBase method) {
            byte[] il = method.GetMethodBody()?.GetILAsByteArray();
            if (il == null) {
                return false;
            }
            bool bugFound = false;
            using (var br = new BinaryReader(new MemoryStream(il))) {
                while (br.BaseStream.Position < br.BaseStream.Length) {
                    byte firstByte = br.ReadByte();
                    short opCodeValue = firstByte == 0xFE ? BitConverter.ToInt16(new[] { br.ReadByte(), firstByte }, 0) : firstByte;
                    OpCode opCode = OpCodeDict[opCodeValue];
                    switch (opCode.OperandType) {
                        case OperandType.ShortInlineBrTarget:
                        case OperandType.ShortInlineVar:
                        case OperandType.ShortInlineI:
                            br.ReadByte();
                            break;
                        case OperandType.InlineVar:
                            br.ReadInt16();
                            break;
                        case OperandType.InlineField:
                        case OperandType.InlineType:
                        case OperandType.ShortInlineR:
                        case OperandType.InlineString:
                        case OperandType.InlineSig:
                        case OperandType.InlineI:
                        case OperandType.InlineBrTarget:
                            br.ReadInt32();
                            break;
                        case OperandType.InlineI8:
                        case OperandType.InlineR:
                            br.ReadInt64();
                            break;
                        case OperandType.InlineSwitch:
                            var size = (int)br.ReadUInt32();
                            br.ReadBytes(size * 4);
                            break;
                        case OperandType.InlineTok:
                            br.ReadUInt32();
                            break;
                        case OperandType.InlineMethod:
                            int token = (int)br.ReadUInt32();
                            if (method.Module.ResolveMethod(token) is MethodInfo resolvedMethod) {
                                if (bugFound) {
                                    if (groupMethods.Contains(resolvedMethod)) {
                                        Debug.LogError($"PrimeTween updated to version {InstallerInspector.version}: potential breaking change found in the '{method.DeclaringType}.{method.Name}()' method.\n" +
                                                       "Please double-check the behavior if Group() is called immediately after the ChainCallback() or InsertCallback() and apply the fix manually if necessary.\n" +
                                                       "Or use ChainCallbackObsolete/InsertCallbackObsolete() instead to preserve the old incorrect behavior.\n" +
                                                       $"More info: {moreInfoUrl}\n");
                                        return true;
                                    }
                                } else {
                                    bugFound = isMethodWithBug(resolvedMethod);
                                }
                            }
                            break;
                        case OperandType.InlineNone:
                            break;
                        default:
                            throw new Exception();
                    }
                }
            }
            return false;
        }

        static bool isMethodWithBug(MethodInfo method) {
            foreach (var methodWithBug in methodsWithBug) {
                if (methodWithBug.IsGenericMethodDefinition && method.IsGenericMethod) {
                    if (methodWithBug == method.GetGenericMethodDefinition()) {
                        return true;
                    }
                } else if (methodWithBug == method) {
                    return true;
                }
            }
            return false;
        }
    }
}
