/// <summary>
        /// Scroll TableView to proper row, fire events.
        /// </summary>
        /// <param name="table"></param>
        /// <param name="levelID"></param>
        public void SelectAndScrollToLevel(string levelID)
        {
            Logger.Debug("Scrolling to LevelID: {0}", levelID);

            // Check once per load
            if (!_checkedForTwitchPlugin)
            {
                Logger.Info("Checking for BeatSaber Twitch Integration Plugin...");
                _detectedTwitchPluginQueue = Resources.FindObjectsOfTypeAll <HMUI.ViewController>().Any(x => x.name == "RequestInfo");
                Logger.Info("BeatSaber Twitch Integration plugin detected: " + _detectedTwitchPluginQueue);

                _checkedForTwitchPlugin = true;
            }

            // Skip scrolling to level if twitch plugin has queue active.
            if (_detectedTwitchPluginQueue)
            {
                Logger.Debug("Skipping SelectAndScrollToLevel() because we detected Twitch Integration Plugin has a Queue active...");
                return;
            }

            // try to find the index and scroll to it
            int selectedIndex = 0;
            List <IPreviewBeatmapLevel> levels = GetCurrentLevelCollectionLevels().ToList();

            if (levels.Count <= 0)
            {
                return;
            }

            // acquire the index or try the last row
            selectedIndex = levels.FindIndex(x => x.levelID == levelID);
            if (selectedIndex < 0)
            {
                // this might look like an off by one error but the _level list we keep is missing the header entry BeatSaber.
                // so the last row is +1 the max index, the count.
                int maxCount = levels.Count;

                int selectedRow = LevelCollectionTableView.GetPrivateField <int>("_selectedRow");

                Logger.Debug("Song is not in the level pack, cannot scroll to it...  Using last known row {0}/{1}", selectedRow, maxCount);
                selectedIndex = Math.Min(maxCount, selectedRow);
            }
            else if (LevelCollectionViewController.GetPrivateField <bool>("_showHeader"))
            {
                // the header counts as an index, so if the index came from the level array we have to add 1.
                selectedIndex += 1;
            }

            ScrollToLevelByRow(selectedIndex);
        }
        /// <summary>
        /// Sort the song list based on the settings.
        /// </summary>
        public void ProcessSongList(IBeatmapLevelPack selectedLevelPack, LevelCollectionViewController levelCollectionViewController, LevelSelectionNavigationController navController)
        {
            Logger.Trace("ProcessSongList()");

            List <IPreviewBeatmapLevel> unsortedSongs = null;
            List <IPreviewBeatmapLevel> filteredSongs = null;
            List <IPreviewBeatmapLevel> sortedSongs   = null;

            // Abort
            if (selectedLevelPack == null)
            {
                Logger.Debug("Cannot process songs yet, no level pack selected...");
                return;
            }

            Logger.Debug("Using songs from level pack: {0}", selectedLevelPack.packID);
            unsortedSongs = selectedLevelPack.beatmapLevelCollection.beatmapLevels.ToList();

            // filter
            Logger.Debug($"Starting filtering songs by {_settings.filterMode}");
            Stopwatch stopwatch = Stopwatch.StartNew();

            switch (_settings.filterMode)
            {
            case SongFilterMode.Favorites:
                filteredSongs = FilterFavorites(unsortedSongs);
                break;

            case SongFilterMode.Search:
                filteredSongs = FilterSearch(unsortedSongs);
                break;

            case SongFilterMode.Ranked:
                filteredSongs = FilterRanked(unsortedSongs, true, false);
                break;

            case SongFilterMode.Unranked:
                filteredSongs = FilterRanked(unsortedSongs, false, true);
                break;

            case SongFilterMode.Custom:
                Logger.Info("Song filter mode set to custom. Deferring filter behaviour to another mod.");
                filteredSongs = CustomFilterHandler != null?CustomFilterHandler.Invoke(selectedLevelPack) : unsortedSongs;

                break;

            case SongFilterMode.None:
            default:
                Logger.Info("No song filter selected...");
                filteredSongs = unsortedSongs;
                break;
            }

            stopwatch.Stop();
            Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);

            // sort
            Logger.Debug("Starting to sort songs...");
            stopwatch = Stopwatch.StartNew();

            SortWasMissingData = false;

            switch (_settings.sortMode)
            {
            case SongSortMode.Original:
                sortedSongs = SortOriginal(filteredSongs);
                break;

            case SongSortMode.Newest:
                sortedSongs = SortNewest(filteredSongs);
                break;

            case SongSortMode.Author:
                sortedSongs = SortAuthor(filteredSongs);
                break;

            case SongSortMode.UpVotes:
                sortedSongs = SortUpVotes(filteredSongs);
                break;

            case SongSortMode.PlayCount:
                sortedSongs = SortBeatSaverPlayCount(filteredSongs);
                break;

            case SongSortMode.Rating:
                sortedSongs = SortBeatSaverRating(filteredSongs);
                break;

            case SongSortMode.Heat:
                sortedSongs = SortBeatSaverHeat(filteredSongs);
                break;

            case SongSortMode.YourPlayCount:
                sortedSongs = SortPlayCount(filteredSongs);
                break;

            case SongSortMode.PP:
                sortedSongs = SortPerformancePoints(filteredSongs);
                break;

            case SongSortMode.Stars:
                sortedSongs = SortStars(filteredSongs);
                break;

            case SongSortMode.Random:
                sortedSongs = SortRandom(filteredSongs);
                break;

            case SongSortMode.Default:
            default:
                sortedSongs = SortSongName(filteredSongs);
                break;
            }

            if (this.Settings.invertSortResults && _settings.sortMode != SongSortMode.Random)
            {
                sortedSongs.Reverse();
            }

            stopwatch.Stop();
            Logger.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);

            // Asterisk the pack name so it is identifable as filtered.
            var packName = selectedLevelPack.packName;

            if (!packName.EndsWith("*") && _settings.filterMode != SongFilterMode.None)
            {
                packName += "*";
            }
            BeatmapLevelPack levelPack = new BeatmapLevelPack(SongBrowserModel.FilteredSongsPackId, packName, selectedLevelPack.shortPackName, selectedLevelPack.coverImage, new BeatmapLevelCollection(sortedSongs.ToArray()));

            GameObject _noDataGO = levelCollectionViewController.GetPrivateField <GameObject>("_noDataInfoGO");
            //string _headerText = tableView.GetPrivateField<string>("_headerText");
            //Sprite _headerSprite = tableView.GetPrivateField<Sprite>("_headerSprite");

            bool _showPlayerStatsInDetailView    = navController.GetPrivateField <bool>("_showPlayerStatsInDetailView");
            bool _showPracticeButtonInDetailView = navController.GetPrivateField <bool>("_showPracticeButtonInDetailView");

            navController.SetData(levelPack, true, _showPlayerStatsInDetailView, _showPracticeButtonInDetailView, _noDataGO);

            //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
        }