using UnityEditorInternal;
using System.Reflection;
using System;
using System.Collections;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using UnityEngine.Profiling;
#if UNITY_2020_1_OR_NEWER
using UnityEditor.Profiling;
#endif
#if UNITY_2021_2_OR_NEWER
using Unity.Profiling.Editor;
// stub so that ProfilerWindow can be moved to this namespace in trunk without a need to change PA
namespace Unity.Profiling.Editor {}
#endif

namespace UnityEditor.Performance.ProfileAnalyzer
{
    internal class ProfilerWindowInterface
    {
        bool m_ProfilerWindowInitialized = false;
        const float k_NsToMs = 1000000;
#if UNITY_2020_1_OR_NEWER
        bool s_UseRawIterator = true;
#endif
        ProgressBarDisplay m_progressBar;

        [NonSerialized] bool m_SendingSelectionEventToProfilerWindowInProgress = false;
        [NonSerialized] int m_LastSelectedFrameInProfilerWindow = 0;

#if UNITY_2021_1_OR_NEWER
        [NonSerialized] ProfilerWindow m_ProfilerWindow;
        [NonSerialized] IProfilerFrameTimeViewSampleSelectionController m_CpuProfilerModule;
#else
        Type m_ProfilerWindowType;
        EditorWindow m_ProfilerWindow;
        FieldInfo m_CurrentFrameFieldInfo;
        FieldInfo m_TimeLineGUIFieldInfo;
        FieldInfo m_SelectedEntryFieldInfo;
        FieldInfo m_SelectedNameFieldInfo;
        FieldInfo m_SelectedTimeFieldInfo;
        FieldInfo m_SelectedDurationFieldInfo;
        FieldInfo m_SelectedInstanceIdFieldInfo;
        FieldInfo m_SelectedFrameIdFieldInfo;
        FieldInfo m_SelectedThreadIndexFieldInfo;
        FieldInfo m_SelectedNativeIndexFieldInfo;
        FieldInfo m_SelectedInstanceCountFieldInfo;
        FieldInfo m_SelectedInstanceCountForThreadFieldInfo;
        FieldInfo m_SelectedInstanceCountForFrameFieldInfo;
        FieldInfo m_SelectedMetaDataFieldInfo;
        FieldInfo m_SelectedThreadCountFieldInfo;
        FieldInfo m_SelectedCallstackInfoFieldInfo;

        MethodInfo m_GetProfilerModuleInfo;
        Type m_CPUProfilerModuleType;
#endif

