﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;

namespace SingularityGroup.HotReload.Editor {
    internal enum TimelineType {
        Suggestions,
        Timeline,
    }
    
    internal enum AlertType {
        Suggestion,
        UnsupportedChange,
        CompileError,
        PartiallySupportedChange,
        AppliedChange,
        UndetectedChange,
    }
    
    internal enum AlertEntryType {
        Error,
        Failure,
        InlinedMethod,
        PatchApplied,
        PartiallySupportedChange,
        UndetectedChange,
    }
    
    internal enum EntryType {
        Parent,
        Child,
        Standalone,
        Foldout,
    }
    
    internal class PersistedAlertData {
        public readonly AlertData[] alertDatas;

        public PersistedAlertData(AlertData[] alertDatas) {
            this.alertDatas = alertDatas;
        }
    }

    internal class AlertData {
        public readonly AlertEntryType alertEntryType;
        public readonly string errorString;
        public readonly string methodName;
        public readonly string methodSimpleName;
        public readonly PartiallySupportedChange partiallySupportedChange;
        public readonly EntryType entryType;
        public readonly bool detiled;
        public readonly DateTime createdAt;
        public readonly string[] patchedMembersDisplayNames;
        public readonly bool isCompile;

        public AlertData(AlertEntryType alertEntryType, DateTime createdAt, bool detiled = false, EntryType entryType = EntryType.Standalone, string errorString = null, string methodName = null, string methodSimpleName = null, PartiallySupportedChange partiallySupportedChange = default(PartiallySupportedChange), string[] patchedMembersDisplayNames = null, bool isCompile = false) {
            this.alertEntryType = alertEntryType;
            this.createdAt = createdAt;
            this.detiled = detiled;
            this.entryType = entryType;
            this.errorString = errorString;
            this.methodName = methodName;
            this.methodSimpleName = methodSimpleName;
            this.partiallySupportedChange = partiallySupportedChange;
            this.patchedMembersDisplayNames = patchedMembersDisplayNames;
            this.isCompile = isCompile;
        }
    }
    
    internal class AlertEntry {
        internal readonly AlertType alertType;
        internal readonly string title;
        internal readonly DateTime timestamp;
        internal readonly string description;
        [CanBeNull] internal readonly Action actionData;
        internal readonly AlertType iconType;
        internal readonly string shortDescription;
        internal readonly EntryType entryType;
        internal readonly AlertData alertData;
        internal readonly bool hasExitButton;

        internal AlertEntry(AlertType alertType, string title, string description, DateTime timestamp, string shortDescription = null, Action actionData = null, AlertType? iconType = null, EntryType entryType = EntryType.Standalone, AlertData alertData = default(AlertData), bool hasExitButton = true) {
            this.alertType = alertType;
            this.title = title;
            this.description = description;
            this.shortDescription = shortDescription;
            this.actionData = actionData;
            this.iconType = iconType ?? alertType;
            this.timestamp = timestamp;
            this.entryType = entryType;
            this.alertData = alertData;
            this.hasExitButton = hasExitButton;
        }
    }

    internal static class HotReloadTimelineHelper {
        internal const int maxVisibleEntries = 40;
        
        private static List<AlertEntry> eventsTimeline = new List<AlertEntry>();
        internal static List<AlertEntry> EventsTimeline => eventsTimeline;

        static readonly string filePath = Path.Combine(PackageConst.LibraryCachePath, "eventEntries.json");

