using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.EditorDependencies;
using SingularityGroup.HotReload.Editor.Localization;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
using Color = UnityEngine.Color;
using Task = System.Threading.Tasks.Task;
#if UNITY_2019_4_OR_NEWER
using Unity.CodeEditor;
#endif

namespace SingularityGroup.HotReload.Editor {
    internal class ErrorData {
        public string fileName;
        public string error;
        public TextAsset file;
        public int lineNumber;
        public string stacktrace;
        public string linkString;
        private static string[] supportedPaths = new[] { Path.GetFullPath("Assets"), Path.GetFullPath("Plugins") };
        
        public static ErrorData GetErrorData(string errorString) {
            // Get the relevant file name
            string stackTrace = errorString;
            string fileName = null;
            try {
                int csIndex = 0;
                int attempt = 0;
                do {
                    csIndex = errorString.IndexOf(".cs", csIndex + 1, StringComparison.Ordinal);
                    if (csIndex == -1) {
                        break;
                    }
                    int fileNameStartIndex = csIndex - 1;
                    for (; fileNameStartIndex >= 0; fileNameStartIndex--) {
                        if (!char.IsLetter(errorString[fileNameStartIndex])) {
                            if (errorString.Contains("error CS")) {
                                fileName = errorString.Substring(fileNameStartIndex + 1,
                                    csIndex - fileNameStartIndex + ".cs".Length - 1);
                            } else {
                                fileName = errorString.Substring(fileNameStartIndex,
                                    csIndex - fileNameStartIndex + ".cs".Length);
                            }
                            break;
                        }
                    }
                } while (attempt++ < 100 && fileName == null);
            } catch {
                // ignore
            }
            fileName = fileName ?? Translations.UI.TapToShowStacktrace;
            
            // Get the error
            string error = (errorString.Contains("error CS") 
                               ? Translations.UI.CompileErrorMessage + ", " 
                               : Translations.UI.UnsupportedChangeMessage + ", ") + Translations.UI.TapHereToSeeMore;
            int endOfError = errorString.IndexOf(". in ", StringComparison.Ordinal);
            string specialChars = "\"'/\\";
            char[] characters = specialChars.ToCharArray();
            int specialChar = errorString.IndexOfAny(characters);
            try {
                if (errorString.Contains("error CS") ) {
                    error = errorString.Substring(errorString.IndexOf("error CS", StringComparison.Ordinal), errorString.Length - errorString.IndexOf("error CS", StringComparison.Ordinal)).Trim();
                    using (StringReader reader = new StringReader(error)) {
                        string line;
                        while ((line = reader.ReadLine()) != null) {
                            error = line;
                            break;
                        }
                    }
                } else if (errorString.StartsWith("errors:", StringComparison.Ordinal) && endOfError > 0) {
                    error = errorString.Substring("errors: ".Length, endOfError - "errors: ".Length).Trim();
                } else if (errorString.StartsWith("errors:", StringComparison.Ordinal) && specialChar > 0) {
                    error = errorString.Substring("errors: ".Length, specialChar - "errors: ".Length).Trim();
                } 
            } catch {
                // ignore
            }

            // Get relative path
            TextAsset file = null;
            try {
                foreach (var path in supportedPaths) {
                    int lastprojectIndex = 0;
                    int attempt = 0;
                    while (attempt++ < 100 && !file) {
                        lastprojectIndex = errorString.IndexOf(path, lastprojectIndex + 1, StringComparison.Ordinal);
                        if (lastprojectIndex == -1) {
                            break;
                        }
                        var fullCsIndex = errorString.IndexOf(".cs", lastprojectIndex, StringComparison.Ordinal);
                        var l = fullCsIndex - lastprojectIndex + ".cs".Length;
                        if (l <= 0) {
                            continue;
                        }
                        var candidateAbsolutePath = errorString.Substring(lastprojectIndex, fullCsIndex - lastprojectIndex + ".cs".Length);
                        var candidateRelativePath = EditorCodePatcher.GetRelativePath(filespec: candidateAbsolutePath, folder: path);
                        file = AssetDatabase.LoadAssetAtPath<TextAsset>(candidateRelativePath);
                    }
                }
            } catch {
                // ignore
            }
            
            // Get the line number
            int lineNumber = 0;
            try {
                int lastIndex = 0;
                int attempt = 0;
                do {
                    lastIndex = errorString.IndexOf(fileName, lastIndex + 1, StringComparison.Ordinal);
                    if (lastIndex == -1) {
                        break;
                    }
                    var part = errorString.Substring(lastIndex + fileName.Length);
                    if (!part.StartsWith(errorString.Contains("error CS") ? "(" : ":", StringComparison.Ordinal) 
                        || part.Length == 1 
                        || !char.IsDigit(part[1])
                       ) {
                        continue;
                    }
                    int y = 1;
                    for (; y < part.Length; y++) {
                        if (!char.IsDigit(part[y])) {
                            break;
                        }
                    }
                    if (int.TryParse(part.Substring(1, errorString.Contains("error CS") ? y - 1 : y), out lineNumber)) {
                        break;
                    }
                } while (attempt++ < 100);
            } catch { 
                //ignore
            }

            return new ErrorData() {
                fileName = fileName,
                error = error,
                file = file,
                lineNumber = lineNumber,
                stacktrace = stackTrace,
                linkString = lineNumber > 0 ? fileName + ":" + lineNumber : fileName
            };
        }
        
    }
    
    internal struct HotReloadRunTabState {
        public readonly bool spinnerActive;
        public readonly string indicationIconPath;
        public readonly bool requestingDownloadAndRun;
        public readonly bool starting;
        public readonly bool stopping;
        public readonly bool running;
        public readonly Tuple<float, string> startupProgress;
        public readonly string indicationStatusText;
        public readonly LoginStatusResponse loginStatus;
        public readonly bool downloadRequired;
        public readonly bool downloadStarted;
        public readonly bool requestingLoginInfo;
        public readonly RedeemStage redeemStage;
        public readonly int suggestionCount;

        public HotReloadRunTabState(
            bool spinnerActive, 
            string indicationIconPath,
            bool requestingDownloadAndRun,
            bool starting,
            bool stopping,
            bool running,
            Tuple<float, string> startupProgress,
            string indicationStatusText,
            LoginStatusResponse loginStatus,
            bool downloadRequired,
            bool downloadStarted,
            bool requestingLoginInfo,
            RedeemStage redeemStage,
            int suggestionCount
        ) {
            this.spinnerActive = spinnerActive;
            this.indicationIconPath = indicationIconPath;
            this.requestingDownloadAndRun = requestingDownloadAndRun;
            this.starting = starting;
            this.stopping = stopping;
            this.running = running;
            this.startupProgress = startupProgress;
            this.indicationStatusText = indicationStatusText;
            this.loginStatus = loginStatus;
            this.downloadRequired = downloadRequired;
            this.downloadStarted = downloadStarted;
            this.requestingLoginInfo = requestingLoginInfo;
            this.redeemStage = redeemStage;
            this.suggestionCount = suggestionCount;
        }

        public static HotReloadRunTabState Current => new HotReloadRunTabState(
            spinnerActive: EditorIndicationState.SpinnerActive,
            indicationIconPath: EditorIndicationState.IndicationIconPath,
            requestingDownloadAndRun: EditorCodePatcher.RequestingDownloadAndRun,
            starting: EditorCodePatcher.Starting,
            stopping: EditorCodePatcher.Stopping,
            running: EditorCodePatcher.Running,
            startupProgress: EditorCodePatcher.StartupProgress,
            indicationStatusText: EditorIndicationState.IndicationStatusText,
            loginStatus: EditorCodePatcher.Status,
            downloadRequired: EditorCodePatcher.DownloadRequired,
            downloadStarted: EditorCodePatcher.DownloadStarted,
            requestingLoginInfo: EditorCodePatcher.RequestingLoginInfo,
            redeemStage: RedeemLicenseHelper.I.RedeemStage,
            suggestionCount: HotReloadTimelineHelper.Suggestions.Count
        );
    }

    internal struct LicenseErrorData {
        public readonly string description;
        public bool showBuyButton;
        public string buyButtonText;
        public readonly bool showLoginButton;
        public readonly string loginButtonText;
        public readonly bool showSupportButton;
        public readonly string supportButtonText;
        public readonly bool showManageLicenseButton;
        public readonly string manageLicenseButtonText;

