using System.Collections.Generic;
using UnityEngine;
using UnityEditorInternal;
using System.Text.RegularExpressions;
using System;

namespace UnityEditor.Performance.ProfileAnalyzer
{
    internal class ProfileAnalyzer
    {
        public const int kDepthAll = -1;

        int m_Progress = 0;
        ProfilerFrameDataIterator m_frameData;
        List<string> m_threadNames = new List<string>();
        ProfileAnalysis m_analysis;

        public ProfileAnalyzer()
        {
        }

        public void QuickScan()
        {
            var frameData = new ProfilerFrameDataIterator();

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

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

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

                if (!threadNameCount.ContainsKey(threadName))
                    threadNameCount.Add(threadName, 1);
                else
                    threadNameCount[threadName] += 1;

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

                m_threadNames.Add(threadNameWithIndex);
            }

            frameData.Dispose();
        }

        public List<string> GetThreadNames()
        {
            return m_threadNames;
        }

        void CalculateFrameTimeStats(ProfileData data, out float median, out float mean, out float standardDeviation)
        {
            List<float> frameTimes = new List<float>();
            for (int frameIndex = 0; frameIndex < data.GetFrameCount(); frameIndex++)
            {
                var frame = data.GetFrame(frameIndex);
                float msFrame = frame.msFrame;
                frameTimes.Add(msFrame);
            }
            frameTimes.Sort();
            median = frameTimes[frameTimes.Count / 2];


            double total = 0.0f;
            foreach (float msFrame in frameTimes)
            {
                total += msFrame;
            }
            mean = (float)(total / (double)frameTimes.Count);


            if (frameTimes.Count <= 1)
            {
                standardDeviation = 0f;
            }
            else
            {
                total = 0.0f;
                foreach (float msFrame in frameTimes)
                {
                    float d = msFrame - mean;
                    total += (d * d);
                }
                total /= (frameTimes.Count - 1);
                standardDeviation = (float)Math.Sqrt(total);
            }
        }

        int GetClampedOffsetToFrame(ProfileData profileData, int frameIndex)
        {
            int frameOffset = profileData.DisplayFrameToOffset(frameIndex);
            if (frameOffset < 0)
            {
                Debug.Log(string.Format("Frame index {0} offset {1} < 0, clamping", frameIndex, frameOffset));
                frameOffset = 0;
            }
            if (frameOffset >= profileData.GetFrameCount())
            {
                Debug.Log(string.Format("Frame index {0} offset {1} >= frame count {2}, clamping", frameIndex, frameOffset, profileData.GetFrameCount()));
                frameOffset = profileData.GetFrameCount() - 1;
            }

            return frameOffset;
        }

        public static bool MatchThreadFilter(string threadNameWithIndex, List<string> threadFilters)
        {
            if (threadFilters == null || threadFilters.Count == 0)
                return false;

            if (threadFilters.Contains(threadNameWithIndex))
                return true;

            return false;
        }

        public bool IsNullOrWhiteSpace(string s)
        {
            // return string.IsNullOrWhiteSpace(parentMarker);
            if (s == null || Regex.IsMatch(s, @"^[\s]*$"))
                return true;

            return false;
        }