        public static async Task InitPersistedEvents() {
            if (MultiplayerPlaymodeHelper.IsClone) {
                return;
            }
            if (!File.Exists(filePath)) {
                return;
            }
            var redDotShown = HotReloadState.ShowingRedDot;
            try {
                var persistedAlertData = await Task.Run(() => JsonConvert.DeserializeObject<PersistedAlertData>(File.ReadAllText(filePath)));
                eventsTimeline = new List<AlertEntry>(persistedAlertData.alertDatas.Length);
                for (int i = persistedAlertData.alertDatas.Length - 1; i >= 0; i--) {
                    AlertData alertData = persistedAlertData.alertDatas[i];
                    switch (alertData.alertEntryType) {
                        case AlertEntryType.Error:
                            CreateErrorEventEntry(errorString: alertData.errorString, entryType: alertData.entryType, createdAt: alertData.createdAt);
                            break;
#if UNITY_2020_1_OR_NEWER
                        case AlertEntryType.InlinedMethod:
                            CreateInlinedMethodsEntry(alertData.patchedMembersDisplayNames, alertData.entryType, alertData.createdAt);
                            break;
#endif
                        case AlertEntryType.Failure:
                            if (alertData.entryType == EntryType.Parent) {
                                CreateReloadFinishedWithWarningsEventEntry(createdAt: alertData.createdAt, patchedMembersDisplayNames: alertData.patchedMembersDisplayNames);
                            } else {
                                CreatePatchFailureEventEntry(errorString: alertData.errorString, methodName: alertData.methodName, methodSimpleName: alertData.methodSimpleName, entryType: alertData.entryType, createdAt: alertData.createdAt);
                            }
                            break;
                        case AlertEntryType.PatchApplied:
                            CreateReloadFinishedEventEntry(
                                createdAt: alertData.createdAt,
                                patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames,
                                isCompile: alertData.isCompile
                            );
                            break;
                        case AlertEntryType.PartiallySupportedChange:
                            if (alertData.entryType == EntryType.Parent) {
                                CreateReloadPartiallyAppliedEventEntry(createdAt: alertData.createdAt, patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames);
                            } else {
                                CreatePartiallyAppliedEventEntry(alertData.partiallySupportedChange, entryType: alertData.entryType, detailed: alertData.detiled, createdAt: alertData.createdAt);
                            }
                            break;
                        case AlertEntryType.UndetectedChange:
                            CreateReloadUndetectedChangeEventEntry(createdAt: alertData.createdAt);
                            break;
                    }
                }
            } catch (Exception e) {
                Log.Warning(Translations.Errors.WarningInitializingEventEntries, e);
            } finally {
                // Ensure red dot is not triggered for existing entries
                HotReloadState.ShowingRedDot = redDotShown;
            }
        }

        internal static async Task PersistTimeline() {
            if (MultiplayerPlaymodeHelper.IsClone) {
                return;
            }
            var persistedData = new PersistedAlertData(eventsTimeline.Where(x => x.alertType != AlertType.CompileError).Select(x => x.alertData).ToArray());
            try {
                await Task.Run(() => File.WriteAllText(path: filePath, contents: JsonConvert.SerializeObject(persistedData)));
            } catch (Exception e) {
                Log.Warning(Translations.Errors.WarningPersistingEventEntries, e);
            }
        }
        
        internal static void ClearPersistance() {
            if (MultiplayerPlaymodeHelper.IsClone) {
                return;
            }
            Task.Run(() => File.Delete(filePath));
            eventsTimeline = new List<AlertEntry>();
        }
        
