private void AddToList(PlayerStats playerStats, List <PlayerSubStats> list, List <PlayerSubStats> additionalStats)
 {
     additionalStats.ForEach(stats =>
     {
         if (list.Find(item => item.Name == stats.Name) is PlayerSubStats found)
         {
             var combined = new PlayerSubStats()
             {
                 Name = found.Name
             };
             StatsUtil.MergeStats(combined, found);
             StatsUtil.MergeStats(combined, stats);
             StatsUtil.CalculateRates(combined, RaidStats, playerStats);
             list.Remove(found);
             list.Add(combined);
         }
         else
         {
             list.Add(stats);
         }
     });
 }
        internal void ComputeOverlayDamageStats(DamageRecord record, double beginTime, int timeout, OverlayDamageStats overlayStats = null)
        {
            try
            {
                // set current time
                overlayStats.LastTime = beginTime;

                if (record != null && (record.Type != Labels.BANE || MainWindow.IsBaneDamageEnabled))
                {
                    overlayStats.RaidStats.Total += record.Total;

                    var raidTimeRange = new TimeRange();
                    overlayStats.InactiveFights.ForEach(fight => raidTimeRange.Add(new TimeSegment(Math.Max(fight.BeginDamageTime, overlayStats.BeginTime), fight.LastDamageTime)));
                    overlayStats.ActiveFights.ForEach(fight => raidTimeRange.Add(new TimeSegment(Math.Max(fight.BeginDamageTime, overlayStats.BeginTime), fight.LastDamageTime)));
                    overlayStats.RaidStats.TotalSeconds = Math.Max(raidTimeRange.GetTotal(), overlayStats.RaidStats.TotalSeconds);

                    // update pets
                    UpdatePetMapping(record);

                    bool isPet         = PetToPlayer.TryGetValue(record.Attacker, out string player);
                    bool needAggregate = isPet || (!isPet && PlayerPets.ContainsKey(record.Attacker) && overlayStats.TopLevelStats.ContainsKey(record.Attacker + " +Pets"));

                    if (!needAggregate)
                    {
                        // not a pet
                        PlayerStats stats = StatsUtil.CreatePlayerStats(overlayStats.IndividualStats, record.Attacker);
                        overlayStats.TopLevelStats[record.Attacker] = stats;
                        StatsUtil.UpdateStats(stats, record);
                        stats.LastTime = beginTime;
                    }
                    else
                    {
                        string origName      = player ?? record.Attacker;
                        string aggregateName = origName + " +Pets";

                        PlayerStats aggregatePlayerStats;
                        aggregatePlayerStats = StatsUtil.CreatePlayerStats(overlayStats.IndividualStats, aggregateName, origName);
                        overlayStats.TopLevelStats[aggregateName] = aggregatePlayerStats;

                        if (overlayStats.TopLevelStats.ContainsKey(origName))
                        {
                            var origPlayer = overlayStats.TopLevelStats[origName];
                            StatsUtil.MergeStats(aggregatePlayerStats, origPlayer);
                            overlayStats.TopLevelStats.Remove(origName);
                            overlayStats.IndividualStats.Remove(origName);
                        }

                        if (record.Attacker != origName && overlayStats.TopLevelStats.ContainsKey(record.Attacker))
                        {
                            var origPet = overlayStats.TopLevelStats[record.Attacker];
                            StatsUtil.MergeStats(aggregatePlayerStats, origPet);
                            overlayStats.TopLevelStats.Remove(record.Attacker);
                            overlayStats.IndividualStats.Remove(record.Attacker);
                        }

                        StatsUtil.UpdateStats(aggregatePlayerStats, record);
                        aggregatePlayerStats.LastTime = beginTime;
                    }

                    overlayStats.RaidStats.DPS = (long)Math.Round(overlayStats.RaidStats.Total / overlayStats.RaidStats.TotalSeconds, 2);

                    var list  = overlayStats.TopLevelStats.Values.OrderByDescending(item => item.Total).ToList();
                    int found = list.FindIndex(stats => stats.Name.StartsWith(ConfigUtil.PlayerName, StringComparison.Ordinal));
                    var you   = found > -1 ? list[found] : null;

                    int renumber;
                    if (found > 4)
                    {
                        you.Rank = Convert.ToUInt16(found + 1);
                        overlayStats.StatsList.Clear();
                        overlayStats.StatsList.AddRange(list.Where(stats => (stats != null && stats == you) || beginTime - stats.LastTime <= timeout).Take(4));
                        overlayStats.StatsList.Add(you);
                        renumber = overlayStats.StatsList.Count - 1;
                    }
                    else
                    {
                        overlayStats.StatsList.Clear();
                        overlayStats.StatsList.AddRange(list.Where(stats => (stats != null && stats == you) || beginTime - stats.LastTime <= timeout).Take(5));
                        renumber = overlayStats.StatsList.Count;
                    }

                    for (int i = 0; i < overlayStats.StatsList.Count; i++)
                    {
                        if (i < renumber)
                        {
                            overlayStats.StatsList[i].Rank = Convert.ToUInt16(i + 1);
                        }

                        // only update time if damage changed
                        if (overlayStats.StatsList[i].LastTime == beginTime && overlayStats.StatsList[i].CalcTime != beginTime)
                        {
                            var timeRange = new TimeRange();
                            if (PlayerPets.TryGetValue(overlayStats.StatsList[i].OrigName, out ConcurrentDictionary <string, byte> mapping))
                            {
                                mapping.Keys.ToList().ForEach(key =>
                                {
                                    AddSegments(timeRange, overlayStats.InactiveFights, key, overlayStats.BeginTime);
                                    AddSegments(timeRange, overlayStats.ActiveFights, key, overlayStats.BeginTime);
                                });
                            }

                            AddSegments(timeRange, overlayStats.InactiveFights, overlayStats.StatsList[i].OrigName, overlayStats.BeginTime);
                            AddSegments(timeRange, overlayStats.ActiveFights, overlayStats.StatsList[i].OrigName, overlayStats.BeginTime);
                            overlayStats.StatsList[i].TotalSeconds = Math.Max(timeRange.GetTotal(), overlayStats.StatsList[i].TotalSeconds);
                            overlayStats.StatsList[i].CalcTime     = beginTime;
                        }

                        StatsUtil.UpdateCalculations(overlayStats.StatsList[i], overlayStats.RaidStats);
                    }

                    var count = overlayStats.InactiveFights.Count + overlayStats.ActiveFights.Count;
                    overlayStats.TargetTitle = (count > 1 ? "C(" + count + "): " : "") + record.Defender;
                    overlayStats.TimeTitle   = string.Format(CultureInfo.CurrentCulture, StatsUtil.TIME_FORMAT, overlayStats.RaidStats.TotalSeconds);
                    overlayStats.TotalTitle  = string.Format(CultureInfo.CurrentCulture, StatsUtil.TOTAL_FORMAT, StatsUtil.FormatTotals(overlayStats.RaidStats.Total),
                                                             " Damage ", StatsUtil.FormatTotals(overlayStats.RaidStats.DPS));
                }
            }
#pragma warning disable CA1031 // Do not catch general exception types
            catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
            {
                if (ex is ArgumentNullException || ex is NullReferenceException || ex is ArgumentOutOfRangeException || ex is ArgumentException || ex is OutOfMemoryException)
                {
                    LOG.Error(ex);
                }
            }

            void AddSegments(TimeRange range, List <Fight> fights, string key, double start)
            {
                fights.ForEach(fight =>
                {
                    if (fight.DamageSegments.TryGetValue(key, out TimeSegment segment) && segment.EndTime >= start)
                    {
                        range.Add(new TimeSegment(Math.Max(segment.BeginTime, start), segment.EndTime));
                    }
                });
            }
        }
        private void BuildGroups(PlayerStats playerStats, List <PlayerSubStats> all)
        {
            List <PlayerSubStats> list = new List <PlayerSubStats>();
            PlayerSubStats        dots = new PlayerSubStats()
            {
                Name = Labels.DOT, Type = Labels.DOT
            };
            PlayerSubStats dds = new PlayerSubStats()
            {
                Name = Labels.DD, Type = Labels.DD
            };
            PlayerSubStats procs = new PlayerSubStats()
            {
                Name = Labels.PROC, Type = Labels.PROC
            };
            PlayerSubStats resisted = new PlayerSubStats()
            {
                Name = Labels.RESIST, Type = Labels.RESIST, ResistRate = 100
            };
            List <PlayerSubStats> allDots     = new List <PlayerSubStats>();
            List <PlayerSubStats> allDds      = new List <PlayerSubStats>();
            List <PlayerSubStats> allProcs    = new List <PlayerSubStats>();
            List <PlayerSubStats> allResisted = new List <PlayerSubStats>();

            all.ForEach(sub =>
            {
                PlayerSubStats stats = null;

                switch (sub.Type)
                {
                case Labels.DOT:
                    stats = dots;
                    allDots.Add(sub);
                    break;

                case Labels.DD:
                case Labels.BANE:
                    stats = dds;
                    allDds.Add(sub);
                    break;

                case Labels.PROC:
                    stats = procs;
                    allProcs.Add(sub);
                    break;

                case Labels.RESIST:
                    stats = resisted;
                    allResisted.Add(sub);
                    break;

                default:
                    list.Add(sub);
                    break;
                }

                StatsUtil.MergeStats(stats, sub);
            });

            foreach (var stats in new PlayerSubStats[] { dots, dds, procs, resisted })
            {
                StatsUtil.CalculateRates(stats, RaidStats, playerStats);
            }

            UnGroupedDD[playerStats.Name]       = allDds;
            UnGroupedDoT[playerStats.Name]      = allDots;
            UnGroupedProcs[playerStats.Name]    = allProcs;
            UnGroupedResisted[playerStats.Name] = allResisted;
            GroupedDD[playerStats.Name]         = dds;
            GroupedDoT[playerStats.Name]        = dots;
            GroupedProcs[playerStats.Name]      = procs;
            GroupedResisted[playerStats.Name]   = resisted;
            OtherDamage[playerStats.Name]       = list;

            Dispatcher.InvokeAsync(() =>
            {
                if (allDds.Count > 0 && !groupDirectDamage.IsEnabled)
                {
                    groupDirectDamage.IsEnabled = true;
                }

                if (allProcs.Count > 0 && !groupProcs.IsEnabled)
                {
                    groupProcs.IsEnabled = true;
                }

                if (allDots.Count > 0 && !groupDoT.IsEnabled)
                {
                    groupDoT.IsEnabled = true;
                }

                if (allResisted.Count > 0 && !groupResisted.IsEnabled)
                {
                    groupResisted.IsEnabled = true;
                }
            });
        }
        internal OverlayDamageStats ComputeOverlayDamageStats(DamageRecord record, double beginTime, OverlayDamageStats overlayStats = null)
        {
            if (overlayStats == null)
            {
                overlayStats = new OverlayDamageStats
                {
                    RaidStats = new PlayerStats()
                };

                overlayStats.RaidStats.BeginTime = beginTime;
            }
            else
            {
                overlayStats.RaidStats = overlayStats.RaidStats;
            }

            if (overlayStats.UniqueNpcs.Count == 0 || (beginTime - overlayStats.RaidStats.LastTime > DataManager.FIGHT_TIMEOUT))
            {
                overlayStats.RaidStats.Total     = 0;
                overlayStats.RaidStats.BeginTime = beginTime;
                overlayStats.UniqueNpcs.Clear();
                overlayStats.TopLevelStats.Clear();
                overlayStats.AggregateStats.Clear();
                overlayStats.IndividualStats.Clear();
            }

            overlayStats.RaidStats.LastTime     = beginTime;
            overlayStats.RaidStats.TotalSeconds = overlayStats.RaidStats.LastTime - overlayStats.RaidStats.BeginTime + 1;

            if (record != null && (record.Type != Labels.BANE || MainWindow.IsBaneDamageEnabled))
            {
                overlayStats.UniqueNpcs[record.Defender] = 1;
                overlayStats.RaidStats.Total            += record.Total;

                // see if there's a pet mapping, check this first
                string pname = PlayerManager.Instance.GetPlayerFromPet(record.Attacker);
                if (pname != null || !string.IsNullOrEmpty(pname = record.AttackerOwner))
                {
                    PlayerHasPet[pname]          = 1;
                    PetToPlayer[record.Attacker] = pname;
                }

                bool isPet         = PetToPlayer.TryGetValue(record.Attacker, out string player);
                bool needAggregate = isPet || (!isPet && PlayerHasPet.ContainsKey(record.Attacker) && overlayStats.TopLevelStats.ContainsKey(record.Attacker + " +Pets"));

                if (!needAggregate || player == Labels.UNASSIGNED)
                {
                    // not a pet
                    PlayerStats stats = StatsUtil.CreatePlayerStats(overlayStats.IndividualStats, record.Attacker);
                    StatsUtil.UpdateStats(stats, record, beginTime);
                    overlayStats.TopLevelStats[record.Attacker] = stats;
                    stats.TotalSeconds = stats.LastTime - stats.BeginTime + 1;
                }
                else
                {
                    string origName      = player ?? record.Attacker;
                    string aggregateName = origName + " +Pets";

                    PlayerStats aggregatePlayerStats;
                    aggregatePlayerStats = StatsUtil.CreatePlayerStats(overlayStats.IndividualStats, aggregateName, origName);
                    overlayStats.TopLevelStats[aggregateName] = aggregatePlayerStats;

                    if (overlayStats.TopLevelStats.ContainsKey(origName))
                    {
                        var origPlayer = overlayStats.TopLevelStats[origName];
                        StatsUtil.MergeStats(aggregatePlayerStats, origPlayer);
                        overlayStats.TopLevelStats.Remove(origName);
                        overlayStats.IndividualStats.Remove(origName);
                    }

                    if (record.Attacker != origName && overlayStats.TopLevelStats.ContainsKey(record.Attacker))
                    {
                        var origPet = overlayStats.TopLevelStats[record.Attacker];
                        StatsUtil.MergeStats(aggregatePlayerStats, origPet);
                        overlayStats.TopLevelStats.Remove(record.Attacker);
                        overlayStats.IndividualStats.Remove(record.Attacker);
                    }

                    StatsUtil.UpdateStats(aggregatePlayerStats, record, beginTime);
                    aggregatePlayerStats.TotalSeconds = aggregatePlayerStats.LastTime - aggregatePlayerStats.BeginTime + 1;
                }

                overlayStats.RaidStats.DPS = (long)Math.Round(overlayStats.RaidStats.Total / overlayStats.RaidStats.TotalSeconds, 2);

                var list  = overlayStats.TopLevelStats.Values.OrderByDescending(item => item.Total).ToList();
                int found = list.FindIndex(stats => stats.Name.StartsWith(ConfigUtil.PlayerName, StringComparison.Ordinal));

                int renumber;
                if (found > 4)
                {
                    var you = list[found];
                    you.Rank = Convert.ToUInt16(found + 1);
                    overlayStats.StatsList.Clear();
                    overlayStats.StatsList.AddRange(list.Take(4));
                    overlayStats.StatsList.Add(you);
                    renumber = overlayStats.StatsList.Count - 1;
                }
                else
                {
                    overlayStats.StatsList.Clear();
                    overlayStats.StatsList.AddRange(list.Take(5));
                    renumber = overlayStats.StatsList.Count;
                }

                for (int i = 0; i < renumber; i++)
                {
                    overlayStats.StatsList[i].Rank = Convert.ToUInt16(i + 1);
                }

                // only calculate the top few
                Parallel.ForEach(overlayStats.StatsList, top => StatsUtil.UpdateCalculations(top, overlayStats.RaidStats));
                overlayStats.TargetTitle = (overlayStats.UniqueNpcs.Count > 1 ? "Combined (" + overlayStats.UniqueNpcs.Count + "): " : "") + record.Defender;
                overlayStats.TimeTitle   = string.Format(CultureInfo.CurrentCulture, StatsUtil.TIME_FORMAT, overlayStats.RaidStats.TotalSeconds);
                overlayStats.TotalTitle  = string.Format(CultureInfo.CurrentCulture, StatsUtil.TOTAL_FORMAT, StatsUtil.FormatTotals(overlayStats.RaidStats.Total), " Damage ", StatsUtil.FormatTotals(overlayStats.RaidStats.DPS));
            }

            return(overlayStats);
        }