        public ProfilerWindowInterface(ProgressBarDisplay progressBar)
        {
            m_progressBar = progressBar;

#if !UNITY_2021_1_OR_NEWER
            Assembly assem = typeof(Editor).Assembly;
            m_ProfilerWindowType = assem.GetType("UnityEditor.ProfilerWindow");
            m_CurrentFrameFieldInfo = m_ProfilerWindowType.GetField("m_CurrentFrame", BindingFlags.NonPublic | BindingFlags.Instance);

            m_TimeLineGUIFieldInfo = m_ProfilerWindowType.GetField("m_CPUTimelineGUI", BindingFlags.NonPublic | BindingFlags.Instance);
            if (m_TimeLineGUIFieldInfo == null)
            {
                // m_CPUTimelineGUI isn't present in 2019.3.0a8 onward
                m_GetProfilerModuleInfo = m_ProfilerWindowType.GetMethod("GetProfilerModule", BindingFlags.NonPublic | BindingFlags.Instance);
                if (m_GetProfilerModuleInfo == null)
                {
                    Debug.Log("Unable to initialise link to Profiler Timeline, no GetProfilerModule found");
                }

                m_CPUProfilerModuleType = assem.GetType("UnityEditorInternal.Profiling.CPUProfilerModule");
                m_TimeLineGUIFieldInfo = m_CPUProfilerModuleType.GetField("m_TimelineGUI", BindingFlags.NonPublic | BindingFlags.Instance);
                if (m_TimeLineGUIFieldInfo == null)
                {
                    Debug.Log("Unable to initialise link to Profiler Timeline");
                }
            }

            if (m_TimeLineGUIFieldInfo != null)
                m_SelectedEntryFieldInfo = m_TimeLineGUIFieldInfo.FieldType.GetField("m_SelectedEntry", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (m_SelectedEntryFieldInfo != null)
            {
                m_SelectedNameFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("name", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedTimeFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("time", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedDurationFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("duration", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedInstanceIdFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("instanceId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedFrameIdFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("frameId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                // confusingly this is called threadId but is the thread _index_
                m_SelectedThreadIndexFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("threadId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedNativeIndexFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("nativeIndex", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedInstanceCountFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("instanceCount", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedInstanceCountForThreadFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("instanceCountForThread", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedInstanceCountForFrameFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("instanceCountForFrame", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedThreadCountFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("threadCount", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedMetaDataFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("metaData", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                m_SelectedCallstackInfoFieldInfo = m_SelectedEntryFieldInfo.FieldType.GetField("callstackInfo", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            }
#endif
        }

        public bool IsReady()
        {
            return m_ProfilerWindow != null && m_ProfilerWindowInitialized;
        }

        public void GetProfilerWindowHandle()
        {
            Profiler.BeginSample("GetProfilerWindowHandle");
#if UNITY_2021_1_OR_NEWER
            if (m_CpuProfilerModule != null)
            {
                m_CpuProfilerModule.selectionChanged -= OnSelectionChangedInCpuProfilerModule;
                m_CpuProfilerModule = null;
            }

            var windows = Resources.FindObjectsOfTypeAll<ProfilerWindow>();
            if (windows != null && windows.Length > 0)
                m_ProfilerWindow = windows[0];
            if (m_ProfilerWindow != null)
            {
#if UNITY_2021_2_OR_NEWER
                var cpuModuleIdentifier = ProfilerWindow.cpuModuleIdentifier;
#else
                var cpuModuleIdentifier = ProfilerWindow.cpuModuleName;
#endif
                m_CpuProfilerModule =
                    m_ProfilerWindow.GetFrameTimeViewSampleSelectionController(cpuModuleIdentifier);
                m_CpuProfilerModule.selectionChanged -= OnSelectionChangedInCpuProfilerModule;
                m_CpuProfilerModule.selectionChanged += OnSelectionChangedInCpuProfilerModule;

                m_ProfilerWindow.Repaint();
                m_ProfilerWindowInitialized = false;
                // wait a frame for the Profiler to get Repainted
                EditorApplication.delayCall += () => m_ProfilerWindowInitialized = true;
            }
#else
            UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(m_ProfilerWindowType);
            if (windows != null && windows.Length > 0)
                m_ProfilerWindow = (EditorWindow)windows[0];
            m_ProfilerWindowInitialized = true;
#endif
            Profiler.EndSample();
        }

        public void OpenProfilerOrUseExisting()
        {
            // Note we use existing if possible to fix a bug after domain reload
            // Where calling EditorWindow.GetWindow directly causes a second window to open
            if (m_ProfilerWindow == null)
            {
#if UNITY_2021_1_OR_NEWER
                m_ProfilerWindow = EditorWindow.GetWindow<ProfilerWindow>();
#if UNITY_2021_2_OR_NEWER
                var cpuModuleIdentifier = ProfilerWindow.cpuModuleIdentifier;
#else
                var cpuModuleIdentifier = ProfilerWindow.cpuModuleName;
#endif
                m_CpuProfilerModule = m_ProfilerWindow.GetFrameTimeViewSampleSelectionController(cpuModuleIdentifier);
                m_CpuProfilerModule.selectionChanged -= OnSelectionChangedInCpuProfilerModule;
                m_CpuProfilerModule.selectionChanged += OnSelectionChangedInCpuProfilerModule;
#else
                // Create new
                m_ProfilerWindow = EditorWindow.GetWindow(m_ProfilerWindowType);
#endif
            }
        }

        public bool GetFrameRangeFromProfiler(out int first, out int last)
        {
            if (m_ProfilerWindow != null)
            {
                first = 1 + ProfilerDriver.firstFrameIndex;
                last = 1 + ProfilerDriver.lastFrameIndex;
#if !UNITY_2018_4_OR_NEWER
                //Prior to 18.4 we need to clip to the visible frames in the profile which indents 1 in from end
                //as the last frame is not visible and sometimes still being processed
                if (first < last)
                    last--;
#endif
                return true;
            }

            first = 1;
            last = 1;
            return false;
        }

        public void CloseProfiler()
        {
            if (m_ProfilerWindow != null)
                m_ProfilerWindow.Close();
        }

#if !UNITY_2021_1_OR_NEWER
        object GetTimeLineGUI()
        {
            object timeLineGUI = null;

            if (m_CPUProfilerModuleType != null)
            {
                object[] parametersArray = new object[] { ProfilerArea.CPU };
                var getCPUProfilerModuleInfo = m_GetProfilerModuleInfo.MakeGenericMethod(m_CPUProfilerModuleType);
                var cpuModule = getCPUProfilerModuleInfo.Invoke(m_ProfilerWindow, parametersArray);

                timeLineGUI = m_TimeLineGUIFieldInfo.GetValue(cpuModule);
            }
            else if (m_TimeLineGUIFieldInfo != null)
            {
                timeLineGUI = m_TimeLineGUIFieldInfo.GetValue(m_ProfilerWindow);
            }

            return timeLineGUI;
        }

#endif


#if UNITY_2021_1_OR_NEWER
        private void OnSelectionChangedInCpuProfilerModule(IProfilerFrameTimeViewSampleSelectionController controller, ProfilerTimeSampleSelection selection)
        {
            if (controller == m_CpuProfilerModule && !m_SendingSelectionEventToProfilerWindowInProgress)
            {
                if (selection != null && selection.markerNamePath != null && selection.markerNamePath.Count > 0)
                {
                    selectedMarkerChanged(selection.markerNamePath[selection.markerNamePath.Count - 1], selection.threadGroupName, selection.threadName);
                }
            }
        }

#endif

        public event Action<string, string, string> selectedMarkerChanged = delegate {};

        public void PollProfilerWindowMarkerName()
        {
#if !UNITY_2021_1_OR_NEWER
            if (m_ProfilerWindow != null)
            {
                var timeLineGUI = GetTimeLineGUI();
                if (timeLineGUI != null && m_SelectedEntryFieldInfo != null)
                {
                    var selectedEntry = m_SelectedEntryFieldInfo.GetValue(timeLineGUI);
                    if (selectedEntry != null && m_SelectedNameFieldInfo != null)
                    {
                        string threadGroupName = null;
                        string threadName = null;
#if UNITY_2020_1_OR_NEWER
                        if (m_SelectedFrameIdFieldInfo != null && m_SelectedThreadIndexFieldInfo != null)
                        {
                            using (RawFrameDataView frameData = ProfilerDriver.GetRawFrameDataView((int)m_SelectedFrameIdFieldInfo.GetValue(selectedEntry), (int)m_SelectedThreadIndexFieldInfo.GetValue(selectedEntry)))
                            {
                                if (frameData != null && frameData.valid)
                                {
                                    threadGroupName = frameData.threadGroupName;
                                    threadName = frameData.threadName;
                                }
                            }
                        }
#endif
                        selectedMarkerChanged(m_SelectedNameFieldInfo.GetValue(selectedEntry).ToString(), threadGroupName, threadName);
                    }
                }
            }
#endif
        }

        public ProfileData PullFromProfiler(int firstFrameDisplayIndex, int lastFrameDisplayIndex)
        {
            Profiler.BeginSample("ProfilerWindowInterface.PullFromProfiler");

            bool recording = IsRecording();
            if (recording)
                StopRecording();

            int firstFrameIndex = Mathf.Max(firstFrameDisplayIndex - 1, 0);
            int lastFrameIndex = lastFrameDisplayIndex - 1;
            ProfileData profileData = GetData(firstFrameIndex, lastFrameIndex);

            if (recording)
                StartRecording();

            Profiler.EndSample();
            return profileData;
        }

        public int GetThreadCountForFrame(int frameIndex)
        {
            if (!IsReady())
                return 0;

            ProfilerFrameDataIterator frameData = new ProfilerFrameDataIterator();
            frameData.SetRoot(frameIndex, 0);
            return frameData.GetThreadCount(frameIndex);
        }

        public ProfileFrame GetProfileFrameForThread(int frameIndex, int threadIndex)
        {
            if (!IsReady())
                return null;

            var frame = new ProfileFrame();
#if UNITY_2020_1_OR_NEWER
            using (RawFrameDataView frameData = ProfilerDriver.GetRawFrameDataView(frameIndex, threadIndex))
            {
                frame.msStartTime = frameData.frameStartTimeMs;
                frame.msFrame = frameData.frameTimeMs;
            }
#else
            ProfilerFrameDataIterator frameData = new ProfilerFrameDataIterator();
            frameData.SetRoot(frameIndex, threadIndex);
            frame.msStartTime = 1000.0 * frameData.GetFrameStartS(frameIndex);
            frame.msFrame = frameData.frameTimeMS;
#endif
            return frame;
        }

#if UNITY_2020_1_OR_NEWER
        ProfileData GetDataRaw(ProfileData data, int firstFrameIndex, int lastFrameIndex)
        {
            bool firstError = true;

            data.SetFrameIndexOffset(firstFrameIndex);

            var depthStack = new Stack<int>();

            var threadNameCount = new Dictionary<string, int>();
            var markerIdToNameIndex = new Dictionary<int, int>();

            for (int frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; ++frameIndex)
            {
                m_progressBar.AdvanceProgressBar();

                int threadIndex = 0;

                threadNameCount.Clear();
                ProfileFrame frame = null;
                while (true)
                {
                    using (RawFrameDataView frameData = ProfilerDriver.GetRawFrameDataView(frameIndex, threadIndex))
                    {
                        if (threadIndex == 0)
                        {
                            if ((frameIndex == firstFrameIndex || frameIndex == lastFrameIndex)
                                && firstFrameIndex != lastFrameIndex && (!frameData.valid || frameData.frameTimeNs == 0))
                            {
                                // skip incomplete frames when they are at the beginning or end of the capture
                                if (++frameIndex <= lastFrameIndex)
                                {
                                    data.FirstFrameIncomplete = true;
                                    data.SetFrameIndexOffset(frameIndex);
                                    continue;
                                }
                                else
                                {
                                    // break out entirely if this is the last frame
                                    data.LastFrameIncomplete = true;
                                    break;
                                }
                            }
                            frame = new ProfileFrame();
                            if (frameData.valid)
                            {
                                frame.msStartTime = frameData.frameStartTimeMs;
                                frame.msFrame = frameData.frameTimeMs;
                            }
                            data.Add(frame);
                        }

                        if (!frameData.valid)
                            break;

                        string threadNameWithIndex = null;
                        string threadName = frameData.threadName;
                        if (threadName.Trim() == "")
                        {
                            Debug.Log(string.Format("Warning: Unnamed thread found on frame {0}. Corrupted data suspected, ignoring frame", frameIndex));
                            threadIndex++;
                            continue;
                        }
                        var groupName = frameData.threadGroupName;
                        threadName = ProfileData.GetThreadNameWithGroup(threadName, groupName);

                        int nameCount = 0;
                        threadNameCount.TryGetValue(threadName, out nameCount);
                        threadNameCount[threadName] = nameCount + 1;

                        threadNameWithIndex = ProfileData.ThreadNameWithIndex(threadNameCount[threadName], threadName);

                        var thread = new ProfileThread();
                        data.AddThreadName(threadNameWithIndex, thread);

                        frame.Add(thread);

                        // The markers are in depth first order
                        depthStack.Clear();
                        // first sample is the thread name
                        for (int i = 1; i < frameData.sampleCount; i++)
                        {
                            float durationMS = frameData.GetSampleTimeMs(i);
                            int markerId = frameData.GetSampleMarkerId(i);
                            if (durationMS < 0)
                            {
                                if (firstError)
                                {
                                    int displayIndex = data.OffsetToDisplayFrame(frameIndex);

                                    string name = frameData.GetSampleName(i);
                                    Debug.LogFormat("Ignoring Invalid marker time found for {0} on frame {1} on thread {2} ({3} < 0)",
                                        name, displayIndex, threadName, durationMS);

                                    firstError = false;
                                }
                            }
                            else
                            {
                                int depth = 1 + depthStack.Count;
                                var markerData = ProfileMarker.Create(durationMS, depth);

                                // Use name index directly if we have already stored this named marker before
                                int nameIndex;
                                if (markerIdToNameIndex.TryGetValue(markerId, out nameIndex))
                                {
                                    markerData.nameIndex = nameIndex;
                                }
                                else
                                {
                                    string name = frameData.GetSampleName(i);
                                    data.AddMarkerName(name, markerData);
                                    markerIdToNameIndex[markerId] = markerData.nameIndex;
                                }

                                thread.AddMarker(markerData);
                            }

                            int childrenCount = frameData.GetSampleChildrenCount(i);
                            if (childrenCount > 0)
                            {
                                depthStack.Push(childrenCount);
                            }
                            else
                            {
                                while (depthStack.Count > 0)
                                {
                                    int remainingChildren = depthStack.Pop();
                                    if (remainingChildren > 1)
                                    {
                                        depthStack.Push(remainingChildren - 1);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    threadIndex++;
                }
            }

            data.Finalise();

            return data;
        }

#endif

        ProfileData GetDataOriginal(ProfileData data, int firstFrameIndex, int lastFrameIndex)
        {
            ProfilerFrameDataIterator frameData = new ProfilerFrameDataIterator();
            bool firstError = true;

            data.SetFrameIndexOffset(firstFrameIndex);

            Dictionary<string, int> threadNameCount = new Dictionary<string, int>();
            for (int frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; ++frameIndex)
            {
                m_progressBar.AdvanceProgressBar();

                int threadCount = frameData.GetThreadCount(frameIndex);
                frameData.SetRoot(frameIndex, 0);

                var msFrame = frameData.frameTimeMS;

                if ((frameIndex == firstFrameIndex || frameIndex == lastFrameIndex)
                    && firstFrameIndex != lastFrameIndex && msFrame == 0)
                {
                    var nextFrame = frameIndex + 1;
                    // skip incomplete frames when they are at the beginning or end of the capture
                    if (nextFrame <= lastFrameIndex)
                    {
                        data.FirstFrameIncomplete = true;
                        data.SetFrameIndexOffset(nextFrame);
                        continue;
                    }
                    else
                    {
                        // break out entirely if this is the last frame
                        data.LastFrameIncomplete = true;
                        break;
                    }
                }

                /*
                if (frameIndex == lastFrameIndex)
                {
                    // Check if last frame appears to be invalid data
                    float median;
                    float mean;
                    float standardDeviation;
                    CalculateFrameTimeStats(data, out median, out mean, out standardDeviation);
                    float execessiveDeviation = (3f * standardDeviation);
                    if (msFrame > (median + execessiveDeviation))
                    {
                        Debug.LogFormat("Dropping last frame as it is significantly larger than the median of the rest of the data set {0} > {1} (median {2} + 3 * standard deviation {3})", msFrame, median + execessiveDeviation, median, standardDeviation);
                        break;
                    }
                    if (msFrame < (median - execessiveDeviation))
                    {
                        Debug.LogFormat("Dropping last frame as it is significantly smaller than the median of the rest of the data set {0} < {1} (median {2} - 3 * standard deviation {3})", msFrame, median - execessiveDeviation, median, standardDeviation);
                        break;
                    }
                }
                */

                ProfileFrame frame = new ProfileFrame();
                frame.msStartTime = 1000.0 * frameData.GetFrameStartS(frameIndex);
                frame.msFrame = msFrame;
                data.Add(frame);

                threadNameCount.Clear();
                for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex)
                {
                    frameData.SetRoot(frameIndex, threadIndex);

                    var threadName = frameData.GetThreadName();
                    if (threadName.Trim() == "")
                    {
                        Debug.Log(string.Format("Warning: Unnamed thread found on frame {0}. Corrupted data suspected, ignoring frame", frameIndex));
                        continue;
                    }

                    var groupName = frameData.GetGroupName();
                    threadName = ProfileData.GetThreadNameWithGroup(threadName, groupName);

                    ProfileThread thread = new ProfileThread();
                    frame.Add(thread);

                    int nameCount = 0;
                    threadNameCount.TryGetValue(threadName, out nameCount);
                    threadNameCount[threadName] = nameCount + 1;

                    data.AddThreadName(ProfileData.ThreadNameWithIndex(threadNameCount[threadName], threadName), thread);

                    const bool enterChildren = true;
                    // The markers are in depth first order and the depth is known
                    // So we can infer a parent child relationship
                    while (frameData.Next(enterChildren))
                    {
                        if (frameData.durationMS < 0)
                        {
                            if (firstError)
                            {
                                int displayIndex = data.OffsetToDisplayFrame(frameIndex);

                                Debug.LogFormat("Ignoring Invalid marker time found for {0} on frame {1} on thread {2} ({3} < 0) : Instance id : {4}",
                                    frameData.name, displayIndex, threadName, frameData.durationMS, frameData.instanceId);

                                firstError = false;
                            }
                            continue;
                        }
                        var markerData = ProfileMarker.Create(frameData);

                        data.AddMarkerName(frameData.name, markerData);
                        thread.AddMarker(markerData);
                    }
                }
            }

            data.Finalise();

            frameData.Dispose();
            return data;
        }

        ProfileData GetData(int firstFrameIndex, int lastFrameIndex)
        {
            ProfileData data = new ProfileData(ProfileAnalyzerWindow.TmpPath);
#if UNITY_2020_1_OR_NEWER
            GetDataRaw(data, firstFrameIndex, lastFrameIndex);
#else
            GetDataOriginal(data, firstFrameIndex, lastFrameIndex);
#endif
            data.Write();
            return data;
        }

#if UNITY_2020_1_OR_NEWER
        public float GetFrameTimeRaw(int frameIndex)
        {
            using (RawFrameDataView frameData = ProfilerDriver.GetRawFrameDataView(frameIndex, 0))
            {
                if (!frameData.valid)
                    return 0f;

                return frameData.frameTimeMs;
            }
        }

#endif


        public float GetFrameTime(int frameIndex)
        {
#if UNITY_2020_1_OR_NEWER
            if (s_UseRawIterator)
                return GetFrameTimeRaw(frameIndex);
#endif
            ProfilerFrameDataIterator frameData = new ProfilerFrameDataIterator();

            frameData.SetRoot(frameIndex, 0);
            float ms = frameData.frameTimeMS;
            frameData.Dispose();

            return ms;
        }

        struct ThreadIndexIterator
        {
            public ProfilerFrameDataIterator frameData;
            public int threadIndex;
        }

        IEnumerator<ThreadIndexIterator> GetNextThreadIndexFittingThreadFilters(int frameIndex, List<string> threadFilters)
        {
            ProfilerFrameDataIterator frameData = new ProfilerFrameDataIterator();

            int threadCount = frameData.GetThreadCount(frameIndex);
            Dictionary<string, int> threadNameCount = new Dictionary<string, int>();
            for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex)
            {
                frameData.SetRoot(frameIndex, threadIndex);

                var threadName = frameData.GetThreadName();
                // Name here could be "Worker Thread 1"

                var groupName = frameData.GetGroupName();
                threadName = ProfileData.GetThreadNameWithGroup(threadName, groupName);

                int nameCount = 0;
                threadNameCount.TryGetValue(threadName, out nameCount);
                threadNameCount[threadName] = nameCount + 1;

                var threadNameWithIndex = ProfileData.ThreadNameWithIndex(threadNameCount[threadName], threadName);

                // To compare on the filter we need to remove the postfix on the thread name
                // "3:Worker Thread 0" -> "1:Worker Thread"
                // The index of the thread (0) is used +1 as a prefix
                // The preceding number (3) is the count of number of threads with this name
                // Unfortunately multiple threads can have the same name
                threadNameWithIndex = ProfileData.CorrectThreadName(threadNameWithIndex);

                if (threadFilters.Contains(threadNameWithIndex))
                {
                    yield return new ThreadIndexIterator {frameData = frameData, threadIndex = threadIndex};
                }
            }
            frameData.Dispose();
        }

        bool GetMarkerInfo(string markerName, int frameIndex, List<string> threadFilters, out int outThreadIndex, out int outNativeIndex, out float time, out float duration, out int instanceId)
        {
            outThreadIndex = 0;
            outNativeIndex = 0;
            time = 0.0f;
            duration = 0.0f;
            instanceId = 0;
            bool found = false;

            var iterator = GetNextThreadIndexFittingThreadFilters(frameIndex, threadFilters);
            while (iterator.MoveNext())
            {
                const bool enterChildren = true;
                while (iterator.Current.frameData.Next(enterChildren))
                {
                    if (iterator.Current.frameData.name == markerName)
                    {
                        time = iterator.Current.frameData.startTimeMS;
                        duration = iterator.Current.frameData.durationMS;
                        instanceId = iterator.Current.frameData.instanceId;
                        outNativeIndex = iterator.Current.frameData.sampleId;
                        outThreadIndex = iterator.Current.threadIndex;
                        found = true;
                        break;
                    }
                }

                if (found)
                    break;
            }

            return found;
        }

        public bool SetProfilerWindowMarkerName(string markerName, List<string> threadFilters)
        {
            m_SendingSelectionEventToProfilerWindowInProgress = true;
            if (m_ProfilerWindow == null)
                return false;
#if UNITY_2021_1_OR_NEWER
#if UNITY_2021_2_OR_NEWER
            var cpuModuleIdentifier = ProfilerWindow.cpuModuleIdentifier;
            var selectedModuleIdentifier = m_ProfilerWindow.selectedModuleIdentifier;
#else
            var cpuModuleIdentifier = ProfilerWindow.cpuModuleName;
            var selectedModuleIdentifier = m_ProfilerWindow.selectedModuleName;
#endif
            m_CpuProfilerModule = m_ProfilerWindow.GetFrameTimeViewSampleSelectionController(cpuModuleIdentifier);
            if (m_CpuProfilerModule != null && selectedModuleIdentifier == cpuModuleIdentifier
                && m_ProfilerWindow.firstAvailableFrameIndex >= 0)
            {
                // Read profiler data direct from profile to find time/duration
                int currentFrameIndex = (int)m_ProfilerWindow.selectedFrameIndex;

                var iterator = GetNextThreadIndexFittingThreadFilters(currentFrameIndex, threadFilters);
                while (iterator.MoveNext())
                {
                    using (var rawFrameDataView = ProfilerDriver.GetRawFrameDataView(currentFrameIndex, iterator.Current.threadIndex))
                    {
                        if (m_CpuProfilerModule.SetSelection(currentFrameIndex,
                            rawFrameDataView.threadGroupName, rawFrameDataView.threadName, markerName,
                            threadId: rawFrameDataView.threadId))
                        {
                            m_ProfilerWindow.Repaint();
                            m_SendingSelectionEventToProfilerWindowInProgress = false;
                            return true; // setting the selection was successful, nothing more to do here.
                        }
                    }
                }
                // selection couldn't be found, so clear the current one to avoid confusion
                m_CpuProfilerModule.ClearSelection();
                m_ProfilerWindow.Repaint();
            }
#else
            var timeLineGUI = GetTimeLineGUI();
            if (timeLineGUI == null)
            {
                m_SendingSelectionEventToProfilerWindowInProgress = false;
                return false;
            }

            if (m_SelectedEntryFieldInfo != null)
            {
                var selectedEntry = m_SelectedEntryFieldInfo.GetValue(timeLineGUI);
                if (selectedEntry != null)
                {
                    // Read profiler data direct from profile to find time/duration
                    int currentFrameIndex = (int)m_CurrentFrameFieldInfo.GetValue(m_ProfilerWindow);
                    float time;
                    float duration;
                    int instanceId;
                    int nativeIndex;
                    int threadIndex;
                    if (GetMarkerInfo(markerName, currentFrameIndex, threadFilters, out threadIndex, out nativeIndex, out time, out duration, out instanceId))
                    {
                        /*
                        Debug.Log(string.Format("Setting profiler to {0} on {1} at frame {2} at {3}ms for {4}ms ({5})",
                                                markerName, currentFrameIndex, threadFilter, time, duration, instanceId));
                         */

                        if (m_SelectedNameFieldInfo != null)
                            m_SelectedNameFieldInfo.SetValue(selectedEntry, markerName);
                        if (m_SelectedTimeFieldInfo != null)
                            m_SelectedTimeFieldInfo.SetValue(selectedEntry, time);
                        if (m_SelectedDurationFieldInfo != null)
                            m_SelectedDurationFieldInfo.SetValue(selectedEntry, duration);
                        if (m_SelectedInstanceIdFieldInfo != null)
                            m_SelectedInstanceIdFieldInfo.SetValue(selectedEntry, instanceId);
                        if (m_SelectedFrameIdFieldInfo != null)
                            m_SelectedFrameIdFieldInfo.SetValue(selectedEntry, currentFrameIndex);
                        if (m_SelectedNativeIndexFieldInfo != null)
                            m_SelectedNativeIndexFieldInfo.SetValue(selectedEntry, nativeIndex);
                        if (m_SelectedThreadIndexFieldInfo != null)
                            m_SelectedThreadIndexFieldInfo.SetValue(selectedEntry, threadIndex);

                        // TODO : Update to fill in the total and number of instances.
                        // For now we force Instance count to 1 to avoid the incorrect info showing.
                        if (m_SelectedInstanceCountFieldInfo != null)
                            m_SelectedInstanceCountFieldInfo.SetValue(selectedEntry, 1);
                        if (m_SelectedInstanceCountForThreadFieldInfo != null)
                            m_SelectedInstanceCountForThreadFieldInfo.SetValue(selectedEntry, 1);
                        if (m_SelectedInstanceCountForFrameFieldInfo != null)
                            m_SelectedInstanceCountForFrameFieldInfo.SetValue(selectedEntry, 1);
                        if (m_SelectedThreadCountFieldInfo != null)
                            m_SelectedThreadCountFieldInfo.SetValue(selectedEntry, 1);
                        if (m_SelectedMetaDataFieldInfo != null)
                            m_SelectedMetaDataFieldInfo.SetValue(selectedEntry, "");
                        if (m_SelectedCallstackInfoFieldInfo != null)
                            m_SelectedCallstackInfoFieldInfo.SetValue(selectedEntry, "");

                        m_ProfilerWindow.Repaint();
                        m_SendingSelectionEventToProfilerWindowInProgress = false;
                        return true;
                    }
                }
            }
#endif
            m_SendingSelectionEventToProfilerWindowInProgress = false;
            return false;
        }

        public bool JumpToFrame(int index)
        {
            if (m_ProfilerWindow == null)
                return false;

            if (index - 1 < ProfilerDriver.firstFrameIndex)
                return false;
            if (index - 1 > ProfilerDriver.lastFrameIndex)
                return false;

#if UNITY_2021_1_OR_NEWER
            m_ProfilerWindow.selectedFrameIndex = index - 1;
#else
            m_CurrentFrameFieldInfo.SetValue(m_ProfilerWindow, index - 1);
#endif
            m_ProfilerWindow.Repaint();
            return true;
        }

        public int selectedFrame
        {
            get
            {
                if (m_ProfilerWindow == null)
                    return 0;
#if UNITY_2021_1_OR_NEWER
                return (int)m_ProfilerWindow.selectedFrameIndex + 1;
#else
                return (int)m_CurrentFrameFieldInfo.GetValue(m_ProfilerWindow) + 1;
#endif
            }
        }

        public event Action<int> selectedFrameChanged = delegate {};

        public void PollSelectedFrameChanges()
        {
            var currentlySelectedFrame = selectedFrame;
            if (m_LastSelectedFrameInProfilerWindow != currentlySelectedFrame && !m_SendingSelectionEventToProfilerWindowInProgress)
            {
                m_LastSelectedFrameInProfilerWindow = currentlySelectedFrame;
                selectedFrameChanged(currentlySelectedFrame);
            }
        }

        public bool IsRecording()
        {
#if UNITY_2017_4_OR_NEWER
            return ProfilerDriver.enabled;
#else
            return false;
#endif
        }

        public void StopRecording()
        {
#if UNITY_2017_4_OR_NEWER
            // Stop recording first
            ProfilerDriver.enabled = false;
#endif
        }

        public void StartRecording()
        {
#if UNITY_2017_4_OR_NEWER
            // Stop recording first
            ProfilerDriver.enabled = true;
#endif
        }

        public void OnDisable()
        {
            if (m_ProfilerWindow != null)
            {
                m_ProfilerWindow = null;
            }

#if UNITY_2021_1_OR_NEWER
            if (m_CpuProfilerModule != null)
            {
                m_CpuProfilerModule.selectionChanged -= OnSelectionChangedInCpuProfilerModule;
                m_CpuProfilerModule = null;
            }
#endif
        }
    }
}