        internal static readonly Dictionary<AlertType, string> alertIconString = new Dictionary<AlertType, string> {
            { AlertType.Suggestion, "Hot_Reload_alert_info" },
            { AlertType.UnsupportedChange, "Hot_Reload_warning" },
            { AlertType.CompileError, "Hot_Reload_error" },
            { AlertType.PartiallySupportedChange, "Hot_Reload_infos" },
            { AlertType.AppliedChange, "Hot_Reload_applied_patch" },
            { AlertType.UndetectedChange, "Hot_Reload_status_undetected" },
        };
        
#pragma warning disable CS0612 // obsolete
        public static Dictionary<PartiallySupportedChange, string> partiallySupportedChangeDescriptions => new Dictionary<PartiallySupportedChange, string> {
            {PartiallySupportedChange.LambdaClosure, Translations.Timeline.PartiallySupportedLambdaClosure},
            {PartiallySupportedChange.EditAsyncMethod, Translations.Timeline.PartiallySupportedEditAsyncMethod},
            {PartiallySupportedChange.AddMonobehaviourMethod, Translations.Timeline.PartiallySupportedAddMonobehaviourMethod},
            {PartiallySupportedChange.EditMonobehaviourField, Translations.Timeline.PartiallySupportedEditMonobehaviourField},
            {PartiallySupportedChange.EditCoroutine, Translations.Timeline.PartiallySupportedEditCoroutine},
            {PartiallySupportedChange.EditGenericFieldInitializer, Translations.Timeline.PartiallySupportedEditGenericFieldInitializer},
            {PartiallySupportedChange.AddEnumMember, Translations.Timeline.PartiallySupportedAddEnumMember},
            {PartiallySupportedChange.EditFieldInitializer, Translations.Timeline.PartiallySupportedEditFieldInitializer},
            {PartiallySupportedChange.AddMethodWithAttributes, Translations.Timeline.PartiallySupportedAddMethodWithAttributes},
            {PartiallySupportedChange.AddFieldWithAttributes, Translations.Timeline.PartiallySupportedAddFieldWithAttributes},
            {PartiallySupportedChange.GenericMethodInGenericClass, Translations.Timeline.PartiallySupportedGenericMethodInGenericClass},
            {PartiallySupportedChange.NewCustomSerializableField, Translations.Timeline.PartiallySupportedNewCustomSerializableField},
            {PartiallySupportedChange.MultipleFieldsEditedInTheSameType, Translations.Timeline.PartiallySupportedMultipleFieldsEditedInTheSameType},
        };
#pragma warning restore CS0612

        public static int CountTimelineEnties(Predicate<AlertEntry> predicate = null) {
            var count = 0;
            foreach (var alertEntry in EventsTimeline) {
                if (predicate == null || predicate(alertEntry)) {
                    count++;
                }
                if (!HotReloadPrefs.TimelineViewAll && alertEntry.alertData?.isCompile == true) {
                    // break on first compile entry if we only viewing recent events
                    break;
                }
            }
            return count;
        }
        
