/**
   * Shuffles the song proposals at weighted random. For every position in the list
   *
   * <pre>
   * P[song choosen] = shifted_vote / votesSum
   * </pre>
   *
   * The votes get shifted to be pure positive and stretched, so that higher ratings are more
   * probable to appear first
   *
   * <pre>
   * shifted_vote = (vote[song] + 1)<sup>STRETCH_FACTOR</sup>
   * </pre>
   *
   * @param votes The song votes
   * @return The shuffled song list
   */
  private List<BaseSong<BaseArtist, BaseAlbum>> getShuffledSongsAtWeightedRandom(
      Map<BaseSong<BaseArtist, BaseAlbum>, Double> votes) {

    final double STRETCH_FACTOR = 4.0f;

    Map<BaseSong<BaseArtist, BaseAlbum>, Double> remainingVotes =
        new HashMap<BaseSong<BaseArtist, BaseAlbum>, Double>(votes);

    // Calculate the total vote sum
    double voteSum = 0.0d;
    for (Map.Entry<BaseSong<BaseArtist, BaseAlbum>, Double> entry : remainingVotes.entrySet()) {
      double adjustedVote =
          Math.pow(entry.getValue() + 1.0d, STRETCH_FACTOR); // to not have negative votes

      remainingVotes.put(entry.getKey(), adjustedVote);
      voteSum += adjustedVote;
    }

    // Shuffle songs
    List<BaseSong<BaseArtist, BaseAlbum>> ret =
        new ArrayList<BaseSong<BaseArtist, BaseAlbum>>(votes.size());
    while (ret.size() < votes.size()) {
      // Search the next weighted random song
      double rand = RandomProvider.getRandom().nextDouble() * voteSum;

      for (Map.Entry<BaseSong<BaseArtist, BaseAlbum>, Double> entry : remainingVotes.entrySet()) {
        if (rand <= entry.getValue()) {
          // Add the song
          ret.add(entry.getKey());

          // Reduce the search space for the next round
          remainingVotes.remove(entry.getKey());
          voteSum -= entry.getValue();

          break;
        } else {
          rand -= entry.getValue();
        }
      }
    }

    return ret;
  }
  /**
   * Returns the set of songs which are proposed by the agents.
   *
   * @param proposalTimes
   * @return The songs
   */
  private List<BaseSong<BaseArtist, BaseAlbum>> getProposedSongs(AgentsTiming proposalTimes) {
    Set<IAgent> agents = agentManager.getAgents();

    // Using set here to ensure no duplicate entries
    Set<BaseSong<BaseArtist, BaseAlbum>> proposedSongs =
        new HashSet<BaseSong<BaseArtist, BaseAlbum>>(SONG_PROPOSAL_USAGE_COUNT * agents.size());
    for (IAgent agent : agents) {
      throwIfAborted();

      StopWatch stopWatch = StopWatch.start();

      List<BaseSong<BaseArtist, BaseAlbum>> agentProposals =
          new ArrayList<BaseSong<BaseArtist, BaseAlbum>>(agent.suggestSongs(SONG_PROPOSAL_COUNT));
      // Get #SONG_PROPOSAL_USAGE_COUNT items of them at random
      while (agentProposals.size() > SONG_PROPOSAL_USAGE_COUNT) {
        agentProposals.remove(RandomProvider.getRandom().nextInt(agentProposals.size()));
      }
      proposedSongs.addAll(agentProposals);

      proposalTimes.addAgentTiming(agent, stopWatch.stop());
    }

    return new ArrayList<BaseSong<BaseArtist, BaseAlbum>>(proposedSongs);
  }