        public LicenseErrorData(string description, bool showManageLicenseButton = false, string manageLicenseButtonText = "", string loginButtonText = "", bool showSupportButton = false, string supportButtonText = "", bool showBuyButton = false, string buyButtonText = "", bool showLoginButton = false) {
            this.description = description;
            this.showManageLicenseButton = showManageLicenseButton;
            this.manageLicenseButtonText = manageLicenseButtonText;
            this.loginButtonText = loginButtonText;
            this.showSupportButton = showSupportButton;
            this.supportButtonText = supportButtonText;
            this.showBuyButton = showBuyButton;
            this.buyButtonText = buyButtonText;
            this.showLoginButton = showLoginButton;
        }
    }
    
    internal class HotReloadRunTab : HotReloadTabBase {
        private static string _pendingEmail;
        private static string _pendingPassword;
        private string _pendingPromoCode;
        private bool _requestingActivatePromoCode;

        private static Tuple<string, MessageType> _activateInfoMessage;

        private HotReloadRunTabState currentState => _window.RunTabState;
        // Has Indie or Pro license (even if not currenctly active)
        public bool HasPayedLicense => currentState.loginStatus != null && (currentState.loginStatus.isIndieLicense || currentState.loginStatus.isBusinessLicense);
        public bool TrialLicense => currentState.loginStatus != null && (currentState.loginStatus?.isTrial == true);
        
        private Vector2 _patchedMethodsScrollPos;
        private Vector2 _runTabScrollPos;

        private string promoCodeError;
        private MessageType promoCodeErrorType;
        private bool promoCodeActivatedThisSession;
        
        public HotReloadRunTab(HotReloadWindow window) : base(window, Translations.UI.RunTabTitle, "forward", Translations.UI.RunTabTooltip) { }

        public override void OnGUI() {
            using(new EditorGUILayout.VerticalScope()) {
                OnGUICore();
            }
        }

        internal static bool ShouldRenderConsumption(HotReloadRunTabState currentState) => (currentState.running && !currentState.starting && !currentState.stopping && currentState.loginStatus?.isLicensed != true && currentState.loginStatus?.isFree != true && !EditorCodePatcher.LoginNotRequired) && !(currentState.loginStatus == null || currentState.loginStatus.isFree);
        
        void OnGUICore() {
            using (var scope = new EditorGUILayout.ScrollViewScope(_runTabScrollPos, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(Math.Max(HotReloadWindowStyles.windowScreenHeight, 800)), GUILayout.MaxWidth(Math.Max(HotReloadWindowStyles.windowScreenWidth, 800)))) {
                _runTabScrollPos.x = scope.scrollPosition.x;
                _runTabScrollPos.y = scope.scrollPosition.y;
                using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamiSection)) {
                    if (HotReloadWindowStyles.windowScreenWidth > Constants.UpgradeLicenseNoteHideWidth
                        && HotReloadWindowStyles.windowScreenHeight > Constants.UpgradeLicenseNoteHideHeight
                    ) {
                        RenderUpgradeLicenseNote(currentState, HotReloadWindowStyles.UpgradeLicenseButtonStyle);
                    }

                    var renderDebuggerInfo = Debugger.IsAttached && !CodePatcher.I.debuggerCompatibilityEnabled;
                    RenderIndicationPanel(!renderDebuggerInfo);
                    if (renderDebuggerInfo) {
                        RenderDebuggerAttachedInfo(false);
                    }
                    if (CanRenderBars(currentState)) {
                        RenderBars(currentState);
                        // clear red dot next time button shows
                        HotReloadState.ShowingRedDot = false;
                    }
                }
            }