        internal static List<AlertEntry> Suggestions = new List<AlertEntry>();
        internal static int UnsupportedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.UnsupportedChange && alert.entryType != EntryType.Child);
        internal static int PartiallySupportedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.PartiallySupportedChange && alert.entryType != EntryType.Child);
        internal static int UndetectedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.UndetectedChange && alert.entryType != EntryType.Child);
        internal static int CompileErrorsCount => CountTimelineEnties(alert => alert.alertType == AlertType.CompileError);
        internal static int AppliedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.AppliedChange);

        static Regex shortDescriptionRegex = new Regex(PackageConst.DefaultLocale == Locale.SimplifiedChinese ? @"^([\p{L}\p{N}_]+)\s([\p{L}\p{N}_]+)(?=:)" : @"^(\w+)\s(\w+)(?=:)", RegexOptions.Compiled);
        
        internal static int GetRunTabTimelineEventCount() {
            int total = 0;
            if (HotReloadPrefs.RunTabUnsupportedChangesFilter) {
                total += UnsupportedChangesCount;
            }
            if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter) {
                total += PartiallySupportedChangesCount;
            }
            if (HotReloadPrefs.RunTabUndetectedPatchesFilter) {
                total += UndetectedChangesCount;
            }
            if (HotReloadPrefs.RunTabCompileErrorFilter) {
                total += CompileErrorsCount;
            }
            if (HotReloadPrefs.RunTabAppliedPatchesFilter) {
                total += AppliedChangesCount;
            }
            return total;
        }
        
        internal static List<AlertEntry> expandedEntries = new List<AlertEntry>();
        
        internal static void RenderCompileButton() {
            if (GUILayout.Button(Translations.Common.ButtonRecompile.Trim(), GUILayout.Width(80))) {
                HotReloadRunTab.RecompileWithChecks();
            }
        }
        
        internal static void RenderReportBugButton(string title, string detail = null) {
            if (GUILayout.Button(Translations.Common.ButtonBugReport.Trim(), GUILayout.Width(80))) {
                ReportWindowAPI.OpenBugReport(title?.Replace($", {Translations.UI.TapHereToSeeMore}", ""), detail);
            }
        }
        
        private static float maxScrollPos;
        internal static void RenderErrorEventActions(string description, ErrorData errorData) {
            int maxLen = 2400;
            string text = errorData.stacktrace;
            if (text.Length > maxLen) {
                text = text.Substring(0, maxLen) + "...";
            }

            GUILayout.TextArea(text, HotReloadWindowStyles.StacktraceTextAreaStyle);

            if (errorData.file || !errorData.stacktrace.Contains("error CS")) {
                GUILayout.Space(10f);
            }
            
            using (new EditorGUILayout.HorizontalScope()) {
                if (!errorData.stacktrace.Contains("error CS")) {
                    RenderCompileButton();
                }
                
                RenderReportBugButton(errorData.error, errorData.stacktrace);
            
                // Link
                if (errorData.file) {
                    GUILayout.FlexibleSpace();
                    if (GUILayout.Button(errorData.linkString, HotReloadWindowStyles.LinkStyle)) {
                        AssetDatabase.OpenAsset(errorData.file, Math.Max(errorData.lineNumber, 1));
                    }
                }
            }
        }

        private static Texture2D GetFilterIcon(int count, AlertType alertType) {
            if (count == 0) {
                return GUIHelper.ConvertToGrayscale(alertIconString[alertType]);
            }
            return GUIHelper.GetLocalIcon(alertIconString[alertType]);
        }
        
        internal static void RenderAlertFilters() {
            using (new EditorGUILayout.HorizontalScope()) {
                var text = AppliedChangesCount > 999 ? "999+" : " " + AppliedChangesCount;
                
                HotReloadPrefs.RunTabAppliedPatchesFilter = GUILayout.Toggle(
                    HotReloadPrefs.RunTabAppliedPatchesFilter,
                    new GUIContent(text, GetFilterIcon(AppliedChangesCount, AlertType.AppliedChange)), 
                    HotReloadWindowStyles.EventFiltersStyle);
                
                GUILayout.Space(-1f);
                
                text = UndetectedChangesCount > 999 ? "999+" : " " + UndetectedChangesCount;
                HotReloadPrefs.RunTabUndetectedPatchesFilter = GUILayout.Toggle(
                    HotReloadPrefs.RunTabUndetectedPatchesFilter,
                    new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UndetectedChange)), 
                    HotReloadWindowStyles.EventFiltersStyle);
                
                GUILayout.Space(-1f);
                
                text = PartiallySupportedChangesCount > 999 ? "999+" : " " + PartiallySupportedChangesCount;
                HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter = GUILayout.Toggle(
                    HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter,
                    new GUIContent(text, GetFilterIcon(PartiallySupportedChangesCount, AlertType.PartiallySupportedChange)), 
                    HotReloadWindowStyles.EventFiltersStyle);
                
                GUILayout.Space(-1f);
                
                text = UnsupportedChangesCount > 999 ? "999+" : " " + UnsupportedChangesCount;
                HotReloadPrefs.RunTabUnsupportedChangesFilter = GUILayout.Toggle(
                    HotReloadPrefs.RunTabUnsupportedChangesFilter, 
                    new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UnsupportedChange)), 
                    HotReloadWindowStyles.EventFiltersStyle);
                
                GUILayout.Space(-1f);
                
                text = CompileErrorsCount > 999 ? "999+" : " " + CompileErrorsCount;
                HotReloadPrefs.RunTabCompileErrorFilter = GUILayout.Toggle(
                    HotReloadPrefs.RunTabCompileErrorFilter,
                    new GUIContent(text, GetFilterIcon(CompileErrorsCount, AlertType.CompileError)), 
                    HotReloadWindowStyles.EventFiltersStyle);
            }
        }

        internal static void CreateErrorEventEntry(string errorString, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
            var timestamp = createdAt ?? DateTime.Now;
            var alertType = errorString.Contains("error CS")
                ? AlertType.CompileError
                : AlertType.UnsupportedChange;
            var title = errorString.Contains("error CS")
                ? Translations.Utility.CompileError
                : Translations.Utility.UnsupportedChange;
            ErrorData errorData = ErrorData.GetErrorData(errorString);
            var description = errorData.error;
            string shortDescription = null;
            if (alertType != AlertType.CompileError) {
                shortDescription = shortDescriptionRegex.Match(description).Value;
            }
            Action actionData = () => RenderErrorEventActions(description, errorData);
            InsertEntry(new AlertEntry(
                timestamp: timestamp,
                alertType: alertType, 
                title: title, 
                description: description, 
                shortDescription: shortDescription, 
                actionData: actionData,
                entryType: entryType,
                alertData: new AlertData(AlertEntryType.Error, createdAt: timestamp, errorString: errorString, entryType: entryType)
            ));
        }
        
