private List<Track> GetWorkingTracks(Track currentTrack,
            IReadOnlyList<Track> currentPath,
            Track workingTrack,
            ICollection<string> availableTrackDescriptions,
            ICollection<string> excludeTrackDescriptions,
            KeyMixStrategy keyMixStrategy)
        {
            var mixTracks = new List<Track>();
            if (currentTrack.Title == workingTrack.Title)
            {
                mixTracks =
                    GetUnrankedTracks(workingTrack, currentPath, availableTrackDescriptions, excludeTrackDescriptions,
                        keyMixStrategy, false).Take(1).ToList();
            }
            else
            {
                mixTracks.Add(workingTrack);
            }

            return mixTracks;
        }
        public int GetAdjustedKeyMixRank(Track track1, Track track2, KeyMixStrategy keyMixStrategy)
        {
            var keyRank = KeyHelper.GetKeyMixRank(track1.Key, track2.Key);

            if (keyMixStrategy == KeyMixStrategy.Good || keyMixStrategy == KeyMixStrategy.VeryGood)
                return keyRank;
            
            var mixLevel = MixLibrary.GetExtendedMixLevel(track1, track2);
            if (mixLevel >= 5) keyRank += 3;
            else if (mixLevel >= 4) keyRank += 2;
            else if (mixLevel >= 3) keyRank += 1;
            if (keyRank > 5) keyRank = 5;

            return keyRank;
        }
        private List<Track> GetUnrankedTracks(Track currentTrack,
            IReadOnlyList<Track> currentPath,
            ICollection<string> availableTrackDescriptions,
            ICollection<string> excludeTrackDescriptions,
            KeyMixStrategy keyMixStrategy,
            bool toTracksOnly)
        {
            var mixTracks = MixLibrary.GetUnrankedToTracks(currentTrack).ToList();

            if (!toTracksOnly)
                mixTracks.AddRange(MixLibrary.GetUnrankedFromTracks(currentTrack));

            mixTracks =
                mixTracks.Where(t => !excludeTrackDescriptions.Contains(t.Description))
                    .Where(t => availableTrackDescriptions.Contains(t.Description))
                    .Distinct()
                    .ToList();

            FilterByKeyMixStrategy(currentTrack, mixTracks, keyMixStrategy);

            if (mixTracks.Count == 0)
            {
                // find all tracks in the list that have already been mixed into from the current track
                var excludeTracks = new List<Track>();
                for (var i = 1; i < currentPath.Count; i++)
                {
                    if (currentPath[i - 1].Title == currentTrack.Title)
                        excludeTracks.Add(currentPath[i]);
                }

                // get all unranked tracks apart from those ones
                mixTracks =
                    MixLibrary.GetUnrankedToTracks(currentTrack)
                        .Union(MixLibrary.GetUnrankedFromTracks(currentTrack))
                        .Except(excludeTracks)
                        .Where(t => availableTrackDescriptions.Contains(t.Description))
                        .Distinct()
                        .ToList();
            }

            FilterByKeyMixStrategy(currentTrack, mixTracks, keyMixStrategy);

            mixTracks =
                mixTracks.OrderByDescending(t => KeyHelper.GetKeyMixRank(currentTrack.Key, t.Key))
                    .ThenBy(t => t.Rank)
                    .ThenBy(t => BpmHelper.GetAdjustedBpmPercentChange(currentTrack.EndBpm, t.StartBpm))
                    .ToList();

            return mixTracks;
        }
        private void FilterByKeyMixStrategy(Track currentTrack, List<Track> mixTracks, KeyMixStrategy keyMixStrategy)
        {
            if (keyMixStrategy == KeyMixStrategy.Any) return;

            var minimumKeyMixRank = 0;
            if (keyMixStrategy == KeyMixStrategy.VeryGood) minimumKeyMixRank = 4;
            if (keyMixStrategy == KeyMixStrategy.Bearable) minimumKeyMixRank = 2;
            if (keyMixStrategy == KeyMixStrategy.Good || keyMixStrategy == KeyMixStrategy.GoodIfPossible)
                minimumKeyMixRank = 3;
            if (minimumKeyMixRank == 0) return;

            if (keyMixStrategy == KeyMixStrategy.GoodIfPossible)
            {
                var notGoodCount =
                    mixTracks.Count(t => GetAdjustedKeyMixRank(currentTrack, t, keyMixStrategy) < minimumKeyMixRank);
                if (notGoodCount == mixTracks.Count)
                    minimumKeyMixRank = 2;
            }

            mixTracks.RemoveAll(t => GetAdjustedKeyMixRank(currentTrack, t, keyMixStrategy) < minimumKeyMixRank);
        }
        /// <summary>
        ///     Filters the mixable tracks.
        /// </summary>
        /// <param name="currentTrack">The current track.</param>
        /// <param name="currentPath">The current path.</param>
        /// <param name="restrictArtistClumping">If set to true, restrict genre clumping.</param>
        /// <param name="restrictGenreClumping">If set to true, restrict genre clumping.</param>
        /// <param name="restrictTitleClumping">If set to true, restrict title clumping.</param>
        /// <param name="keyMixStrategy">The key mixing strategy.</param>
        /// <param name="mixTracks">The mix tracks.</param>
        private void FilterMixableTracks(Track currentTrack,
            IReadOnlyList<Track> currentPath,
            bool restrictArtistClumping,
            bool restrictGenreClumping,
            bool restrictTitleClumping,
            KeyMixStrategy keyMixStrategy,
            List<Track> mixTracks)
        {
            var cycleTracks = GetCycleHistory(currentPath);

            if (restrictTitleClumping)
                FilterMixableTracksByTitle(mixTracks, cycleTracks);

            if (restrictArtistClumping)
                FilterMixableTracksByArtist(mixTracks, cycleTracks);

            if (restrictGenreClumping)
                FilterMixableTracksByGenre(mixTracks, cycleTracks);

            FilterByKeyMixStrategy(currentTrack, mixTracks, keyMixStrategy);
        }
        /// <summary>
        ///     Gets the best mix tracks for play-list generation.
        /// </summary>
        /// <param name="currentTrack">The current track.</param>
        /// <param name="currentPath">The current path.</param>
        /// <param name="allowBearable">The allow bearable.</param>
        /// <param name="availableTrackDescriptions">The available track descriptions.</param>
        /// <param name="excludeTrackDescriptions">The exclude track descriptions.</param>
        /// <param name="restrictArtistClumping">If set to true, genre clumping is restricted.</param>
        /// <param name="restrictGenreClumping">if set to true&gt; restrict genre clumping.</param>
        /// <param name="restrictTitleClumping">if set to true restrict title clumping.</param>
        /// <param name="keyMixStrategy">The key mix strategy.</param>
        /// <returns>
        ///     A list of filtered mix tracks
        /// </returns>
        private List<Track> GetBestMixTracks(Track currentTrack,
            List<Track> currentPath,
            AllowBearableMixStrategy allowBearable,
            ICollection<string> availableTrackDescriptions,
            ICollection<string> excludeTrackDescriptions,
            bool restrictArtistClumping,
            bool restrictGenreClumping,
            bool restrictTitleClumping,
            KeyMixStrategy keyMixStrategy)
        {
            var mixTracks = MixLibrary
                .GetGoodTracks(currentTrack)
                .Where(t => !excludeTrackDescriptions.Contains(t.Description))
                .Where(t => availableTrackDescriptions.Contains(t.Description))
                .ToList();

            FilterMixableTracks(currentTrack,
                currentPath,
                restrictArtistClumping,
                restrictGenreClumping,
                restrictTitleClumping,
                keyMixStrategy,
                mixTracks);

            var bearableAllowed = IsBearableTrackMixAllowed(currentTrack, currentPath, allowBearable, mixTracks);

            if (!bearableAllowed) mixTracks.RemoveAll(t => t.Rank <= 2);

            if (bearableAllowed)
            {
                var bearableTracks = MixLibrary
                    .GetBearableTracks(currentTrack)
                    .Where(t => !excludeTrackDescriptions.Contains(t.Description))
                    .Where(t => availableTrackDescriptions.Contains(t.Description))
                    .ToList();

                mixTracks.AddRange(bearableTracks);
            }

            FilterMixableTracks(currentTrack,
                currentPath,
                restrictArtistClumping,
                restrictGenreClumping,
                restrictTitleClumping,
                keyMixStrategy,
                mixTracks);

            return mixTracks;
        }
        private void GeneratePaths(Direction direction,
            AllowBearableMixStrategy allowBearable,
            MixStrategy strategy,
            UseExtendedMixes useExtendedMixes,
            IReadOnlyDictionary<string, Dictionary<string, Track>> excludedMixes,
            bool restrictArtistClumping,
            bool restrictGenreClumping,
            bool restrictTitleClumping,
            KeyMixStrategy keyMixStrategy,
            Track workingTrack,
            ICollection<string> availableTrackDescriptions,
            List<TrackPath> nextPaths,
            TrackPath currentPath)
        {
            var currentTrack = currentPath.Tracks.Last();

            DebugHelper.WriteLine("Start GeneratePaths " + currentTrack.Description);

            var excludeTrackDescriptions = GetDistinctTrackDescriptions(currentPath.Tracks);

            List<Track> mixTracks;
            if (strategy == MixStrategy.BestMix || strategy == MixStrategy.Variety ||
                strategy == MixStrategy.ExtraVariety)
            {
                mixTracks = GetBestMixTracks(currentTrack, currentPath.Tracks, allowBearable, availableTrackDescriptions,
                    excludeTrackDescriptions, restrictArtistClumping, restrictGenreClumping, restrictTitleClumping,
                    keyMixStrategy);
            }
            else if (strategy == MixStrategy.Unranked)
            {
                mixTracks = GetUnrankedTracks(currentTrack, currentPath.Tracks, availableTrackDescriptions,
                    excludeTrackDescriptions, keyMixStrategy, true);
            }
            else if (strategy == MixStrategy.Working)
            {
                mixTracks = GetWorkingTracks(currentTrack, currentPath.Tracks, workingTrack, availableTrackDescriptions,
                    excludeTrackDescriptions, keyMixStrategy);
            }
            else
            {
                mixTracks = new List<Track>();
            }

            if (direction != Direction.Any)
            {
                var preferredDirection = direction;
                if (preferredDirection == Direction.Cycle)
                    preferredDirection = GetPreferredDirection(currentTrack, currentPath.Tracks);

                var filteredTracks = FilterTracksByDirection(currentTrack, mixTracks, preferredDirection);
                if (filteredTracks.Count > 0) mixTracks = filteredTracks;
            }

            if ((strategy == MixStrategy.BestMix || strategy == MixStrategy.Variety ||
                 strategy == MixStrategy.ExtraVariety)
                && useExtendedMixes != UseExtendedMixes.Any)
            {
                mixTracks = FilterTracksByExtendedMix(currentTrack, mixTracks, useExtendedMixes);
            }

            if (excludedMixes != null)
            {
                mixTracks = FilterExcludedMixes(currentTrack, mixTracks, excludedMixes);
            }

            if (strategy == MixStrategy.BestMix || strategy == MixStrategy.Variety ||
                strategy == MixStrategy.ExtraVariety)
            {
                mixTracks = mixTracks
                    .OrderByDescending(x => GetAverageTrackAndMixAndKeyRank(currentTrack, x))
                    .ThenBy(x => x.Length)
                    .ToList();

                if (strategy == MixStrategy.Variety)
                {
                    mixTracks = FilterTracksForVariety(currentTrack, mixTracks);
                }
                else if (strategy == MixStrategy.ExtraVariety)
                {
                    mixTracks = FilterTracksForExtraVariety(currentTrack, mixTracks);
                }
            }

            var max = 3*Environment.ProcessorCount;
            mixTracks = mixTracks
                .Take(max)
                .ToList();

            var trackPaths = mixTracks.Select(mixTrack => new TrackPath(mixTrack, currentPath));
            foreach (var newPath in trackPaths)
            {
                lock (nextPaths)
                {
                    nextPaths.Add(newPath);
                    CalculateAverageRankForPath(newPath);
                }

                if (IsGenerationHalted()) break;
            }

            DebugHelper.WriteLine("End GeneratePaths " + currentTrack.Description);
        }
        public List<Track> GeneratePlayList(List<Track> availableTracks,
            MixLibrary mixLibrary,
            List<Track> currentPlaylist,
            Direction direction,
            int approximateLength,
            AllowBearableMixStrategy allowBearable,
            MixStrategy strategy,
            UseExtendedMixes useExtendedMixes,
            Dictionary<string, Dictionary<string, Track>> excludedMixes,
            bool restrictArtistClumping,
            bool restrictGenreClumping,
            bool restrictTitleClumping,
            ContinueMix continueMix,
            KeyMixStrategy keyMixStrategy,
            int maxTracksToAdd)
        {
            if (strategy == MixStrategy.Working && currentPlaylist.Count == 0) return currentPlaylist;

            Track workingTrack = null;
            if (strategy == MixStrategy.Working)
            {
                direction = Direction.Any;
                workingTrack = currentPlaylist.Last();
            }

            GeneratePlayListStatus = "";
            MixLibrary = mixLibrary;

            AvailableTracks = availableTracks;

            if (strategy == MixStrategy.Working) AvailableTracks.RemoveAll(t => MixLibrary.GetMixOutCount(t) == 0);

            if (AvailableTracks.Count == 0) return currentPlaylist;

            var availableTrackDescriptions = GetDistinctTrackDescriptions(AvailableTracks);

            var trackCountLimit = int.MaxValue;
            if (approximateLength > 0 && approximateLength != int.MaxValue)
            {
                var currentLength = currentPlaylist.Sum(cp => cp.Length);
                var requiredLength = Convert.ToInt32((approximateLength*60) - currentLength);

                if (requiredLength <= 0) return new List<Track>();
                var averageLength = AvailableTracks.Average(t => t.Length);

                trackCountLimit = (requiredLength/Convert.ToInt32(averageLength)) + currentPlaylist.Count;

                if (trackCountLimit == currentPlaylist.Count) return new List<Track>();
            }

            if (maxTracksToAdd != int.MaxValue && trackCountLimit > currentPlaylist.Count + maxTracksToAdd)
            {
                trackCountLimit = currentPlaylist.Count + maxTracksToAdd;
            }

            if (strategy != MixStrategy.Unranked && strategy != MixStrategy.Working)
            {
                if (trackCountLimit > AvailableTracks.Count)
                {
                    trackCountLimit = AvailableTracks.Count;
                }
            }

            var initialPlaylistCount = currentPlaylist.Count;

            var currentPaths = new List<TrackPath>();
            if (currentPlaylist.Count == 0)
            {
                var trackPaths = AvailableTracks.Select(track => new TrackPath(track));
                foreach (var path in trackPaths)
                {
                    CalculateAverageRankForPath(path);
                    currentPaths.Add(path);
                }
            }
            else if (continueMix == ContinueMix.No)
            {
                var trackPaths = AvailableTracks
                    .Select(track => new List<Track>(currentPlaylist) {track})
                    .Select(playlist => new TrackPath(playlist));

                foreach (var path in trackPaths)
                {
                    CalculateAverageRankForPath(path);
                    currentPaths.Add(path);
                }
            }
            else
            {
                var path = new TrackPath(currentPlaylist);
                CalculateAverageRankForPath(path);
                currentPaths.Add(path);
            }

            _cancelGeneratePlayList = false;
            _stopGeneratePlayList = false;

            var nextPaths = new List<TrackPath>();
            while (!IsGenerationHalted())
            {
                ParallelHelper.ForEach(currentPaths, currentPath => GeneratePaths(direction,
                    allowBearable,
                    strategy,
                    useExtendedMixes,
                    excludedMixes,
                    restrictArtistClumping,
                    restrictGenreClumping,
                    restrictTitleClumping,
                    keyMixStrategy,
                    workingTrack,
                    availableTrackDescriptions,
                    nextPaths,
                    currentPath));

                if (IsGenerationHalted()) break;

                if (nextPaths.Count == 0) break;

                GeneratePlayListStatus =
                    $"Generated {nextPaths.Count} possible paths for {nextPaths[0].Tracks.Count} of {trackCountLimit} tracks.";

                var max = 50*Environment.ProcessorCount;

                if (nextPaths.Count > max)
                {
                    nextPaths = nextPaths
                        .OrderByDescending(t => t.AverageRank)
                        .Take(max)
                        .ToList();
                }

                currentPaths.Clear();
                currentPaths.AddRange(nextPaths);

                if (nextPaths[0].Tracks.Count >= trackCountLimit) break;
                nextPaths.Clear();
            }

            if (_cancelGeneratePlayList) return currentPlaylist;

            var resultPath = currentPaths
                .OrderByDescending(t => GetAverageTrackAndMixAndKeyRank(t.Tracks))
                .FirstOrDefault();

            if ((strategy == MixStrategy.BestMix || strategy == MixStrategy.Variety ||
                 strategy == MixStrategy.ExtraVariety)
                && resultPath != null
                && resultPath.Tracks.Count < trackCountLimit
                && resultPath.Tracks.Count > 0)
            {
                availableTrackDescriptions = GetDistinctTrackDescriptions(AvailableTracks);
                var excludeTrackDescriptions = GetDistinctTrackDescriptions(resultPath.Tracks);
                var currentTrack = resultPath.Tracks[resultPath.Tracks.Count - 1];
                var nextTrack =
                    GetBestMixTracks(currentTrack, resultPath.Tracks, allowBearable, availableTrackDescriptions,
                        excludeTrackDescriptions, restrictArtistClumping, restrictArtistClumping, restrictTitleClumping,
                        keyMixStrategy)
                        .OrderBy(t => GetAverageTrackAndMixAndKeyRank(currentTrack, t))
                        .FirstOrDefault();

                if (nextTrack != null) resultPath.Tracks.Add(nextTrack);
            }

            var resultTracks = (resultPath != null)
                ? resultPath.Tracks
                : new List<Track>();

            if (continueMix == ContinueMix.IfPossible && resultTracks.Count == initialPlaylistCount)
            {
                return GeneratePlayList(availableTracks,
                    mixLibrary,
                    currentPlaylist,
                    direction,
                    approximateLength,
                    allowBearable,
                    strategy,
                    useExtendedMixes,
                    excludedMixes,
                    restrictArtistClumping,
                    restrictGenreClumping,
                    restrictTitleClumping,
                    ContinueMix.No,
                    keyMixStrategy,
                    maxTracksToAdd);
            }

            return resultTracks;
        }