        public void RemoveMarkerTimeFromParents(MarkerData[] markers, ProfileData profileData, ProfileThread threadData, int markerAt)
        {
            // Get the info for the marker we plan to remove (assume thats what we are at)
            ProfileMarker profileMarker = threadData.markers[markerAt];
            float markerTime = profileMarker.msMarkerTotal;

            // Traverse parents and remove time from them
            int currentDepth = profileMarker.depth;
            for (int parentMarkerAt = markerAt - 1; parentMarkerAt >= 0; parentMarkerAt--)
            {
                ProfileMarker parentMarkerData = threadData.markers[parentMarkerAt];
                if (parentMarkerData.depth == currentDepth - 1)
                {
                    currentDepth--;
                    if (parentMarkerData.nameIndex < markers.Length) // Had an issue where marker not yet processed(marker from another thread)
                    {
                        MarkerData parentMarker = markers[parentMarkerData.nameIndex];

                        // If a depth slice is applied we may not have a parent marker stored
                        if (parentMarker != null)
                        {
                            // Revise the duration of parent to remove time from there too
                            // Note if the marker to remove is nested (i.e. parent of the same name, this could reduce the msTotal, more than we add to the timeIgnored)
                            parentMarker.msTotal -= markerTime;

                            // Reduce from the max marker time too
                            // This could be incorrect when there are many instances that contribute the the total time
                            if (parentMarker.msMaxIndividual > markerTime)
                            {
                                parentMarker.msMaxIndividual -= markerTime;
                            }
                            if (parentMarker.msMinIndividual > markerTime)
                            {
                                parentMarker.msMinIndividual -= markerTime;
                            }

                            // Revise stored frame time
                            FrameTime frameTime = parentMarker.frames[parentMarker.frames.Count - 1];
                            frameTime = new FrameTime(frameTime.frameIndex, frameTime.ms - markerTime, frameTime.count);
                            parentMarker.frames[parentMarker.frames.Count - 1] = frameTime;

                            // Note that we have modified the time
                            parentMarker.timeRemoved += markerTime;

                            // Note markerTime can be 0 in some cases.
                            // Make sure timeRemoved is never left at 0.0
                            // This makes sure we can test for non zero to indicate the marker has been removed 
                            if (parentMarker.timeRemoved == 0.0)
                                parentMarker.timeRemoved = double.Epsilon;
                        }
                    }
                }
            }
        }

        public int RemoveMarker(ProfileThread threadData, int markerAt)
        {
            ProfileMarker profileMarker = threadData.markers[markerAt];
            int at = markerAt;

            // skip marker
            at++;

            // Skip children
            int currentDepth = profileMarker.depth;
            while (at < threadData.markers.Count)
            {
                profileMarker = threadData.markers[at];
                if (profileMarker.depth <= currentDepth)
                    break;

                at++;
            }

            // Mark the following number to be ignored
            int markerAndChildCount = at - markerAt;

            return markerAndChildCount;
        }