#if UNITY_2020_1_OR_NEWER
        internal static void CreateInlinedMethodsEntry(string[] patchedMethodsDisplayNames, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
            var truncated = false;
            if (patchedMethodsDisplayNames?.Length > 25) {
                patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
                truncated = true;
            }
            var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
            var timestamp = createdAt ?? DateTime.Now;
            var title = Translations.Timeline.EventTitleFailedApplyingPatch;
            var entry = new AlertEntry(
                timestamp: timestamp,
                alertType : AlertType.UnsupportedChange, 
                title: title, 
                description: $"{Translations.Timeline.EventDescriptionInlinedMethods}\n\n• {(truncated ? patchesList + "\n..." : patchesList)}",
                entryType: EntryType.Parent,
                actionData: () => {
                    GUILayout.Space(10f);
                    using (new EditorGUILayout.HorizontalScope()) {
                        RenderCompileButton();
                        RenderReportBugButton(title);
                        var suggestion = HotReloadSuggestionsHelper.suggestionMap[HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods];
                        if (suggestion?.actionData != null) {
                            suggestion.actionData();
                        }
                    }
                },
                alertData: new AlertData(AlertEntryType.InlinedMethod, createdAt: timestamp, patchedMembersDisplayNames: patchedMethodsDisplayNames, entryType: EntryType.Parent)
            );
            InsertEntry(entry);
            if (patchedMethodsDisplayNames?.Length > 0) {
                expandedEntries.Add(entry);
            }
        }