            // At the end to not fuck up rendering https://answers.unity.com/questions/400454/argumentexception-getting-control-0s-position-in-a-1.html
            var renderStart = !EditorCodePatcher.Running && !EditorCodePatcher.Starting && !currentState.requestingDownloadAndRun && currentState.redeemStage == RedeemStage.None;
            var e = Event.current;
            if (renderStart && e.type == EventType.KeyUp
                && (e.keyCode == KeyCode.Return
                    || e.keyCode == KeyCode.KeypadEnter)
            ) {
                EditorCodePatcher.DownloadAndRun().Forget();
            }
        }

        internal static void RenderUpgradeLicenseNote(HotReloadRunTabState currentState, GUIStyle style) {
            var isIndie = RedeemLicenseHelper.I.RegistrationOutcome == RegistrationOutcome.Indie
                || EditorCodePatcher.licenseType == UnityLicenseType.UnityPersonalPlus;

            if (RedeemLicenseHelper.I.RegistrationOutcome == RegistrationOutcome.Business
                && currentState.loginStatus?.isBusinessLicense != true
                && EditorCodePatcher.Running
                && (PackageConst.IsAssetStoreBuild || HotReloadPrefs.RateAppShown)
            ) {
                // Warn asset store users they need to buy a business license
                // Website users get reminded after using Hot Reload for 5+ days
                RenderBusinessLicenseInfo(style);
            } else if (isIndie
                && HotReloadPrefs.RateAppShown
                && !PackageConst.IsAssetStoreBuild
                && EditorCodePatcher.Running
                && currentState.loginStatus?.isBusinessLicense != true
                && currentState.loginStatus?.isIndieLicense != true
            ) {
                // Reminder users they need to buy an indie license
                RenderIndieLicenseInfo(style);
            }
        }
        
        internal static bool CanRenderBars(HotReloadRunTabState currentState) {
            if (Debugger.IsAttached && !CodePatcher.I.debuggerCompatibilityEnabled) {
                return false;
            }
            return HotReloadWindowStyles.windowScreenHeight > Constants.EventsListHideHeight
                && HotReloadWindowStyles.windowScreenWidth > Constants.EventsListHideWidth
                && !currentState.starting
                && !currentState.stopping
                && !currentState.requestingDownloadAndRun
            ;
        }
        
        static Texture2D GetFoldoutIcon(AlertEntry alertEntry) {
            InvertibleIcon alertIcon = InvertibleIcon.FoldoutClosed;
            if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry)) {
                alertIcon = InvertibleIcon.FoldoutOpen;
            }
            return GUIHelper.GetInvertibleIcon(alertIcon);
        }
        
        static void ToggleEntry(AlertEntry alertEntry) {
            if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry)) {
                HotReloadTimelineHelper.expandedEntries.Remove(alertEntry);
            } else {
                HotReloadTimelineHelper.expandedEntries.Add(alertEntry);
            }
        }
        
        static void RenderEntries(TimelineType timelineType) {
            List<AlertEntry> alertEntries;
            
            alertEntries = timelineType == TimelineType.Suggestions ? HotReloadTimelineHelper.Suggestions : HotReloadTimelineHelper.EventsTimeline;

            bool skipChildren = false;
            for (int i = 0; i < alertEntries.Count; i++) {
                var alertEntry = alertEntries[i];
                if (i > HotReloadTimelineHelper.maxVisibleEntries && alertEntry.entryType != EntryType.Child) {
                    break;
                }
                if (timelineType != TimelineType.Suggestions) {
                    if (alertEntry.entryType != EntryType.Child
                        && !enabledFilters.Contains(alertEntry.alertType)
                    ) {
                        skipChildren = true;
                        continue;
                    } else if (alertEntry.entryType == EntryType.Child && skipChildren) {
                        continue;
                    } else {
                        skipChildren = false;
                    }
                }
                
                EntryType entryType = alertEntry.entryType;

                string title = $" {alertEntry.title}{(!string.IsNullOrEmpty(alertEntry.shortDescription) ? $": {alertEntry.shortDescription}": "")}";
                Texture2D icon = null;
                GUIStyle style;
                if (entryType != EntryType.Child) {
                    icon = GUIHelper.GetLocalIcon(HotReloadTimelineHelper.alertIconString[alertEntry.iconType]);
                }
                if (entryType == EntryType.Child) {
                    style = HotReloadWindowStyles.ChildBarStyle;
                } else if (entryType == EntryType.Foldout) {
                    style = HotReloadWindowStyles.FoldoutBarStyle;
                } else {
                    style = HotReloadWindowStyles.BarStyle;
                }

                Rect startRect;
                using (new EditorGUILayout.HorizontalScope()) {
                    GUILayout.Space(0);
                    Rect spaceRect = GUILayoutUtility.GetLastRect();
                    // entry header foldout arrow
                    if (entryType == EntryType.Foldout) {
                        GUI.Label(new Rect(spaceRect.x + 3, spaceRect.y, 20, 20), new GUIContent(GetFoldoutIcon(alertEntry)));
                    } else if (entryType == EntryType.Child) {
                        GUI.Label(new Rect(spaceRect.x + 26, spaceRect.y + 2, 20, 20), new GUIContent(GetFoldoutIcon(alertEntry)));
                    }
                    // a workaround to limit the width of the label
                    GUILayout.Label(new GUIContent(""), style);
                    startRect = GUILayoutUtility.GetLastRect();
                    GUI.Label(startRect, new GUIContent(title, icon), style);
                }

                bool clickableDescription = (alertEntry.title == Translations.Utility.UnsupportedChange || alertEntry.title == Translations.Utility.CompileError || alertEntry.title == Translations.Timeline.EventTitleFailedApplyingPatch) && alertEntry.alertData.alertEntryType != AlertEntryType.InlinedMethod;
                
                if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry) || alertEntry.alertType == AlertType.CompileError) {
                    using (new EditorGUILayout.VerticalScope()) {
                        using (new EditorGUILayout.HorizontalScope()) {
                            using (new EditorGUILayout.VerticalScope(entryType == EntryType.Child ? HotReloadWindowStyles.ChildEntryBoxStyle : HotReloadWindowStyles.EntryBoxStyle)) {
                                if (alertEntry.alertType == AlertType.Suggestion || !clickableDescription) {
                                    GUILayout.Label(alertEntry.description, HotReloadWindowStyles.LabelStyle);
                                }
                                if (alertEntry.actionData != null) {
                                    alertEntry.actionData.Invoke();
                                }
                                GUILayout.Space(5f);
                            }
                        }
                    }
                }
                
                // remove button
                if (timelineType == TimelineType.Suggestions && alertEntry.hasExitButton) {
                    var isClick = GUI.Button(new Rect(startRect.x + startRect.width - 20, startRect.y + 2, 20, 20), new GUIContent(GUIHelper.GetInvertibleIcon(InvertibleIcon.Close)), HotReloadWindowStyles.RemoveIconStyle);
                    if (isClick) {
                        HotReloadTimelineHelper.EventsTimeline.Remove(alertEntry);
                        var kind = HotReloadSuggestionsHelper.FindSuggestionKind(alertEntry);
                        if (kind != null) {
                            HotReloadSuggestionsHelper.SetSuggestionInactive((HotReloadSuggestionKind)kind);
                            if (kind == HotReloadSuggestionKind.EditorsWithoutHRRunning) {
                                HotReloadState.ShowedEditorsWithoutHR = true;
                            }
                        }
                        _instantRepaint = true;
                    }
                }

                // Extend background to whole entry
                var endRect = GUILayoutUtility.GetLastRect();
                if (GUI.Button(new Rect(startRect) { height = endRect.y - startRect.y + endRect.height}, new GUIContent(""), HotReloadWindowStyles.BarBackgroundStyle) && (entryType == EntryType.Child || entryType == EntryType.Foldout)) {
                    ToggleEntry(alertEntry);
                }
        
                if (alertEntry.alertType != AlertType.Suggestion && HotReloadWindowStyles.windowScreenWidth > 400 && entryType != EntryType.Child) {
                    using (new EditorGUILayout.HorizontalScope()) {
                        var ago = (DateTime.Now - alertEntry.timestamp);
                        GUI.Label(new Rect(startRect.x + startRect.width - 60, startRect.y, 80, 20), ago.TotalMinutes < 1 ? "now" : $"{(ago.TotalHours > 1 ? $"{Math.Floor(ago.TotalHours)} h " : string.Empty)}{ago.Minutes} min", HotReloadWindowStyles.TimestampStyle);
                    }
                }
                
                GUILayout.Space(1f);
            }
            if (timelineType != TimelineType.Suggestions && HotReloadTimelineHelper.GetRunTabTimelineEventCount() > 40) { 
                GUILayout.Space(3f);
                GUILayout.Label(Constants.Only40EntriesShown, HotReloadWindowStyles.EmptyListText);
            }
        }

        private static List<AlertType> _enabledFilters;
        private static List<AlertType> enabledFilters {
            get {
                if (_enabledFilters == null) {
                    _enabledFilters = new List<AlertType>();
                }
                
                if (HotReloadPrefs.RunTabUnsupportedChangesFilter && !_enabledFilters.Contains(AlertType.UnsupportedChange))
                    _enabledFilters.Add(AlertType.UnsupportedChange);
                if (!HotReloadPrefs.RunTabUnsupportedChangesFilter && _enabledFilters.Contains(AlertType.UnsupportedChange))
                    _enabledFilters.Remove(AlertType.UnsupportedChange);
                
                if (HotReloadPrefs.RunTabCompileErrorFilter && !_enabledFilters.Contains(AlertType.CompileError))
                    _enabledFilters.Add(AlertType.CompileError);
                if (!HotReloadPrefs.RunTabCompileErrorFilter && _enabledFilters.Contains(AlertType.CompileError))
                    _enabledFilters.Remove(AlertType.CompileError);
                
                if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter && !_enabledFilters.Contains(AlertType.PartiallySupportedChange))
                    _enabledFilters.Add(AlertType.PartiallySupportedChange);
                if (!HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter && _enabledFilters.Contains(AlertType.PartiallySupportedChange))
                    _enabledFilters.Remove(AlertType.PartiallySupportedChange);
                
                if (HotReloadPrefs.RunTabUndetectedPatchesFilter && !_enabledFilters.Contains(AlertType.UndetectedChange))
                    _enabledFilters.Add(AlertType.UndetectedChange);
                if (!HotReloadPrefs.RunTabUndetectedPatchesFilter && _enabledFilters.Contains(AlertType.UndetectedChange))
                    _enabledFilters.Remove(AlertType.UndetectedChange);
                
                if (HotReloadPrefs.RunTabAppliedPatchesFilter && !_enabledFilters.Contains(AlertType.AppliedChange))
                    _enabledFilters.Add(AlertType.AppliedChange);
                if (!HotReloadPrefs.RunTabAppliedPatchesFilter && _enabledFilters.Contains(AlertType.AppliedChange))
                    _enabledFilters.Remove(AlertType.AppliedChange);
                    
                return _enabledFilters;
            }
        }
        
        private Vector2 suggestionsScroll;
        static GUILayoutOption[] timelineButtonOptions = new[] { GUILayout.Height(27), GUILayout.Width(100) };

        internal static void RenderBars(HotReloadRunTabState currentState) {
            if (currentState.suggestionCount > 0) {
                GUILayout.Space(5f);

                using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.Section)) {
                    using (new EditorGUILayout.VerticalScope()) {
                        HotReloadPrefs.RunTabEventsSuggestionsFoldout = EditorGUILayout.Foldout(HotReloadPrefs.RunTabEventsSuggestionsFoldout, "", true, HotReloadWindowStyles.CustomFoldoutStyle);
                        GUILayout.Space(-23);
                        if (GUILayout.Button(string.Format(Translations.Timeline.LabelSuggestionsFormat, currentState.suggestionCount.ToString()), HotReloadWindowStyles.ClickableLabelBoldStyle, GUILayout.Height(27))) {
                            HotReloadPrefs.RunTabEventsSuggestionsFoldout = !HotReloadPrefs.RunTabEventsSuggestionsFoldout;
                        }
                        if (HotReloadPrefs.RunTabEventsSuggestionsFoldout) {
                            using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.Scroll)) {
                                RenderEntries(TimelineType.Suggestions);
                            }
                        }
                    }
                }
            }
            GUILayout.Space(5f);

            using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.Section)) {
                using (new EditorGUILayout.VerticalScope()) {
                    HotReloadPrefs.RunTabEventsTimelineFoldout = EditorGUILayout.Foldout(HotReloadPrefs.RunTabEventsTimelineFoldout, "", true, HotReloadWindowStyles.CustomFoldoutStyle);
                    GUILayout.Space(-23);
                    if (GUILayout.Button(Translations.Timeline.LabelTimeline, HotReloadWindowStyles.ClickableLabelBoldStyle, timelineButtonOptions)) {
                        HotReloadPrefs.RunTabEventsTimelineFoldout = !HotReloadPrefs.RunTabEventsTimelineFoldout;
                    }
                    if (HotReloadPrefs.RunTabEventsTimelineFoldout) {
                        GUILayout.Space(-10);
                        var noteShown = HotReloadTimelineHelper.GetRunTabTimelineEventCount() == 0 || !currentState.running;
                        using (new EditorGUILayout.HorizontalScope()) {
                            if (noteShown) {
                                GUILayout.Space(2f);
                                using (new EditorGUILayout.VerticalScope()) {
                                    GUILayout.Space(2f);
                                    string text;
                                    if (currentState.redeemStage != RedeemStage.None) {
                                        text = Translations.Timeline.MessageCompleteRegistration;
                                    } else if (!currentState.running) {
                                        text = Translations.Timeline.MessageUseStartButton;
                                    } else if (enabledFilters.Count < 4 && HotReloadTimelineHelper.EventsTimeline.Count != 0) {
                                        text = Translations.Timeline.MessageEnableFilters;
                                    } else {
                                        text = Translations.Timeline.MessageMakeCodeChanges;
                                    }
                                    GUILayout.Label(text, HotReloadWindowStyles.EmptyListText);
                                }
                                GUILayout.FlexibleSpace();
                            } else {
                                GUILayout.FlexibleSpace();
                                if (HotReloadTimelineHelper.EventsTimeline.Count > 0 && GUILayout.Button(Translations.Common.ButtonClear)) {
                                    HotReloadTimelineHelper.ClearEntries();
                                    if (HotReloadWindow.Current) {
                                        HotReloadWindow.Current.Repaint();
                                    }
                                }
                                GUILayout.Space(3);
                            }
                        }
                        if (!noteShown) {
                            GUILayout.Space(2f);
                            using (new EditorGUILayout.VerticalScope()) {
                                RenderEntries(TimelineType.Timeline);
                            }
                        }
                    }
                }
            }
        }
            
        internal static void RenderConsumption(LoginStatusResponse loginStatus) {
            if (loginStatus == null) {
                return;
            }
            EditorGUILayout.Space();
            
            EditorGUILayout.LabelField(Translations.License.TitleHotReloadLimited, HotReloadWindowStyles.H3CenteredTitleStyle);
            EditorGUILayout.Space();
            if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.NetworkUnreachable) {
                EditorGUILayout.HelpBox(Translations.Errors.ErrorNetworkIssue, MessageType.Warning);
            } else if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError) {
                EditorGUILayout.HelpBox(Translations.Errors.ErrorContactSupport, MessageType.Error);
            } else if (loginStatus.freeSessionFinished) {
                var now = DateTime.UtcNow;
                var sessionRefreshesAt = (now.AddDays(1).Date - now).Add(TimeSpan.FromMinutes(5));
                var sessionRefreshString = sessionRefreshesAt.Hours > 0 ? 
                    string.Format(Translations.Miscellaneous.DailySessionNextSessionHours, sessionRefreshesAt.Hours, sessionRefreshesAt.Minutes) : 
                    string.Format(Translations.Miscellaneous.DailySessionNextSessionMinutes, sessionRefreshesAt.Minutes);
                HotReloadGUIHelper.HelpBox(sessionRefreshString, MessageType.Warning, fontSize: 11);
            } else if (loginStatus.freeSessionRunning && loginStatus.freeSessionEndTime != null) {
                var sessionEndsAt = loginStatus.freeSessionEndTime.Value - DateTime.Now;
                var sessionString = sessionEndsAt.Hours > 0 ? 
                    string.Format(Translations.Miscellaneous.DailySessionTimeHoursLeft, sessionEndsAt.Hours, sessionEndsAt.Minutes) : 
                    string.Format(Translations.Miscellaneous.DailySessionTimeMinutesLeft, sessionEndsAt.Minutes);
                HotReloadGUIHelper.HelpBox(sessionString, MessageType.Info, fontSize: 11);
            } else if (loginStatus.freeSessionEndTime == null) {
                HotReloadGUIHelper.HelpBox(Translations.Miscellaneous.DailySessionStart, MessageType.Info, fontSize: 11);
            }
        }

        static bool _repaint;
        static bool _instantRepaint;
        static DateTime _lastRepaint;
        private EditorIndicationState.IndicationStatus _lastStatus;
        public override void Update() {
            if (EditorIndicationState.SpinnerActive) {
                _repaint = true;
            }
            if (EditorCodePatcher.DownloadRequired) {
                _repaint = true;
            }
            if (EditorIndicationState.IndicationIconPath == Spinner.SpinnerIconPath) {
                _repaint = true;
            }
            try {
                // workaround: hovering over non-buttons doesn't repain by default
                if (EditorWindow.mouseOverWindow == HotReloadWindow.Current) {
                    _repaint = true;
                }
                if (EditorWindow.mouseOverWindow
                    && EditorWindow.mouseOverWindow?.GetType() == typeof(PopupWindow)
                    && HotReloadEventPopup.I.open
                ) {
                    _repaint = true;
                }
            } catch (NullReferenceException) {
                // Unity randomly throws nullrefs when EditorWindow.mouseOverWindow gets accessed
            }
            if (_repaint && DateTime.UtcNow - _lastRepaint > TimeSpan.FromMilliseconds(33)) {
                _repaint = false;
                _instantRepaint = true;
            }
            // repaint on status change
            var status = EditorIndicationState.CurrentIndicationStatus;
            if (_lastStatus != status) {
                _lastStatus = status;
                _instantRepaint = true;
            }
            if (_instantRepaint) {
                Repaint();
                HotReloadEventPopup.I.Repaint();
                _instantRepaint = false;
                _repaint = false;
                _lastRepaint = DateTime.UtcNow;
            }
        }
        
        public static void RepaintInstant() {
            _instantRepaint = true;
        }

        private void RenderRecompileButton() {
            string recompileText = HotReloadWindowStyles.windowScreenWidth > Constants.RecompileButtonTextHideWidth ? Translations.UI.RecompileButtonLabel : "";
            var recompileButton = new GUIContent(recompileText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Recompile));
            if (!GUILayout.Button(recompileButton, HotReloadWindowStyles.RecompileButton)) {
                return;
            }
            RecompileWithChecks();
        }

        public static void RecompileWithChecks() {
            var firstDialoguePass = HotReloadPrefs.RecompileDialogueShown
                || EditorUtility.DisplayDialog(
                    title: Translations.Dialogs.DialogTitleRecompile,
                    message: Translations.Dialogs.DialogMessageRecompile,
                    ok: Translations.Common.ButtonRecompile.Trim(),
                    cancel: Translations.Common.ButtonNotNow);
            HotReloadPrefs.RecompileDialogueShown = true;
            if (!firstDialoguePass) {
                return;
            }
            if (!ConfirmExitPlaymode(Translations.Dialogs.DialogMessageStopPlayMode)) {
                return;
            }
            Recompile();
        }

        #if UNITY_2020_1_OR_NEWER
        public static void SwitchToDebugMode() {
            CompilationPipeline.codeOptimization = CodeOptimization.Debug;
            HotReloadRunTab.Recompile();
            HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods);
        }
        #endif

        public static bool ConfirmExitPlaymode(string message) {
            return !Application.isPlaying
                || EditorUtility.DisplayDialog(
                    title: Translations.Dialogs.DialogTitleStopPlayMode,
                    message: message,
                    ok: Translations.Common.ButtonStopAndRecompile,
                    cancel: Translations.Common.ButtonCancel);
        }

        public static bool recompiling;
        public static void Recompile() {
            recompiling = true;
            EditorApplication.isPlaying = false;

            CompileMethodDetourer.Reset();
            AssetDatabase.Refresh();
            // This forces the recompilation if no changes were made.
            // This is better UX because otherwise the recompile button is unresponsive
            // which can be extra annoying if there are compile error entries in the list
            if (!EditorApplication.isCompiling) {
                CompilationPipeline.RequestScriptCompilation();
            }
        }
        
        private void RenderIndicationButtons() {
            if (currentState.requestingDownloadAndRun || currentState.starting || currentState.stopping || currentState.redeemStage != RedeemStage.None) {
                return;
            }
            
            if (!currentState.running && (currentState.startupProgress?.Item1 ?? 0) == 0) {
                string startText = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? Translations.UI.StartButtonLabel : "";
                if (GUILayout.Button(new GUIContent(startText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Start)), HotReloadWindowStyles.StartButton)) {
                    EditorCodePatcher.DownloadAndRun().Forget();
                }
            } else if (currentState.running && !currentState.starting) {
                if (HotReloadWindowStyles.windowScreenWidth > 150) {
                    RenderRecompileButton();
                }
                string stopText = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? Translations.UI.StopButtonLabel : "";
                if (GUILayout.Button(new GUIContent(stopText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Stop)), HotReloadWindowStyles.StopButton)) {
                    if (!EditorCodePatcher.StoppedServerRecently()) {
                        EditorCodePatcher.StopCodePatcher().Forget();
                    }
                }
            }
        }

        void RenderIndicationPanel(bool renderLicenseInfo = true) {
            using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBox)) {
                RenderIndication();
                if (HotReloadWindowStyles.windowScreenWidth > Constants.IndicationTextHideWidth) {
                    GUILayout.FlexibleSpace();
                }
                RenderIndicationButtons();
                if (HotReloadWindowStyles.windowScreenWidth <= Constants.IndicationTextHideWidth) {
                    GUILayout.FlexibleSpace();
                }
            }
            if (currentState.requestingDownloadAndRun || currentState.starting) {
                RenderProgressBar();
                if (EditorCodePatcher.serverDownloader.Attempts > 0) {
                    RenderManualDownloadSection();
                }
            } 

            if (HotReloadWindowStyles.windowScreenWidth > Constants.ConsumptionsHideWidth
                && HotReloadWindowStyles.windowScreenHeight > Constants.ConsumptionsHideHeight
                && renderLicenseInfo
            ) {
                RenderLicenseInfo(currentState);
            }
        }

        bool copiedPath = false;
        bool openedDownloadUrl = false;
        void RenderManualDownloadSection() {
            var downloadUrl = ServerDownloader.GetDownloadUrl(HotReloadCli.controller);
            var downloadPath = EditorCodePatcher.serverDownloader.GetBinaryPath(HotReloadCli.controller);

            EditorGUILayout.Space();
            HotReloadGUIHelper.HelpBox(
                Translations.Timeline.ManualDownloadWarning,
                MessageType.Warning, 11);

            HotReloadGUIHelper.HelpBox(
                string.Format(Translations.Timeline.ManualDownloadInfo, downloadPath),
                MessageType.Info, 11);

            using (new EditorGUILayout.HorizontalScope()) {
                if (GUILayout.Button(Translations.Timeline.ManualDownloadButtonCopyToClipboard + (copiedPath ? " ✓" : ""))) {
                    GUIUtility.systemCopyBuffer = downloadPath;
                    copiedPath = true;
                }
                if (GUILayout.Button(Translations.Timeline.ManualDownloadButtonOpenDownloadUrl + (openedDownloadUrl ? " ✓" : ""))) {
                    Application.OpenURL(downloadUrl);
                    openedDownloadUrl = true;
                }
            }
            if (GUILayout.Button(Translations.Timeline.ManualDownloadButtonComplete)) {
                EditorCodePatcher.downloadCancelToken?.Cancel();
                copiedPath = false;
                openedDownloadUrl = false;
            }
            OpenURLButton.Render(Translations.Timeline.ManualDownloadButtonContactSupport, Constants.ContactURL);
            EditorGUILayout.Space();
        }

        internal static void RenderLicenseInfo(HotReloadRunTabState currentState) {
            var showRedeem = currentState.redeemStage != RedeemStage.None;
            var showConsumptions = ShouldRenderConsumption(currentState);
            if (!showConsumptions && !showRedeem) {
                return;
            }
            using (new EditorGUILayout.VerticalScope()) {
                // space needed only for consumptions because of Stop/Start button's margin
                if (showConsumptions) {
                    GUILayout.Space(6);
                }
                using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.Section)) {
                    if (showRedeem) {
                        RedeemLicenseHelper.I.RenderStage(currentState);
                    } else {
                        RenderConsumption(currentState.loginStatus);
                        GUILayout.Space(10);
                        RenderLicenseInfo(currentState, currentState.loginStatus);
                        RenderLicenseButtons(currentState);
                        GUILayout.Space(10);
                    }
                }
                GUILayout.Space(6);
            }
        }
        
        private Spinner _spinner = new Spinner(85);
        private void RenderIndication() {
            using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.IndicationBox)) {
                // icon box
                if (HotReloadWindowStyles.windowScreenWidth <= Constants.IndicationTextHideWidth) {
                    GUILayout.FlexibleSpace();
                }

                using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.IndicationHelpBox)) {
                    var text = HotReloadWindowStyles.windowScreenWidth > Constants.IndicationTextHideWidth ? $"  {currentState.indicationStatusText}" : "";
                    if (currentState.indicationIconPath == Spinner.SpinnerIconPath) {
                        GUILayout.Label(new GUIContent(text, _spinner.GetIcon()), style: HotReloadWindowStyles.IndicationIcon);
                    } else if (currentState.indicationIconPath != null) {
                        var style = HotReloadWindowStyles.IndicationIcon;
                        if (HotReloadTimelineHelper.alertIconString.ContainsValue(currentState.indicationIconPath)) {
                            style = HotReloadWindowStyles.IndicationAlertIcon;
                        }
                        GUILayout.Label(new GUIContent(text, GUIHelper.GetLocalIcon(currentState.indicationIconPath)), style);
                    }
                } 
            }
        }
        
        static GUIStyle _openSettingsStyle;
        static GUIStyle openSettingsStyle => _openSettingsStyle ?? (_openSettingsStyle = new GUIStyle(GUI.skin.button) {
            fontStyle = FontStyle.Normal,
            fixedHeight = 25,
        });
        
        static GUILayoutOption[] _bigButtonHeight;
        public static GUILayoutOption[] bigButtonHeight => _bigButtonHeight ?? (_bigButtonHeight = new [] {GUILayout.Height(25)});
        
        private static GUIContent indieLicenseContent;
        private static GUIContent businessLicenseContent;

        internal static void RenderLicenseStatusInfo(HotReloadRunTabState currentState, LoginStatusResponse loginStatus, bool allowHide = true, bool verbose = false) {
            string message = null;
            MessageType messageType = default(MessageType);
            Action customGUI = null;
            GUIContent content = null;
            if (loginStatus == null) {
                // no info
            } else if (loginStatus.lastLicenseError != null) {
                messageType = !loginStatus.freeSessionFinished ? MessageType.Warning : MessageType.Error;
                message = GetMessageFromError(currentState, loginStatus.lastLicenseError);
            } else if (loginStatus.isTrial && !PackageConst.IsAssetStoreBuild) {
                message = string.Format(Translations.UI.TrialLicenseMessage, loginStatus.licenseExpiresAt.ToShortDateString());
                messageType = MessageType.Info;
            } else if (loginStatus.isIndieLicense) {
                if (verbose) {
                    message = Translations.UI.IndieLicenseMessage;
                    messageType = MessageType.Info;
                    customGUI = () => {
                        if (loginStatus.licenseExpiresAt.Date != DateTime.MaxValue.Date) {
                            EditorGUILayout.LabelField(string.Format(Translations.UI.LicenseRenewalMessage, loginStatus.licenseExpiresAt.ToShortDateString()));
                            EditorGUILayout.Space();
                        }
                        using (new GUILayout.HorizontalScope()) {
                            HotReloadAboutTab.manageLicenseButton.OnGUI();
                            HotReloadAboutTab.manageAccountButton.OnGUI();
                        }
                        EditorGUILayout.Space();
                    };
                    if (indieLicenseContent == null) {
                        indieLicenseContent = new GUIContent(message, EditorGUIUtility.FindTexture("TestPassed"));
                    }
                    content = indieLicenseContent;
                }
            } else if (loginStatus.isBusinessLicense) {
                if (verbose) {
                    message = Translations.UI.BusinessLicenseMessage;
                    messageType = MessageType.Info;
                    if (businessLicenseContent == null) {
                        businessLicenseContent = new GUIContent(message, EditorGUIUtility.FindTexture("TestPassed"));
                    }
                    content = businessLicenseContent;
                    customGUI = () => {
                        using (new GUILayout.HorizontalScope()) {
                            HotReloadAboutTab.manageLicenseButton.OnGUI();
                            HotReloadAboutTab.manageAccountButton.OnGUI();
                        }
                        EditorGUILayout.Space();
                    };
                }
            }

            if (messageType != MessageType.Info && HotReloadPrefs.ErrorHidden && allowHide) {
                return;
            }
            if (message != null) {
                if (messageType != MessageType.Info) {
                    using(new EditorGUILayout.HorizontalScope()) {
                        EditorGUILayout.HelpBox(message, messageType);
                        var style = HotReloadWindowStyles.HideButtonStyle;
                        if (Event.current.type == EventType.Repaint) {
                            style.fixedHeight = GUILayoutUtility.GetLastRect().height;
                        }
                        if (allowHide) {
                            if (GUILayout.Button(Translations.Common.ButtonHide, style)) {
                                HotReloadPrefs.ErrorHidden = true;
                            }
                        }
                    }
                } else if (content != null) {
                    EditorGUILayout.LabelField(content);
                    EditorGUILayout.Space();
                } else {
                    EditorGUILayout.LabelField(message);
                    EditorGUILayout.Space();
                }
                customGUI?.Invoke();
            }
        }

        internal static void RenderBusinessLicenseInfo(GUIStyle style) {
            GUILayout.Space(8);
            using (new EditorGUILayout.HorizontalScope()) {
                EditorGUILayout.HelpBox(Translations.License.LicenseErrorAssetStorePro, MessageType.Info);
                if (Event.current.type == EventType.Repaint) {
                    style.fixedHeight = GUILayoutUtility.GetLastRect().height;
                }
                if (GUILayout.Button(Translations.Common.ButtonUpgrade, style)) {
                    Application.OpenURL(Constants.ProductPurchaseBusinessURL);
                }
            }
        }
        
        internal static void RenderDebuggerAttachedInfo(bool isPopup) {
            GUILayout.Space(8);
            var autoRefreshDisabled = AutoRefreshSettingChecker.IsUserAutoRefreshDisabled();
            var msg = autoRefreshDisabled ? Translations.Suggestions.DebuggerAttachedMessagePaused : Translations.Suggestions.DebuggerAttachedMessageAutoRecompile;
            using (new EditorGUILayout.VerticalScope()) {
                var _fontSize = EditorStyles.helpBox.fontSize;
                try {
                    EditorStyles.helpBox.fontSize = 12;
                    // empty label field to measure width
                    EditorGUILayout.LabelField(GUIContent.none, new GUILayoutOption[]{GUILayout.Height(0)});
                    var lastRectWidth = GUILayoutUtility.GetLastRect().width;
                    EditorStyles.helpBox.fixedHeight = EditorStyles.helpBox.CalcHeight(new GUIContent(msg), lastRectWidth) + 15;
                    EditorStyles.helpBox.fixedWidth = lastRectWidth;
                    EditorGUILayout.HelpBox(msg, MessageType.Info);
                    // to account for added height
                    GUILayout.Space(isPopup ? 45 : 30);
                    using (new EditorGUILayout.HorizontalScope()) {
                        if (GUILayout.Button("Docs")) {
                            Application.OpenURL(Constants.DebuggerURL);
                        }
                        if (GUILayout.Button("Open Settings") && HotReloadWindow.Current) {
                            HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
                        }
                    }
                } finally {
                    EditorStyles.helpBox.fixedHeight = 0;
                    EditorStyles.helpBox.fixedWidth = 0;
                    EditorStyles.helpBox.fontSize = _fontSize;
                }
            }
        }
        
        internal static void RenderIndieLicenseInfo(GUIStyle style) {
            GUILayout.Space(8);
            using (new EditorGUILayout.HorizontalScope()) {
                EditorGUILayout.HelpBox(Translations.License.LicenseErrorUnityPlusIndie, MessageType.Info);
                if (Event.current.type == EventType.Repaint) {
                    style.fixedHeight = GUILayoutUtility.GetLastRect().height;
                }
                if (GUILayout.Button(Translations.Common.ButtonUpgrade, style)) {
                    Application.OpenURL(Constants.ProductPurchaseURL);
                }
            }
        }

        internal static Dictionary<string, LicenseErrorData> _licenseErrorData;
        internal static Dictionary<string, LicenseErrorData> LicenseErrorData => _licenseErrorData ?? (_licenseErrorData = new Dictionary<string, LicenseErrorData> {
            { "DeviceNotLicensedException", new LicenseErrorData(description: Translations.License.LicenseErrorDeviceInUse, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
            { "DeviceBlacklistedException", new LicenseErrorData(description: Translations.License.LicenseErrorDeviceBlacklisted) },
            { "DateHeaderInvalidException", new LicenseErrorData(description: Translations.License.LicenseErrorIncorrectClock) },
            { "DateTimeCheatingException", new LicenseErrorData(description: Translations.License.LicenseErrorIncorrectClock) },
            { "LicenseActivationException", new LicenseErrorData(description: Translations.License.LicenseErrorActivation, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
            { "LicenseDeletedException", new LicenseErrorData(description: Translations.License.LicenseErrorDeleted, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
            { "LicenseDisabledException", new LicenseErrorData(description: Translations.License.LicenseErrorDisabled, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
            { "LicenseExpiredException", new LicenseErrorData(description: Translations.License.LicenseErrorExpired, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonUpgradeLicense, showManageLicenseButton: true, manageLicenseButtonText: Translations.License.LicenseButtonManageLicense) },
            { "LicenseInactiveException", new LicenseErrorData(description: Translations.License.LicenseErrorInactive) },
            { "LocalLicenseException", new LicenseErrorData(description: Translations.License.LicenseErrorCorrupted) },
            // Note: obsolete
            { "MissingParametersException", new LicenseErrorData(description: "An account already exists for this device. Please login with your existing email/password.", showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense) },
            { "NetworkException", new LicenseErrorData(description: Translations.License.LicenseErrorNetwork, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
            { "TrialLicenseExpiredException", new LicenseErrorData(description: Translations.License.LicenseErrorTrialExpired, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonUpgradeLicense) },
            { "InvalidCredentialException", new LicenseErrorData(description: Translations.License.LicenseErrorInvalidCredentials) },
            // Note: activating free trial with email is not supported anymore. This error shouldn't happen which is why we should rather user the fallback
            // { "LicenseNotFoundException", new LicenseErrorData(description: "The account you're trying to access doesn't seem to exist yet. Please enter your email address to create a new account and receive a trial license.", showLoginButton: true, loginButtonText: CreateAccount) },
            { "LicenseIncompatibleException", new LicenseErrorData(description: Translations.License.LicenseErrorIncompatible, showManageLicenseButton: true, manageLicenseButtonText: Translations.License.LicenseButtonManageLicense) },
        });
        internal static LicenseErrorData defaultLicenseErrorData = new LicenseErrorData(description: Translations.License.LicenseErrorDefault, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport);

        internal static string GetMessageFromError(HotReloadRunTabState currentState, string error) {
            if (PackageConst.IsAssetStoreBuild && error == "TrialLicenseExpiredException") {
                return Translations.License.LicenseErrorAssetStorePro;
            }
            return GetLicenseErrorDataOrDefault(currentState, error).description;
        }
        
        internal static LicenseErrorData GetLicenseErrorDataOrDefault(HotReloadRunTabState currentState, string error) {
            if (currentState.loginStatus?.isFree == true) {
                return default(LicenseErrorData);
            }
            if (currentState.loginStatus == null || string.IsNullOrEmpty(error) && (!currentState.loginStatus.isLicensed || currentState.loginStatus.isTrial)) {
                return new LicenseErrorData(null, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense);
            }
            if (string.IsNullOrEmpty(error)) {
                return default(LicenseErrorData);
            }
            if (!LicenseErrorData.ContainsKey(error)) {
                return defaultLicenseErrorData;
            }
            return LicenseErrorData[error];
        }

        internal static void RenderBuyLicenseButton(string buyLicenseButton) {
            OpenURLButton.Render(buyLicenseButton, Constants.ProductPurchaseURL);
        }

        static void RenderLicenseActionButtons(HotReloadRunTabState currentState) {
            var errInfo = GetLicenseErrorDataOrDefault(currentState, currentState.loginStatus?.lastLicenseError);
            if (errInfo.showBuyButton || errInfo.showManageLicenseButton) {
                using(new EditorGUILayout.HorizontalScope()) {
                    if (errInfo.showBuyButton) {
                        RenderBuyLicenseButton(errInfo.buyButtonText);
                    }
                    if (errInfo.showManageLicenseButton && !HotReloadPrefs.ErrorHidden) {
                        OpenURLButton.Render(errInfo.manageLicenseButtonText, Constants.ManageLicenseURL);
                    }
                }
            }
            if (errInfo.showLoginButton && GUILayout.Button(errInfo.loginButtonText, openSettingsStyle)) {
                // show license section
                HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
                HotReloadWindow.Current.SettingsTab.FocusLicenseFoldout();
            }
            if (errInfo.showSupportButton && !HotReloadPrefs.ErrorHidden) {
                OpenURLButton.Render(errInfo.supportButtonText, Constants.ContactURL);
            }
            if (currentState.loginStatus?.lastLicenseError != null) {
                HotReloadAboutTab.reportIssueButton.OnGUI();
            }
        }
        
        internal static void RenderLicenseInfo(HotReloadRunTabState currentState, LoginStatusResponse loginStatus, bool verbose = false, bool allowHide = true, string overrideActionButton = null, bool showConsumptions = false) {
            HotReloadPrefs.ShowLogin = EditorGUILayout.Foldout(HotReloadPrefs.ShowLogin, Translations.License.TitleHotReloadLicense, true, HotReloadWindowStyles.FoldoutStyle);
            if (HotReloadPrefs.ShowLogin) {
                EditorGUILayout.Space();
                if ((loginStatus?.isLicensed != true && showConsumptions) && !(loginStatus == null || loginStatus.isFree)) {
                    RenderConsumption(loginStatus);
                }
                RenderLicenseStatusInfo(currentState, loginStatus: loginStatus, allowHide: allowHide, verbose: verbose);

                RenderLicenseInnerPanel(currentState, overrideActionButton: overrideActionButton);
                
                EditorGUILayout.Space();
                EditorGUILayout.Space();
            }
        }

        internal void RenderPromoCodes() {
            HotReloadPrefs.ShowPromoCodes = EditorGUILayout.Foldout(HotReloadPrefs.ShowPromoCodes, Translations.License.PromoCodesTitle, true, HotReloadWindowStyles.FoldoutStyle);
            if (!HotReloadPrefs.ShowPromoCodes) {
                return;
            }
            if (promoCodeActivatedThisSession) {
                EditorGUILayout.HelpBox(Translations.License.MessagePromoCodeActivated, MessageType.Info);
            } else {
                if (promoCodeError != null && promoCodeErrorType != MessageType.None) {
                    EditorGUILayout.HelpBox(promoCodeError, promoCodeErrorType);
                }
                EditorGUILayout.LabelField(Translations.Common.LabelPromoCode);
                _pendingPromoCode = EditorGUILayout.TextField(_pendingPromoCode);
                EditorGUILayout.Space();

                using (new EditorGUI.DisabledScope(_requestingActivatePromoCode)) {
                    if (GUILayout.Button(Translations.Common.ButtonActivatePromoCode, HotReloadRunTab.bigButtonHeight)) {
                        RequestActivatePromoCode().Forget();
                    }
                }
            }
            
            EditorGUILayout.Space();
            EditorGUILayout.Space();
        }
        
        private async Task RequestActivatePromoCode() {
            _requestingActivatePromoCode = true;
            try {
                var resp = await RequestHelper.RequestActivatePromoCode(_pendingPromoCode);
                if (resp != null && resp.error == null) {
                    promoCodeActivatedThisSession = true;
                } else {
                    var requestError = resp?.error ?? "Network error";
                    var errorType = ToErrorType(requestError);
                    promoCodeError = ToPrettyErrorMessage(errorType);
                    promoCodeErrorType = ToMessageType(errorType);
                }
            } finally {
                _requestingActivatePromoCode = false;
            }
        }

        PromoCodeErrorType ToErrorType(string error) {
            switch (error) {
                case "Input is missing":           return PromoCodeErrorType.MISSING_INPUT;
                case "only POST is supported":     return PromoCodeErrorType.INVALID_HTTP_METHOD;
                case "body is not a valid json":   return PromoCodeErrorType.BODY_INVALID;
                case "Promo code is not found":    return PromoCodeErrorType.PROMO_CODE_NOT_FOUND;
                case "Promo code already claimed": return PromoCodeErrorType.PROMO_CODE_CLAIMED;
                case "Promo code expired":         return PromoCodeErrorType.PROMO_CODE_EXPIRED;
                case "License not found":          return PromoCodeErrorType.LICENSE_NOT_FOUND;
                case "License is not a trial":     return PromoCodeErrorType.LICENSE_NOT_TRIAL;
                case "License already extended":   return PromoCodeErrorType.LICENSE_ALREADY_EXTENDED;
                case "conditionalCheckFailed":     return PromoCodeErrorType.CONDITIONAL_CHECK_FAILED;
            }
            if (error.Contains("Updating License Failed with error")) {
                return PromoCodeErrorType.UPDATING_LICENSE_FAILED;
            } else if (error.Contains("Unknown exception")) {
                return PromoCodeErrorType.UNKNOWN_EXCEPTION;
            } else if (error.Contains("Unsupported path")) {
                return PromoCodeErrorType.UNSUPPORTED_PATH;
            }
            return PromoCodeErrorType.NONE;
        }

        string ToPrettyErrorMessage(PromoCodeErrorType errorType) {
            var defaultMsg = Translations.Errors.ErrorPromoCodeActivation;
            switch (errorType) {
                case PromoCodeErrorType.MISSING_INPUT:
                case PromoCodeErrorType.INVALID_HTTP_METHOD:
                case PromoCodeErrorType.BODY_INVALID:
                case PromoCodeErrorType.UNKNOWN_EXCEPTION:
                case PromoCodeErrorType.UNSUPPORTED_PATH:
                case PromoCodeErrorType.LICENSE_NOT_FOUND:
                case PromoCodeErrorType.UPDATING_LICENSE_FAILED:
                case PromoCodeErrorType.LICENSE_NOT_TRIAL:
                    return defaultMsg;
                case PromoCodeErrorType.PROMO_CODE_NOT_FOUND:     return Translations.Errors.ErrorPromoCodeInvalid;
                case PromoCodeErrorType.PROMO_CODE_CLAIMED:       return Translations.Errors.ErrorPromoCodeUsed;
                case PromoCodeErrorType.PROMO_CODE_EXPIRED:       return Translations.Errors.ErrorPromoCodeExpired;
                case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return Translations.Errors.ErrorLicenseExtended;
                case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return Translations.Errors.ErrorPromoCodeActivation;
                case PromoCodeErrorType.NONE:                     return Translations.Errors.ErrorPromoCodeNetwork;
                default:                                          return defaultMsg;
            }
        }

        MessageType ToMessageType(PromoCodeErrorType errorType) {
            switch (errorType) {
                case PromoCodeErrorType.MISSING_INPUT:            return MessageType.Error;
                case PromoCodeErrorType.INVALID_HTTP_METHOD:      return MessageType.Error;
                case PromoCodeErrorType.BODY_INVALID:             return MessageType.Error;
                case PromoCodeErrorType.PROMO_CODE_NOT_FOUND:     return MessageType.Warning;
                case PromoCodeErrorType.PROMO_CODE_CLAIMED:       return MessageType.Warning;
                case PromoCodeErrorType.PROMO_CODE_EXPIRED:       return MessageType.Warning;
                case PromoCodeErrorType.LICENSE_NOT_FOUND:        return MessageType.Error;
                case PromoCodeErrorType.LICENSE_NOT_TRIAL:        return MessageType.Error;
                case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return MessageType.Warning;
                case PromoCodeErrorType.UPDATING_LICENSE_FAILED:  return MessageType.Error;
                case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return MessageType.Error;
                case PromoCodeErrorType.UNKNOWN_EXCEPTION:        return MessageType.Error;
                case PromoCodeErrorType.UNSUPPORTED_PATH:         return MessageType.Error;
                case PromoCodeErrorType.NONE:                     return MessageType.Error;
                default:                                          return MessageType.Error;
            }
        }

        public static void RenderLicenseButtons(HotReloadRunTabState currentState) {
            RenderLicenseActionButtons(currentState);
        }

        internal static void RenderLicenseInnerPanel(HotReloadRunTabState currentState, string overrideActionButton = null, bool renderLogout = true) {
            EditorGUILayout.LabelField(Translations.Common.LabelEmail);
            GUI.SetNextControlName("email");
            _pendingEmail = EditorGUILayout.TextField(string.IsNullOrEmpty(_pendingEmail) ? HotReloadPrefs.LicenseEmail : _pendingEmail);
            _pendingEmail = _pendingEmail.Trim();

            EditorGUILayout.LabelField(Translations.Common.LabelPassword);
            GUI.SetNextControlName("password");
            _pendingPassword = EditorGUILayout.PasswordField(string.IsNullOrEmpty(_pendingPassword) ? HotReloadPrefs.LicensePassword : _pendingPassword);
            
            RenderSwitchAuthMode();
            
            var e = Event.current;
            using(new EditorGUI.DisabledScope(currentState.requestingLoginInfo)) {
                var btnLabel = overrideActionButton;
                if (String.IsNullOrEmpty(overrideActionButton)) {
                    btnLabel = Translations.Common.ButtonLogin;
                }
                using (new EditorGUILayout.HorizontalScope()) {
                    var focusedControl = GUI.GetNameOfFocusedControl();
                    if (GUILayout.Button(btnLabel, bigButtonHeight)
                        || (focusedControl == "email" 
                            || focusedControl == "password") 
                        && e.type == EventType.KeyUp 
                        && (e.keyCode == KeyCode.Return 
                            || e.keyCode == KeyCode.KeypadEnter)
                    ) {
                        var error = ValidateEmail(_pendingEmail);
                        if (!string.IsNullOrEmpty(error)) {
                            _activateInfoMessage = new Tuple<string, MessageType>(error, MessageType.Warning);
                        } else if (string.IsNullOrEmpty(_pendingPassword)) {
                            _activateInfoMessage = new Tuple<string, MessageType>(Translations.Errors.ErrorEnterPassword, MessageType.Warning);
                        } else {
                            HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab));

                            _activateInfoMessage = null;
                            if (RedeemLicenseHelper.I.RedeemStage == RedeemStage.Login) {
                                RedeemLicenseHelper.I.FinishRegistration(RegistrationOutcome.Indie);
                            }
                            if (!EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Running) {
                                LoginOnDownloadAndRun(new LoginData(email: _pendingEmail, password: _pendingPassword)).Forget();
                            } else {
                                EditorCodePatcher.RequestLogin(_pendingEmail, _pendingPassword).Forget();
                            }
                        }
                    }
                    if (renderLogout) {
                        RenderLogout(currentState);
                    }
                }
            }
            if (_activateInfoMessage != null && (e.type == EventType.Layout || e.type == EventType.Repaint)) {
                EditorGUILayout.HelpBox(_activateInfoMessage.Item1, _activateInfoMessage.Item2);
            }
        }

        public static string ValidateEmail(string email) {
            if (string.IsNullOrEmpty(email)) {
                return Translations.Errors.ErrorEnterEmail;
            } else if (!EditorWindowHelper.IsValidEmailAddress(email)) {
                return Translations.Errors.ErrorValidEmail;
            } else if (email.Contains("+")) {
                return Translations.Errors.ErrorMailExtensions;
            }
            return null;
        }

        public static void RenderLogout(HotReloadRunTabState currentState) {
            if (currentState.loginStatus?.isLicensed != true) {
                return;
            }
            if (GUILayout.Button(Translations.Common.ButtonLogout, bigButtonHeight)) {
                HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab));
                if (!EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Running) {
                    LogoutOnDownloadAndRun().Forget();
                } else {
                    RequestLogout().Forget();
                }
            }
        }
        
        async static Task LoginOnDownloadAndRun(LoginData loginData = null) {
            var ok = await EditorCodePatcher.DownloadAndRun(loginData);
            if (ok && loginData != null) {
                HotReloadPrefs.ErrorHidden = false;
                HotReloadPrefs.LicenseEmail = loginData.email;
                HotReloadPrefs.LicensePassword = loginData.password;
            }
        }

        async static Task LogoutOnDownloadAndRun() {
            var ok = await EditorCodePatcher.DownloadAndRun();
            if (!ok) {
                return;
            }
            await RequestLogout();
        }

        private async static Task RequestLogout() {
            int i = 0;
            while (!EditorCodePatcher.Running && i < 100) {
                await Task.Delay(100);
                i++;
            }
            var resp = await RequestHelper.RequestLogout();
            if (!EditorCodePatcher.RequestingLoginInfo && resp != null) {
                EditorCodePatcher.HandleStatus(resp);
            }
        }

        private static void RenderSwitchAuthMode() {
            var color = EditorGUIUtility.isProSkin ? new Color32(0x3F, 0x9F, 0xFF, 0xFF) : new Color32(0x0F, 0x52, 0xD7, 0xFF); 
            if (HotReloadGUIHelper.LinkLabel(Translations.Miscellaneous.LinkForgotPassword, 12, FontStyle.Normal, TextAnchor.MiddleLeft, color)) {
                if (EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRecoverPassword, Translations.Dialogs.DialogMessageRecoverPassword, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel)) {
                    Application.OpenURL(Constants.ForgotPasswordURL);
                }
            }
        }
        
        Texture2D _greenTextureLight;
        Texture2D _greenTextureDark;
        Texture2D GreenTexture => EditorGUIUtility.isProSkin 
            ? _greenTextureDark ? _greenTextureDark : (_greenTextureDark = MakeTexture(0.5f))
            : _greenTextureLight ? _greenTextureLight : (_greenTextureLight = MakeTexture(0.85f));
        
        private void RenderProgressBar() {
            if (currentState.downloadRequired && !currentState.downloadStarted) {
                return;
            }
            
            using(var scope = new EditorGUILayout.VerticalScope(HotReloadWindowStyles.MiddleCenterStyle)) {
                float progress;
                var bg = HotReloadWindowStyles.ProgressBarBarStyle.normal.background;
                try {
                    HotReloadWindowStyles.ProgressBarBarStyle.normal.background = GreenTexture;
                    var barRect = scope.rect;

                    barRect.height = 25;
                    if (currentState.downloadRequired) {
                        barRect.width = barRect.width - 65;
                        using (new EditorGUILayout.HorizontalScope()) {
                            progress = EditorCodePatcher.DownloadProgress;
                            EditorGUI.ProgressBar(barRect, Mathf.Clamp(progress, 0f, 1f), "");
                            if (GUI.Button(new Rect(barRect) { x = barRect.x + barRect.width + 5, height = barRect.height, width = 60 }, new GUIContent(" Info", GUIHelper.GetLocalIcon("alert_info")))) {
                                Application.OpenURL(Constants.AdditionalContentURL);
                            }
                        }
                    } else {
                        progress = EditorCodePatcher.Stopping ? 1 : Mathf.Clamp(EditorCodePatcher.StartupProgress?.Item1 ?? 0f, 0f, 1f);
                        EditorGUI.ProgressBar(barRect, progress, "");
                    }
                    GUILayout.Space(barRect.height);
                } finally {
                    HotReloadWindowStyles.ProgressBarBarStyle.normal.background = bg;
                }
            }
        }

        private Texture2D MakeTexture(float maxHue) {
            var width = 11;
            var height = 11;
            Color[] pix = new Color[width * height];
            for (int y = 0; y < height; y++) {
                var middle = Math.Ceiling(height / (double)2);
                var maxGreen = maxHue;
                var yCoord = y + 1;
                var green = maxGreen - Math.Abs(yCoord - middle) * 0.02;
                for (int x = 0; x < width; x++) {
                    pix[y * width + x] = new Color(0.1f, (float)green, 0.1f, 1.0f);
                }
            }
            var result = new Texture2D(width, height);
            result.SetPixels(pix);
            result.Apply();
            return result;
        }
        
        
        /*
        [MenuItem("codepatcher/restart")]
        public static void TestRestart() {
            CodePatcherCLI.Restart(Application.dataPath, false);
        }
        */
        
    }

    internal static class HotReloadGUIHelper {
        public static bool LinkLabel(string labelText, int fontSize, FontStyle fontStyle, TextAnchor alignment, Color? color = null) {
            var stl = EditorStyles.label;

            // copy
            var origSize = stl.fontSize;
            var origStyle = stl.fontStyle;
            var origAnchor = stl.alignment;
            var origColor = stl.normal.textColor;

            // temporarily modify the built-in style
            stl.fontSize = fontSize;
            stl.fontStyle = fontStyle;
            stl.alignment = alignment;
            stl.normal.textColor = color ?? origColor;
            stl.active.textColor = color ?? origColor;
            stl.focused.textColor = color ?? origColor;
            stl.hover.textColor = color ?? origColor;

            try {
                return GUILayout.Button(labelText, stl);
            }  finally{
                // set the editor style (stl) back to normal
                stl.fontSize = origSize;
                stl.fontStyle = origStyle;
                stl.alignment = origAnchor;
                stl.normal.textColor = origColor;
                stl.active.textColor = origColor;
                stl.focused.textColor = origColor;
                stl.hover.textColor = origColor;
            }
        }

        public static void HelpBox(string message, MessageType type, int fontSize) {
            var _fontSize = EditorStyles.helpBox.fontSize;
            try {
                EditorStyles.helpBox.fontSize = fontSize;
                EditorGUILayout.HelpBox(message, type);
            } finally {
                EditorStyles.helpBox.fontSize = _fontSize;
            }
        }
    }

    internal enum PromoCodeErrorType {
        NONE,
        MISSING_INPUT,
        INVALID_HTTP_METHOD,
        BODY_INVALID,
        PROMO_CODE_NOT_FOUND,
        PROMO_CODE_CLAIMED,
        PROMO_CODE_EXPIRED,
        LICENSE_NOT_FOUND,
        LICENSE_NOT_TRIAL,
        LICENSE_ALREADY_EXTENDED,
        UPDATING_LICENSE_FAILED,
        CONDITIONAL_CHECK_FAILED,
        UNKNOWN_EXCEPTION,
        UNSUPPORTED_PATH,
    }

    internal class LoginData {
        public readonly string email;
        public readonly string password;

        public LoginData(string email, string password) {
            this.email = email;
            this.password = password;
        }
    }
}