        public ProfileAnalysis Analyze(ProfileData profileData, List<int> selectionIndices, List<string> threadFilters, int depthFilter, bool selfTimes = false, string parentMarker = null, float timeScaleMax = 0, string removeMarker = null)
        {
            m_Progress = 0;
            if (profileData == null)
            {
                return null;
            }
            if (profileData.GetFrameCount() <= 0)
            {
                return null;
            }

            int frameCount = selectionIndices.Count;
            if (frameCount < 0)
            {
                return null;
            }

            if (profileData.HasFrames && !profileData.HasThreads)
            {
                if (!ProfileData.Load(profileData.FilePath, out profileData))
                {
                    return null;
                }
            }

            bool processMarkers = (threadFilters != null);

            ProfileAnalysis analysis = new ProfileAnalysis();
            if (selectionIndices.Count > 0)
                analysis.SetRange(selectionIndices[0], selectionIndices[selectionIndices.Count - 1]);
            else
                analysis.SetRange(0, 0);

            m_threadNames.Clear();

            int maxMarkerDepthFound = 0;
            var threads = new Dictionary<string, ThreadData>();
            var markers = new MarkerData[profileData.MarkerNameCount];
            var removedMarkers = new Dictionary<string, double>();

            var mainThreadIdentifier = new ThreadIdentifier("Main Thread", 1);

            int markerCount = 0;

            bool filteringByParentMarker = false;
            int parentMarkerIndex = -1;
            if (!IsNullOrWhiteSpace(parentMarker))
            {
                // Returns -1 if this marker doesn't exist in the data set
                parentMarkerIndex = profileData.GetMarkerIndex(parentMarker);
                filteringByParentMarker = true;
            }

            int at = 0;
            foreach (int frameIndex in selectionIndices)
            {
                int frameOffset = profileData.DisplayFrameToOffset(frameIndex);
                var frameData = profileData.GetFrame(frameOffset);
                if (frameData == null)
                    continue;
                var msFrame = frameData.msFrame;

                if (processMarkers)
                {
                    // get the file reader in case we need to rebuild the markers rather than opening
                    // the file for every marker
                    for (int threadIndex = 0; threadIndex < frameData.threads.Count; threadIndex++)
                    {
                        float msTimeOfMinDepthMarkers = 0.0f;
                        float msIdleTimeOfMinDepthMarkers = 0.0f;

                        var threadData = frameData.threads[threadIndex];
                        var threadNameWithIndex = profileData.GetThreadName(threadData);

                        ThreadData thread;
                        if (!threads.ContainsKey(threadNameWithIndex))
                        {
                            m_threadNames.Add(threadNameWithIndex);

                            thread = new ThreadData(threadNameWithIndex);

                            analysis.AddThread(thread);
                            threads[threadNameWithIndex] = thread;

                            // Update threadsInGroup for all thread records of the same group name
                            foreach (var threadAt in threads.Values)
                            {
                                if (threadAt == thread)
                                    continue;

                                if (thread.threadGroupName == threadAt.threadGroupName)
                                {
                                    threadAt.threadsInGroup += 1;
                                    thread.threadsInGroup += 1;
                                }
                            }
                        }
                        else
                        {
                            thread = threads[threadNameWithIndex];
                        }

                        bool include = MatchThreadFilter(threadNameWithIndex, threadFilters);

                        int parentMarkerDepth = -1;

                        if (threadData.markers.Count != threadData.markerCount)
                        {
                            if (!threadData.ReadMarkers(profileData.FilePath))
                            {
                                Debug.LogError("failed to read markers");
                            }
                        }

                        int markerAndChildCount = 0;
                        for (int markerAt = 0, n = threadData.markers.Count; markerAt < n; markerAt++)
                        {
                            var markerData = threadData.markers[markerAt];

                            if (markerAndChildCount > 0)
                                markerAndChildCount--;

                            string markerName = null;

                            float ms = markerData.msMarkerTotal - (selfTimes ? markerData.msChildren : 0);
                            var markerDepth = markerData.depth;
                            if (markerDepth > maxMarkerDepthFound)
                                maxMarkerDepthFound = markerDepth;

                            if (markerDepth == 1)
                            {
                                markerName = profileData.GetMarkerName(markerData);
                                if (markerName.Equals("Idle", StringComparison.Ordinal))
                                    msIdleTimeOfMinDepthMarkers += ms;
                                else
                                    msTimeOfMinDepthMarkers += ms;
                            }

                            if (removeMarker != null)
                            {
                                if (markerAndChildCount <= 0)   // If we are already removing markers - don't focus on other occurances in the children
                                {
                                    if (markerName == null)
                                        markerName = profileData.GetMarkerName(markerData);

                                    if (markerName == removeMarker)
                                    {
                                        float removeMarkerTime = markerData.msMarkerTotal;

                                        // Remove this markers time from frame time (if its on the main thread)
                                        if (thread.threadNameWithIndex == mainThreadIdentifier.threadNameWithIndex)
                                        {
                                            msFrame -= removeMarkerTime;
                                        }

                                        if (selfTimes == false) // (Self times would not need thread or parent adjustments)
                                        {
                                            // And from thread time
                                            if (markerName == "Idle")
                                                msIdleTimeOfMinDepthMarkers -= removeMarkerTime;
                                            else
                                                msTimeOfMinDepthMarkers -= removeMarkerTime;

                                            // And from parents
                                            RemoveMarkerTimeFromParents(markers, profileData, threadData, markerAt);
                                        }

                                        markerAndChildCount = RemoveMarker(threadData, markerAt);
                                    }
                                }
                            }

                            if (!include)
                                continue;

                            // If only looking for markers below the parent
                            if (filteringByParentMarker)
                            {
                                // If found the parent marker
                                if (markerData.nameIndex == parentMarkerIndex)
                                {
                                    // And we are not already below the parent higher in the depth tree
                                    if (parentMarkerDepth < 0)
                                    {
                                        // record the parent marker depth
                                        parentMarkerDepth = markerData.depth;
                                    }
                                }
                                else
                                {
                                    // If we are now above or beside the parent marker then we are done for this level
                                    if (markerData.depth <= parentMarkerDepth)
                                    {
                                        parentMarkerDepth = -1;
                                    }
                                }

                                if (parentMarkerDepth < 0)
                                    continue;
                            }

                            if (depthFilter != kDepthAll && markerDepth != depthFilter)
                                continue;

                            MarkerData marker = markers[markerData.nameIndex];
                            if (marker != null)
                            {
                                if (!marker.threads.Contains(threadNameWithIndex))
                                    marker.threads.Add(threadNameWithIndex);
                            }
                            else
                            {
                                if (markerName == null)
                                    markerName = profileData.GetMarkerName(markerData);
                                marker = new MarkerData(markerName);
                                marker.firstFrameIndex = frameIndex;
                                marker.minDepth = markerDepth;
                                marker.maxDepth = markerDepth;
                                marker.threads.Add(threadNameWithIndex);
                                analysis.AddMarker(marker);
                                markers[markerData.nameIndex] = marker;
                                markerCount += 1;
                            }
                            marker.count += 1;

                            if (markerAndChildCount > 0)
                            {
                                marker.timeIgnored += ms;

                                // Note ms can be 0 in some cases.
                                // Make sure timeIgnored is never left at 0.0
                                // This makes sure we can test for non zero to indicate the marker has been ignored 
                                if (marker.timeIgnored == 0.0)
                                    marker.timeIgnored = double.Epsilon;

                                // zero out removed marker time
                                // so we don't record in the individual marker times, marker frame times or min/max times
                                // ('min/max times' is calculated later from marker frame times)
                                ms = 0f;
                            }

                            marker.msTotal += ms;

                            // Individual marker time (not total over frame)
                            if (ms < marker.msMinIndividual)
                            {
                                marker.msMinIndividual = ms;
                                marker.minIndividualFrameIndex = frameIndex;
                            }
                            if (ms > marker.msMaxIndividual)
                            {
                                marker.msMaxIndividual = ms;
                                marker.maxIndividualFrameIndex = frameIndex;
                            }

                            // Record highest depth foun
                            if (markerDepth < marker.minDepth)
                                marker.minDepth = markerDepth;
                            if (markerDepth > marker.maxDepth)
                                marker.maxDepth = markerDepth;

                            FrameTime frameTime;
                            if (frameIndex != marker.lastFrame)
                            {
                                marker.presentOnFrameCount += 1;
                                frameTime = new FrameTime(frameIndex, ms, 1);
                                marker.frames.Add(frameTime);
                                marker.lastFrame = frameIndex;
                            }
                            else
                            {
                                frameTime = marker.frames[marker.frames.Count - 1];
                                frameTime = new FrameTime(frameTime.frameIndex, frameTime.ms + ms, frameTime.count + 1);
                                marker.frames[marker.frames.Count - 1] = frameTime;
                            }
                        }

                        if (include)
                            thread.frames.Add(new ThreadFrameTime(frameIndex, msTimeOfMinDepthMarkers, msIdleTimeOfMinDepthMarkers));
                    }
                }

                analysis.UpdateSummary(frameIndex, msFrame);

                at++;
                m_Progress = (100 * at) / frameCount;
            }

            analysis.GetFrameSummary().totalMarkers = profileData.MarkerNameCount;
            analysis.Finalise(timeScaleMax, maxMarkerDepthFound);

            /*
            foreach (int frameIndex in selectionIndices)
            {
                int frameOffset = profileData.DisplayFrameToOffset(frameIndex);

                var frameData = profileData.GetFrame(frameOffset);
                foreach (var threadData in frameData.threads)
                {
                    var threadNameWithIndex = profileData.GetThreadName(threadData);

                    if (filterThreads && threadFilter != threadNameWithIndex)
                        continue;

                    const bool enterChildren = true;
                    foreach (var markerData in threadData.markers)
                    {
                        var markerName = markerData.name;
                        var ms = markerData.msFrame;
                        var markerDepth = markerData.depth;
                        if (depthFilter != kDepthAll && markerDepth != depthFilter)
                            continue;

                        MarkerData marker = markers[markerName];
                        bucketIndex = (range > 0) ? (int)(((marker.buckets.Length-1) * (ms - first)) / range) : 0;
                        if (bucketIndex<0 || bucketIndex > (marker.buckets.Length - 1))
                        {
                            // This can happen if a single marker range is longer than the frame start end (which could occur if running on a separate thread)
                            // Debug.Log(string.Format("Marker {0} : {1}ms exceeds range {2}-{3} on frame {4}", marker.name, ms, first, last, frameIndex));
                            if (bucketIndex > (marker.buckets.Length - 1))
                                bucketIndex = (marker.buckets.Length - 1);
                            else
                                bucketIndex = 0;
                        }
                        marker.individualBuckets[bucketIndex] += 1;
                    }
                }
            }
*/
            m_Progress = 100;
            return analysis;
        }

        public int GetProgress()
        {
            return m_Progress;
        }
    }
}