#endif
        
        internal static void CreatePatchFailureEventEntry(string errorString, string methodName, string methodSimpleName = null, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
            var timestamp = createdAt ?? DateTime.Now;
            ErrorData errorData = ErrorData.GetErrorData(errorString);
            var title = Translations.Timeline.EventTitleFailedApplyingPatch;
            Action actionData = () => RenderErrorEventActions(errorData.error, errorData);
            InsertEntry(new AlertEntry(
                timestamp: timestamp,
                alertType : AlertType.UnsupportedChange, 
                title: title, 
                description: string.Format(Translations.Timeline.EventDescriptionFailedApplyingPatchTapForMore, title, methodName),
                shortDescription: methodSimpleName, 
                actionData: actionData,
                entryType: entryType,
                alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, errorString: errorString, methodName: methodName, methodSimpleName: methodSimpleName, entryType: entryType)
            ));
        }

        public static T[] TruncateList<T>(T[] originalList, int len) {
            if (originalList.Length <= len) {
                return originalList;
            }
            // Create a new list with a maximum of 25 items
            T[] truncatedList = new T[len];

            for (int i = 0; i < originalList.Length && i < len; i++) {
                truncatedList[i] = originalList[i];
            }

            return truncatedList;
        }
        
        internal static void CreateReloadFinishedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null, bool isCompile = false) {
            var truncated = false;
            if (patchedMethodsDisplayNames?.Length > 25) {
                patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
                truncated = true;
            }
            var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
            var timestamp = createdAt ?? DateTime.Now;
            var entry = new AlertEntry(
                timestamp: timestamp,
                alertType: AlertType.AppliedChange,
                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Reloaded],
                description: patchedMethodsDisplayNames?.Length > 0 
                    ? $"• {(truncated ? patchesList + "\n..." : patchesList)}" 
                    : Translations.Timeline.EventDescriptionNoIssuesFound,
                entryType: patchedMethodsDisplayNames?.Length > 0 ? EntryType.Parent : EntryType.Standalone,
                alertData: new AlertData(
                    AlertEntryType.PatchApplied, 
                    createdAt: timestamp, 
                    entryType: EntryType.Standalone,
                    isCompile: isCompile,
                    patchedMembersDisplayNames: patchedMethodsDisplayNames)
            );
            
            InsertEntry(entry);
            if (patchedMethodsDisplayNames?.Length > 0) {
                expandedEntries.Add(entry);
            }
        }
        
        internal static void CreateReloadFinishedWithWarningsEventEntry(DateTime? createdAt = null, string[] patchedMembersDisplayNames = null) {
            var truncated = false;
            if (patchedMembersDisplayNames?.Length > 25) {
                patchedMembersDisplayNames = TruncateList(patchedMembersDisplayNames, 25);
                truncated = true;
            }
            var patchesList = patchedMembersDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMembersDisplayNames) : "";
            var timestamp = createdAt ?? DateTime.Now;
            var entry = new AlertEntry(
                timestamp: timestamp,
                alertType: AlertType.UnsupportedChange,
                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Unsupported],
                description: patchedMembersDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\n" + Translations.Timeline.EventDescriptionSeeUnsupportedChangesBelow : patchesList + "\n\n" + Translations.Timeline.EventDescriptionSeeUnsupportedChangesBelow)}" : Translations.Timeline.EventDescriptionSeeDetailedEntriesBelow,
                entryType: EntryType.Parent,
                alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMembersDisplayNames)
            );
            InsertEntry(entry);
            if (patchedMembersDisplayNames?.Length > 0) {
                expandedEntries.Add(entry);
            }
        }
        
        internal static void CreateReloadPartiallyAppliedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null) {
            var truncated = false;
            if (patchedMethodsDisplayNames?.Length > 25) {
                patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
                truncated = true;
            }
            var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
            var timestamp = createdAt ?? DateTime.Now;
            var entry = new AlertEntry(
                timestamp: timestamp,
                alertType: AlertType.PartiallySupportedChange,
                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.PartiallySupported],
                description: patchedMethodsDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\n" + Translations.Timeline.EventDescriptionSeePartiallyAppliedChangesBelow : patchesList + "\n\n" + Translations.Timeline.EventDescriptionSeePartiallyAppliedChangesBelow)}"  : Translations.Timeline.EventDescriptionSeeDetailedEntriesBelow,
                entryType: EntryType.Parent,
                alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMethodsDisplayNames)
            );
            InsertEntry(entry);
            if (patchedMethodsDisplayNames?.Length > 0) {
                expandedEntries.Add(entry);
            }
        }
        
        internal static void CreateReloadUndetectedChangeEventEntry(DateTime? createdAt = null) {
            var timestamp = createdAt ?? DateTime.Now;
            var title = EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Undetected];
            InsertEntry(new AlertEntry(
                timestamp: timestamp,
                alertType : AlertType.UndetectedChange, 
                title: title,
                description: Translations.Timeline.EventDescriptionUndetectedChange,
                actionData: () => {
                    GUILayout.Space(10f);
                    using (new EditorGUILayout.HorizontalScope()) {
                        RenderCompileButton();
                        RenderReportBugButton(title);
                        GUILayout.FlexibleSpace();
                        OpenURLButton.Render(Translations.Suggestions.ButtonDocs, Constants.UndetectedChangesURL);
                        GUILayout.Space(10f);
                    }
                },
                entryType: EntryType.Foldout,
                alertData: new AlertData(AlertEntryType.UndetectedChange, createdAt: timestamp, entryType: EntryType.Parent)
            ));
        }
        
        internal static void CreatePartiallyAppliedEventEntry(PartiallySupportedChange partiallySupportedChange, EntryType entryType = EntryType.Standalone, bool detailed = true, DateTime? createdAt = null) {
            var timestamp = createdAt ?? DateTime.Now;
            string description;
            if (!partiallySupportedChangeDescriptions.TryGetValue(partiallySupportedChange, out description)) {
                return;
            }
            var title = detailed ? Translations.Timeline.EventTitleChangePartiallyApplied : ToString(partiallySupportedChange);
            InsertEntry(new AlertEntry(
                timestamp: timestamp,
                alertType : AlertType.PartiallySupportedChange, 
                title : title,
                description : description,
                shortDescription: detailed ? ToString(partiallySupportedChange) : null,
                actionData: () => {
                    GUILayout.Space(10f);
                    using (new EditorGUILayout.HorizontalScope()) {
                        RenderCompileButton();
                        RenderReportBugButton(title);
                        GUILayout.FlexibleSpace();
                        if (GetPartiallySupportedChangePref(partiallySupportedChange)) {
                            if (GUILayout.Button(Translations.Timeline.ButtonIgnoreEventType, HotReloadWindowStyles.LinkStyle)) {
                                HidePartiallySupportedChange(partiallySupportedChange);
                                HotReloadRunTab.RepaintInstant();
                            }
                        }
                    }
                },
                entryType: entryType,
                alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, partiallySupportedChange: partiallySupportedChange, entryType: entryType, detiled: detailed)
            ));
        }
        
        internal static void InsertEntry(AlertEntry entry) {
            eventsTimeline.Insert(0, entry);
            if (entry.alertType != AlertType.AppliedChange) {
                HotReloadState.ShowingRedDot = true;
            }
        }
        
        internal static void ClearEntries() {
            eventsTimeline.Clear();
        }
        
        internal static bool GetPartiallySupportedChangePref(PartiallySupportedChange key) {
            return EditorPrefs.GetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", true);
        }
        
        internal static void HidePartiallySupportedChange(PartiallySupportedChange key) {
            EditorPrefs.SetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", false);
            // loop over scroll entries to remove hidden entries
            for (var i = EventsTimeline.Count - 1; i >= 0; i--) {
                var eventEntry = EventsTimeline[i];
                if (eventEntry.alertData.partiallySupportedChange == key) {
                    EventsTimeline.Remove(eventEntry);
                }
            }
        }

        // performance optimization (Enum.ToString uses reflection)
        internal static string ToString(this PartiallySupportedChange change) {
#pragma warning disable CS0612 // obsolete
            switch (change) {
                case PartiallySupportedChange.LambdaClosure:
                    return nameof(PartiallySupportedChange.LambdaClosure);
                case PartiallySupportedChange.EditAsyncMethod:
                   return nameof(PartiallySupportedChange.EditAsyncMethod);
                case PartiallySupportedChange.AddMonobehaviourMethod:
                   return nameof(PartiallySupportedChange.AddMonobehaviourMethod);
                case PartiallySupportedChange.EditMonobehaviourField:
                    return nameof(PartiallySupportedChange.EditMonobehaviourField);
                case PartiallySupportedChange.EditCoroutine:
                   return nameof(PartiallySupportedChange.EditCoroutine);
                case PartiallySupportedChange.EditGenericFieldInitializer:
                   return nameof(PartiallySupportedChange.EditGenericFieldInitializer);
                case PartiallySupportedChange.AddEnumMember:
                   return nameof(PartiallySupportedChange.AddEnumMember);
                case PartiallySupportedChange.EditFieldInitializer:
                   return nameof(PartiallySupportedChange.EditFieldInitializer);
                case PartiallySupportedChange.AddMethodWithAttributes:
                   return nameof(PartiallySupportedChange.AddMethodWithAttributes);
                case PartiallySupportedChange.GenericMethodInGenericClass:
                   return nameof(PartiallySupportedChange.GenericMethodInGenericClass);
                case PartiallySupportedChange.AddFieldWithAttributes:
                   return nameof(PartiallySupportedChange.AddFieldWithAttributes);
                case PartiallySupportedChange.NewCustomSerializableField:
                   return nameof(PartiallySupportedChange.NewCustomSerializableField);
                case PartiallySupportedChange.MultipleFieldsEditedInTheSameType:
                   return nameof(PartiallySupportedChange.MultipleFieldsEditedInTheSameType);
#pragma warning restore CS0612
                default:
                    throw new ArgumentOutOfRangeException(nameof(change), change, null);
            }
        }
    }
}
