/*
 * Copyright (C) 2012-2017 CypherCore <http://github.com/CypherCore>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using Framework.Constants;
using Game.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Game.DungeonFinding
{
    public class LFGQueue
    {
        public static string ConcatenateGuids(List<ObjectGuid> guids)
        {
            if (guids.Empty())
                return "";

            // need the guids in order to avoid duplicates
            StringBuilder val = new StringBuilder();
            guids.Sort();
            var it = guids.First();
            val.Append(it);
            foreach (var guid in guids)
            {
                if (guid == it)
                    continue;
                val.AppendFormat("|{0}", guid);
            }

            return val.ToString();
        }

        public static string GetRolesString(LfgRoles roles)
        {
            StringBuilder rolesstr = new StringBuilder();

            if (roles.HasAnyFlag(LfgRoles.Tank))
                rolesstr.Append("Tank");

            if (roles.HasAnyFlag(LfgRoles.Healer))
            {
                if (rolesstr.Capacity != 0)
                    rolesstr.Append(", ");
                rolesstr.Append("Healer");
            }

            if (roles.HasAnyFlag(LfgRoles.Damage))
            {
                if (rolesstr.Capacity != 0)
                    rolesstr.Append(", ");
                rolesstr.Append("Damage");
            }

            if (roles.HasAnyFlag(LfgRoles.Leader))
            {
                if (rolesstr.Capacity != 0)
                    rolesstr.Append(", ");
                rolesstr.Append("Leader");
            }

            if (rolesstr.Capacity == 0)
                rolesstr.Append("None");

            return rolesstr.ToString();
        }

        public static string ConcatenateDungeons(List<uint> dungeons)
        {
            string str = "";
            if (!dungeons.Empty())
            {
                foreach (var it in dungeons)
                {
                    if (!string.IsNullOrEmpty(str))
                        str += ", ";
                    str += it;
                }
            }
            return str;
        }

        string GetCompatibleString(LfgCompatibility compatibles)
        {
            switch (compatibles)
            {
                case LfgCompatibility.Pending:
                    return "Pending";
                case LfgCompatibility.BadStates:
                    return "Compatibles (Bad States)";
                case LfgCompatibility.Match:
                    return "Match";
                case LfgCompatibility.WithLessPlayers:
                    return "Compatibles (Not enough players)";
                case LfgCompatibility.HasIgnores:
                    return "Has ignores";
                case LfgCompatibility.MultipleLfgGroups:
                    return "Multiple Lfg Groups";
                case LfgCompatibility.NoDungeons:
                    return "Incompatible dungeons";
                case LfgCompatibility.NoRoles:
                    return "Incompatible roles";
                case LfgCompatibility.TooMuchPlayers:
                    return "Too much players";
                case LfgCompatibility.WrongGroupSize:
                    return "Wrong group size";
                default:
                    return "Unknown";
            }
        }

        public void AddToQueue(ObjectGuid guid, bool reAdd = false)
        {
            if (!QueueDataStore.ContainsKey(guid))
            {
                Log.outError(LogFilter.Lfg, "AddToQueue: Queue data not found for [{0}]", guid);
                return;
            }

            if (reAdd)
                AddToFrontCurrentQueue(guid);
            else
                AddToNewQueue(guid);
        }

        public void RemoveFromQueue(ObjectGuid guid)
        {
            RemoveFromNewQueue(guid);
            RemoveFromCurrentQueue(guid);
            RemoveFromCompatibles(guid);

            string sguid = guid.ToString();

            var itDelete = QueueDataStore.LastOrDefault().Key;
            foreach (var key in QueueDataStore.Keys.ToList())
            {
                var data = QueueDataStore[key];
                if (key != guid)
                {
                    if (data.bestCompatible.Contains(sguid))
                    {
                        data.bestCompatible = "";
                        FindBestCompatibleInQueue(key, data);
                    }
                }
                else
                    itDelete = key;
            }

            if (!itDelete.IsEmpty())
                QueueDataStore.Remove(itDelete);
        }

        public void AddToNewQueue(ObjectGuid guid)
        {
            newToQueueStore.Add(guid);
        }

        public void RemoveFromNewQueue(ObjectGuid guid)
        {
            newToQueueStore.Remove(guid);
        }

        public void AddToCurrentQueue(ObjectGuid guid)
        {
            currentQueueStore.Add(guid);
        }

        void AddToFrontCurrentQueue(ObjectGuid guid)
        {
            currentQueueStore.Insert(0, guid);
        }

        public void RemoveFromCurrentQueue(ObjectGuid guid)
        {
            currentQueueStore.Remove(guid);
        }

        public void AddQueueData(ObjectGuid guid, long joinTime, List<uint> dungeons, Dictionary<ObjectGuid, LfgRoles> rolesMap)
        {
            QueueDataStore[guid] = new LfgQueueData(joinTime, dungeons, rolesMap);
            AddToQueue(guid);
        }

        public void RemoveQueueData(ObjectGuid guid)
        {
            QueueDataStore.Remove(guid);
        }

        public void UpdateWaitTimeAvg(int waitTime, uint dungeonId)
        {
            LfgWaitTime wt = waitTimesAvgStore[dungeonId];
            uint old_number = wt.number++;
            wt.time = (int)((wt.time * old_number + waitTime) / wt.number);
        }

        public void UpdateWaitTimeTank(int waitTime, uint dungeonId)
        {
            LfgWaitTime wt = waitTimesTankStore[dungeonId];
            uint old_number = wt.number++;
            wt.time = (int)((wt.time * old_number + waitTime) / wt.number);
        }

        public void UpdateWaitTimeHealer(int waitTime, uint dungeonId)
        {
            LfgWaitTime wt = waitTimesHealerStore[dungeonId];
            uint old_number = wt.number++;
            wt.time = (int)((wt.time * old_number + waitTime) / wt.number);
        }

        public void UpdateWaitTimeDps(int waitTime, uint dungeonId)
        {
            LfgWaitTime wt = waitTimesDpsStore[dungeonId];
            uint old_number = wt.number++;
            wt.time = (int)((wt.time * old_number + waitTime) / wt.number);
        }

        void RemoveFromCompatibles(ObjectGuid guid)
        {
            string strGuid = guid.ToString();

            Log.outDebug(LogFilter.Lfg, "RemoveFromCompatibles: Removing [{0}]", guid);
            foreach (var itNext in CompatibleMapStore.ToList())
            {
                if (itNext.Key.Contains(strGuid))
                    CompatibleMapStore.Remove(itNext.Key);
            }
        }

        void SetCompatibles(string key, LfgCompatibility compatibles)
        {
            if (!CompatibleMapStore.ContainsKey(key))
                CompatibleMapStore[key] = new LfgCompatibilityData();

            CompatibleMapStore[key].compatibility = compatibles;
        }

        void SetCompatibilityData(string key, LfgCompatibilityData data)
        {
            CompatibleMapStore[key] = data;
        }

        LfgCompatibility GetCompatibles(string key)
        {
            var compatibilityData = CompatibleMapStore.LookupByKey(key);
            if (compatibilityData != null)
                return compatibilityData.compatibility;

            return LfgCompatibility.Pending;
        }

        LfgCompatibilityData GetCompatibilityData(string key)
        {
            var compatibilityData = CompatibleMapStore.LookupByKey(key);
            if (compatibilityData != null)
                return compatibilityData;

            return null;
        }

        public byte FindGroups()
        {
            byte proposals = 0;
            List<ObjectGuid> firstNew = new List<ObjectGuid>();
            while (!newToQueueStore.Empty())
            {
                ObjectGuid frontguid = newToQueueStore.First();
                Log.outDebug(LogFilter.Lfg, "FindGroups: checking [{0}] newToQueue({1}), currentQueue({2})", frontguid, newToQueueStore.Count, currentQueueStore.Count);
                firstNew.Clear();
                firstNew.Add(frontguid);
                RemoveFromNewQueue(frontguid);

                List<ObjectGuid> temporalList = new List<ObjectGuid>(currentQueueStore);
                LfgCompatibility compatibles = FindNewGroups(firstNew, temporalList);

                if (compatibles == LfgCompatibility.Match)
                    ++proposals;
                else
                    AddToCurrentQueue(frontguid);                  // Lfg group not found, add this group to the queue.
            }
            return proposals;
        }

        LfgCompatibility FindNewGroups(List<ObjectGuid> check, List<ObjectGuid> all)
        {
            string strGuids = ConcatenateGuids(check);
            LfgCompatibility compatibles = GetCompatibles(strGuids);

            Log.outDebug(LogFilter.Lfg, "FindNewGroup: ({0}): {1} - all({2})", strGuids, GetCompatibleString(compatibles), ConcatenateGuids(all));
            if (compatibles == LfgCompatibility.Pending) // Not previously cached, calculate
                compatibles = CheckCompatibility(check);

            if (compatibles == LfgCompatibility.BadStates && Global.LFGMgr.AllQueued(check))
            {
                Log.outDebug(LogFilter.Lfg, "FindNewGroup: ({0}) compatibles (cached) changed from bad states to match", strGuids);
                SetCompatibles(strGuids, LfgCompatibility.Match);
                return LfgCompatibility.Match;
            }

            if (compatibles != LfgCompatibility.WithLessPlayers)
                return compatibles;

            // Try to match with queued groups
            while (!all.Empty())
            {
                check.Add(all.First());
                all.RemoveAt(0);
                LfgCompatibility subcompatibility = FindNewGroups(check, all);
                if (subcompatibility == LfgCompatibility.Match)
                    return LfgCompatibility.Match;
                check.RemoveAt(check.Count - 1);
            }
            return compatibles;
        }

        LfgCompatibility CheckCompatibility(List<ObjectGuid> check)
        {
            string strGuids = ConcatenateGuids(check);
            LfgProposal proposal = new LfgProposal();
            List<uint> proposalDungeons;
            Dictionary<ObjectGuid, ObjectGuid> proposalGroups = new Dictionary<ObjectGuid, ObjectGuid>();
            Dictionary<ObjectGuid, LfgRoles> proposalRoles = new Dictionary<ObjectGuid, LfgRoles>();

            // Check for correct size
            if (check.Count > MapConst.MaxGroupSize || check.Empty())
            {
                Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}): Size wrong - Not compatibles", strGuids);
                return LfgCompatibility.WrongGroupSize;
            }

            // Check all-but-new compatiblitity
            if (check.Count > 2)
            {
                ObjectGuid frontGuid = check.First();
                check.RemoveAt(0);

                // Check all-but-new compatibilities (New, A, B, C, D) -. check(A, B, C, D)
                LfgCompatibility child_compatibles = CheckCompatibility(check);
                if (child_compatibles < LfgCompatibility.WithLessPlayers) // Group not compatible
                {
                    Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) child {1} not compatibles", strGuids, ConcatenateGuids(check));
                    SetCompatibles(strGuids, child_compatibles);
                    return child_compatibles;
                }
                check.Insert(0, frontGuid);
            }

            // Check if more than one LFG group and number of players joining
            byte numPlayers = 0;
            byte numLfgGroups = 0;
            foreach (var guid in check)
            {
                if (!(numLfgGroups < 2) && !(numPlayers <= MapConst.MaxGroupSize))
                    break; ;

                var itQueue = QueueDataStore.LookupByKey(guid);
                if (itQueue == null)
                {
                    Log.outError(LogFilter.Lfg, "CheckCompatibility: [{0}] is not queued but listed as queued!", guid);
                    RemoveFromQueue(guid);
                    return LfgCompatibility.Pending;
                }

                // Store group so we don't need to call Mgr to get it later (if it's player group will be 0 otherwise would have joined as group)
                foreach (var it2 in itQueue.roles)
                    proposalGroups[it2.Key] = guid.IsPlayer() ? guid : ObjectGuid.Empty;

                numPlayers += (byte)itQueue.roles.Count;

                if (Global.LFGMgr.IsLfgGroup(guid))
                {
                    if (numLfgGroups == 0)
                        proposal.group = guid;
                    ++numLfgGroups;
                }
            }

            // Group with less that MAXGROUPSIZE members always compatible
            if (check.Count == 1 && numPlayers != MapConst.MaxGroupSize)
            {
                Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) sigle group. Compatibles", strGuids);
                var guid = check.First();
                var itQueue = QueueDataStore.LookupByKey(guid);

                LfgCompatibilityData data = new LfgCompatibilityData(LfgCompatibility.WithLessPlayers);
                data.roles = itQueue.roles;
                Global.LFGMgr.CheckGroupRoles(data.roles);

                UpdateBestCompatibleInQueue(guid, itQueue, strGuids, data.roles);
                SetCompatibilityData(strGuids, data);
                return LfgCompatibility.WithLessPlayers;
            }

            if (numLfgGroups > 1)
            {
                Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) More than one Lfggroup ({1})", strGuids, numLfgGroups);
                SetCompatibles(strGuids, LfgCompatibility.MultipleLfgGroups);
                return LfgCompatibility.MultipleLfgGroups;
            }

            if (numPlayers > MapConst.MaxGroupSize)
            {
                Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) Too much players ({1})", strGuids, numPlayers);
                SetCompatibles(strGuids, LfgCompatibility.TooMuchPlayers);
                return LfgCompatibility.TooMuchPlayers;
            }

            // If it's single group no need to check for duplicate players, ignores, bad roles or bad dungeons as it's been checked before joining
            if (check.Count > 1)
            {
                foreach (var it in check)
                {
                    Dictionary<ObjectGuid, LfgRoles> roles = QueueDataStore[it].roles;
                    foreach (var rolePair in roles)
                    {
                        KeyValuePair<ObjectGuid, LfgRoles> itPlayer = new KeyValuePair<ObjectGuid, LfgRoles>();
                        foreach (var _player in proposalRoles)
                        {
                            itPlayer = _player;
                            if (rolePair.Key == itPlayer.Key)
                                Log.outError(LogFilter.Lfg, "CheckCompatibility: ERROR! Player multiple times in queue! [{0}]", rolePair.Key);
                            else if (Global.LFGMgr.HasIgnore(rolePair.Key, itPlayer.Key))
                                break;
                        }
                        if (itPlayer.Key == proposalRoles.LastOrDefault().Key)
                            proposalRoles[rolePair.Key] = rolePair.Value;
                    }
                }

                byte playersize = (byte)(numPlayers - proposalRoles.Count);
                if (playersize != 0)
                {
                    Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) not compatible, {1} players are ignoring each other", strGuids, playersize);
                    SetCompatibles(strGuids, LfgCompatibility.HasIgnores);
                    return LfgCompatibility.HasIgnores;
                }
                StringBuilder o;
                Dictionary<ObjectGuid, LfgRoles> debugRoles = proposalRoles;
                if (!Global.LFGMgr.CheckGroupRoles(proposalRoles))
                {
                    o = new StringBuilder();
                    foreach (var it in debugRoles)
                        o.AppendFormat(", {0}: {1}", it.Key, GetRolesString(it.Value));

                    Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) Roles not compatible{1}", strGuids, o.ToString());
                    SetCompatibles(strGuids, LfgCompatibility.NoRoles);
                    return LfgCompatibility.NoRoles;
                }

                var itguid = check.First();
                proposalDungeons = QueueDataStore[itguid].dungeons;
                o = new StringBuilder();
                o.AppendFormat(", {0}: ({1})", itguid, Global.LFGMgr.ConcatenateDungeons(proposalDungeons));
                foreach (var guid in check)
                {
                    if (guid == itguid)
                        continue;

                    List<uint> temporal;
                    List<uint> dungeons = QueueDataStore[itguid].dungeons;
                    o.AppendFormat(", {0}: ({1})", guid, Global.LFGMgr.ConcatenateDungeons(dungeons));
                    temporal = proposalDungeons.Intersect(dungeons).ToList();
                    proposalDungeons = temporal;
                }

                if (proposalDungeons.Empty())
                {
                    Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) No compatible dungeons{1}", strGuids, o.ToString());
                    SetCompatibles(strGuids, LfgCompatibility.NoDungeons);
                    return LfgCompatibility.NoDungeons;
                }
            }
            else
            {
                ObjectGuid gguid = check.First();
                LfgQueueData queue = QueueDataStore[gguid];
                proposalDungeons = queue.dungeons;
                proposalRoles = queue.roles;
                Global.LFGMgr.CheckGroupRoles(proposalRoles);          // assing new roles
            }

            // Enough players?
            if (numPlayers != MapConst.MaxGroupSize)
            {
                Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) Compatibles but not enough players({1})", strGuids, numPlayers);
                LfgCompatibilityData data = new LfgCompatibilityData(LfgCompatibility.WithLessPlayers);
                data.roles = proposalRoles;

                foreach (var guid in check)
                {
                    var queueData = QueueDataStore.LookupByKey(guid);
                    UpdateBestCompatibleInQueue(guid, queueData, strGuids, data.roles);
                }

                SetCompatibilityData(strGuids, data);
                return LfgCompatibility.WithLessPlayers;
            }

            ObjectGuid _guid = check.First();
            proposal.queues = check;
            proposal.isNew = numLfgGroups != 1 || Global.LFGMgr.GetOldState(_guid) != LfgState.Dungeon;

            if (!Global.LFGMgr.AllQueued(check))
            {
                Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) Group MATCH but can't create proposal!", strGuids);
                SetCompatibles(strGuids, LfgCompatibility.BadStates);
                return LfgCompatibility.BadStates;
            }

            // Create a new proposal
            proposal.cancelTime = Time.UnixTime + SharedConst.LFGTimeProposal;
            proposal.state = LfgProposalState.Initiating;
            proposal.leader = ObjectGuid.Empty;
            proposal.dungeonId = proposalDungeons.PickRandom();

            bool leader = false;
            foreach (var rolePair in proposalRoles)
            {
                // Assing new leader
                if (rolePair.Value.HasAnyFlag(LfgRoles.Leader))
                {
                    if (!leader || proposal.leader.IsEmpty() || Convert.ToBoolean(RandomHelper.IRand(0, 1)))
                        proposal.leader = rolePair.Key;
                    leader = true;
                }
                else if (!leader && (proposal.leader.IsEmpty() || Convert.ToBoolean(RandomHelper.IRand(0, 1))))
                    proposal.leader = rolePair.Key;

                // Assing player data and roles
                LfgProposalPlayer data = new LfgProposalPlayer();
                data.role = rolePair.Value;
                data.group = proposalGroups.LookupByKey(rolePair.Key);
                if (!proposal.isNew && !data.group.IsEmpty() && data.group == proposal.group) // Player from existing group, autoaccept
                    data.accept = LfgAnswer.Agree;

                proposal.players[rolePair.Key] = data;
            }

            // Mark proposal members as not queued (but not remove queue data)
            foreach (var guid in proposal.queues)
            {
                RemoveFromNewQueue(guid);
                RemoveFromCurrentQueue(guid);
            }

            Global.LFGMgr.AddProposal(proposal);

            Log.outDebug(LogFilter.Lfg, "CheckCompatibility: ({0}) MATCH! Group formed", strGuids);
            SetCompatibles(strGuids, LfgCompatibility.Match);
            return LfgCompatibility.Match;
        }

        public void UpdateQueueTimers(byte queueId, long currTime)
        {
            Log.outDebug(LogFilter.Lfg, "Updating queue timers...");
            foreach (var itQueue in QueueDataStore)
            {
                LfgQueueData queueinfo = itQueue.Value;
                uint dungeonId = queueinfo.dungeons.FirstOrDefault();
                uint queuedTime = (uint)(currTime - queueinfo.joinTime);
                LfgRoles role = LfgRoles.None;
                int waitTime = -1;

                if (!waitTimesTankStore.ContainsKey(dungeonId))
                    waitTimesTankStore[dungeonId] = new LfgWaitTime();
                if (!waitTimesHealerStore.ContainsKey(dungeonId))
                    waitTimesHealerStore[dungeonId] = new LfgWaitTime();
                if (!waitTimesDpsStore.ContainsKey(dungeonId))
                    waitTimesDpsStore[dungeonId] = new LfgWaitTime();
                if (!waitTimesAvgStore.ContainsKey(dungeonId))
                    waitTimesAvgStore[dungeonId] = new LfgWaitTime();

                int wtTank = waitTimesTankStore[dungeonId].time;
                int wtHealer = waitTimesHealerStore[dungeonId].time;
                int wtDps = waitTimesDpsStore[dungeonId].time;
                int wtAvg = waitTimesAvgStore[dungeonId].time;

                foreach (var itPlayer in queueinfo.roles)
                    role |= itPlayer.Value;
                role &= ~LfgRoles.Leader;

                switch (role)
                {
                    case LfgRoles.None:                                // Should not happen - just in case
                        waitTime = -1;
                        break;
                    case LfgRoles.Tank:
                        waitTime = wtTank;
                        break;
                    case LfgRoles.Healer:
                        waitTime = wtHealer;
                        break;
                    case LfgRoles.Damage:
                        waitTime = wtDps;
                        break;
                    default:
                        waitTime = wtAvg;
                        break;
                }

                if (string.IsNullOrEmpty(queueinfo.bestCompatible))
                    FindBestCompatibleInQueue(itQueue.Key, itQueue.Value);

                LfgQueueStatusData queueData = new LfgQueueStatusData(queueId, dungeonId, queueinfo.joinTime, waitTime, wtAvg, wtTank, wtHealer, wtDps, queuedTime, queueinfo.tanks, queueinfo.healers, queueinfo.dps);
                foreach (var itPlayer in queueinfo.roles)
                {
                    ObjectGuid pguid = itPlayer.Key;
                    Global.LFGMgr.SendLfgQueueStatus(pguid, queueData);
                }
            }
        }

        public long GetJoinTime(ObjectGuid guid)
        {
            var queueData = QueueDataStore.LookupByKey(guid);
            if (queueData != null)
                return queueData.joinTime;

            return 0;
        }

        public string DumpQueueInfo()
        {
            uint players = 0;
            uint groups = 0;
            uint playersInGroup = 0;

            for (byte i = 0; i < 2; ++i)
            {
                List<ObjectGuid> queue = i != 0 ? newToQueueStore : currentQueueStore;
                foreach (var guid in queue)
                {
                    if (guid.IsParty())
                    {
                        groups++;
                        playersInGroup += Global.LFGMgr.GetPlayerCount(guid);
                    }
                    else
                        players++;
                }
            }

            return string.Format("Queued Players: {0} (in group: {1}) Groups: {2}\n", players, playersInGroup, groups);
        }

        public string DumpCompatibleInfo(bool full = false)
        {
            string str = "Compatible Map size: " + CompatibleMapStore.Count + "\n";
            if (full)
            {
                foreach (var pair in CompatibleMapStore)
                    str += "(" + pair.Key + "): " + GetCompatibleString(pair.Value.compatibility) + "\n";
            }
            return str;
        }

        void FindBestCompatibleInQueue(ObjectGuid guid, LfgQueueData data)
        {
            Log.outDebug(LogFilter.Lfg, "FindBestCompatibleInQueue: {0}", guid);

            foreach (var pair in CompatibleMapStore)
            {
                if (pair.Value.compatibility == LfgCompatibility.WithLessPlayers && pair.Key.Contains(guid.ToString()))
                    UpdateBestCompatibleInQueue(guid, data, pair.Key, pair.Value.roles);
            }
        }

        public void UpdateBestCompatibleInQueue(ObjectGuid guid, LfgQueueData queueData, string key, Dictionary<ObjectGuid, LfgRoles> roles)
        {
            byte storedSize = (byte)(string.IsNullOrEmpty(queueData.bestCompatible) ? 0 : queueData.bestCompatible.Count(p => p == '|') + 1);

            byte size = (byte)(key.Count(p => p == '|') + 1);

            if (size <= storedSize)
                return;

            Log.outDebug(LogFilter.Lfg, "UpdateBestCompatibleInQueue: Changed ({0}) to ({1}) as best compatible group for {2}",
                queueData.bestCompatible, key, guid);

            queueData.bestCompatible = key;
            queueData.tanks = SharedConst.LFGTanksNeeded;
            queueData.healers = SharedConst.LFGHealersNeeded;
            queueData.dps = SharedConst.LFGDPSNeeded;
            foreach (var it in roles)
            {
                LfgRoles role = it.Value;
                if (role.HasAnyFlag(LfgRoles.Tank))
                    --queueData.tanks;
                else if (role.HasAnyFlag(LfgRoles.Healer))
                    --queueData.healers;
                else
                    --queueData.dps;
            }
        }

        // Queue
        Dictionary<ObjectGuid, LfgQueueData> QueueDataStore = new Dictionary<ObjectGuid, LfgQueueData>();
        Dictionary<string, LfgCompatibilityData> CompatibleMapStore = new Dictionary<string, LfgCompatibilityData>();

        Dictionary<uint, LfgWaitTime> waitTimesAvgStore = new Dictionary<uint, LfgWaitTime>();
        Dictionary<uint, LfgWaitTime> waitTimesTankStore = new Dictionary<uint, LfgWaitTime>();
        Dictionary<uint, LfgWaitTime> waitTimesHealerStore = new Dictionary<uint, LfgWaitTime>();
        Dictionary<uint, LfgWaitTime> waitTimesDpsStore = new Dictionary<uint, LfgWaitTime>();
        List<ObjectGuid> currentQueueStore = new List<ObjectGuid>();
        List<ObjectGuid> newToQueueStore = new List<ObjectGuid>();
    }

    public class LfgCompatibilityData
    {
        public LfgCompatibilityData()
        {
            compatibility = LfgCompatibility.Pending;
        }

        public LfgCompatibilityData(LfgCompatibility _compatibility)
        {
            compatibility = _compatibility;
        }

        public LfgCompatibilityData(LfgCompatibility _compatibility, Dictionary<ObjectGuid, LfgRoles> _roles)
        {
            compatibility = _compatibility;
            roles = _roles;
        }

        public LfgCompatibility compatibility;
        public Dictionary<ObjectGuid, LfgRoles> roles;
    }

    // Stores player or group queue info
    public class LfgQueueData
    {
        public LfgQueueData()
        {
            joinTime = Time.UnixTime;
            tanks = SharedConst.LFGTanksNeeded;
            healers = SharedConst.LFGHealersNeeded;
            dps = SharedConst.LFGDPSNeeded;
        }

        public LfgQueueData(long _joinTime, List<uint> _dungeons, Dictionary<ObjectGuid, LfgRoles> _roles)
        {
            joinTime = _joinTime;
            tanks = SharedConst.LFGTanksNeeded;
            healers = SharedConst.LFGHealersNeeded;
            dps = SharedConst.LFGDPSNeeded;
            dungeons = _dungeons;
            roles = _roles;
        }

        public long joinTime;
        public byte tanks;
        public byte healers;
        public byte dps;
        public List<uint> dungeons;
        public Dictionary<ObjectGuid, LfgRoles> roles;
        public string bestCompatible = "";
    }

    public struct LfgWaitTime
    {
        public int time;
        public uint number;
    }
}
