/**
   * Adds songs which did not get any rating data assigned for a long time (or never) to the
   * {proposedSongs} list. This ensures reaching regions in the music space which are not explored
   * yet (or have been forgotten about).<br>
   * The agents vote for these songs is set to {@value #LONG_NOT_RATED_SONG_VOTE}.
   *
   * @param proposedSongs The proposal list
   * @param agentVotes The votes list
   */
  private void addLongTimeNotListenedSongProposals() {

    // Fetch long not rated songs
    List<BaseSong<BaseArtist, BaseAlbum>> longNotRatedSongs;
    try {
      longNotRatedSongs =
          statisticsProvider.getLongNotRatedSongs(
              LONG_NOT_RATED_SONG_COUNT, LONG_NOT_RATED_THRESHOLD);
    } catch (DataUnavailableException e) {
      // Just ignore the warning and do not add any songs
      Log.w(getTag(), e);
      longNotRatedSongs = new LinkedList<BaseSong<BaseArtist, BaseAlbum>>();
    }

    // Prepare agent votes for songs
    Map<IAgent, Float> agentVotesForProposedSongs = new HashMap<IAgent, Float>();
    for (IAgent agent : agentManager.getAgents()) {
      agentVotesForProposedSongs.put(agent, (float) LONG_NOT_RATED_SONG_VOTE);
    }

    // Add long not played songs as proposals
    for (BaseSong<BaseArtist, BaseAlbum> song : longNotRatedSongs) {
      proposedSongs.add(song);
      songVotes.put(song, LONG_NOT_RATED_SONG_VOTE);
      agentVotesBySong.put(song, agentVotesForProposedSongs);
    }
  }
  /**
   * 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);
  }
  /**
   * Returns the average votes of the agents for the given songs. The votes gets weighted by the
   * importance of the agents.
   *
   * @param proposedSongs The proposed songs
   * @param agentsVotes (out) The votes for all the songs by agent
   * @param voteTimes
   * @return The weighted mean votes for the songs
   */
  private Map<BaseSong<BaseArtist, BaseAlbum>, Double> getVotesForProposed(
      List<BaseSong<BaseArtist, BaseAlbum>> proposedSongs,
      Map<IAgent, List<SongVote>> agentsVotes,
      AgentsTiming voteTimes) {

    Set<IAgent> agents = agentManager.getAgents();

    // Init the ratings list & the weight sum table
    Map<BaseSong<BaseArtist, BaseAlbum>, Double> votes =
        new HashMap<BaseSong<BaseArtist, BaseAlbum>, Double>();
    Map<BaseSong<BaseArtist, BaseAlbum>, Double> weightSums =
        new HashMap<BaseSong<BaseArtist, BaseAlbum>, Double>(agents.size());
    for (BaseSong<BaseArtist, BaseAlbum> song : proposedSongs) {
      votes.put(song, 0.0d);
      weightSums.put(song, 0.0d);
    }

    // Calculate the song ratings
    for (IAgent agent : agents) {
      throwIfAborted();

      // Get the agent vote
      List<SongVote> agentVotes;

      StopWatch stopWatch = StopWatch.start();
      {
        agentVotes = agent.vote(Collections.unmodifiableList(proposedSongs));
      }
      voteTimes.addAgentTiming(agent, stopWatch.stop());

      // Fill up the list to ensure that a vote for all songs exist
      List<BaseSong<BaseArtist, BaseAlbum>> noVote =
          new ArrayList<BaseSong<BaseArtist, BaseAlbum>>(proposedSongs);
      for (int i = 0; i < agentVotes.size(); ) {
        final SongVote vote = agentVotes.get(i);
        int idx = noVote.indexOf(vote.getSong());
        if (idx >= 0) {
          noVote.remove(idx);
          ++i;
        } else {
          // No vote for this song is required
          agentVotes.remove(i);
        }
      }
      for (BaseSong<BaseArtist, BaseAlbum> song : noVote) {
        agentVotes.add(new SongVote(song, 0.0f));
      }
      agentsVotes.put(agent, agentVotes);

      for (SongVote vote : agentVotes) {
        final BaseSong<BaseArtist, BaseAlbum> song = vote.getSong();

        double oldRating = votes.get(song);
        double agentWeight = getAgentWeights().get(agent);
        double oldW = weightSums.get(song);
        double newW = oldW + agentWeight;
        double newRating =
            (oldRating * oldW + vote.getVote() * agentWeight)
                / newW; // Continuous, weighted mean calculation
        votes.put(song, newRating);
        weightSums.put(song, newW);
      }
    }

    return votes;
  }
  @Override
  public void run() {
    try {
      throwIfAborted();

      TimingLogger timingLogger = new TimingLogger(getTag(), "next");

      // Begin an immediate transaction
      dbDataPortal.beginTransaction();
      timingLogger.addSplit("Transaction start");

      Set<IAgent> agents = agentManager.getAgents();
      agentWeights = agentManager.getAgentWeights();

      // Enter a fake rating entry to calculate the next song on the predicted future
      Integer meSongId = null;
      if (currentSong != null) {
        double fractionPlayed = (getCalculationCase() == Case.Positive) ? 0.66d : 0.33d;
        playLog.writeToPlayLog(
            new Date(),
            new PlaylistSong<BaseArtist, BaseAlbum>(currentSong, SongSource.SMART_SHUFFLE),
            true,
            (int) (currentSong.getDuration() * fractionPlayed));

        try {
          meSongId = otherDataProvider.getMusicExplorerSongId(currentSong);
        } catch (DataUnavailableException e) {
          Log.w(getTag(), e);
        }
      }
      timingLogger.addSplit("Rating entry");

      // Get the song proposals
      AgentsTiming proposalTimes = new AgentsTiming();
      {
        proposedSongs = getProposedSongs(proposalTimes);
      }
      timingLogger.addSplit("Proposals");

      // Get the agent votes for the proposals
      Map<IAgent, List<SongVote>> agentVotes;
      AgentsTiming voteTimes = new AgentsTiming();
      {
        agentVotes = new HashMap<IAgent, List<SongVote>>();
        songVotes = getVotesForProposed(proposedSongs, agentVotes, voteTimes);
      }
      timingLogger.addSplit("Votes");

      // Fill the song->agentVotes map
      agentVotesBySong =
          new HashMap<BaseSong<BaseArtist, BaseAlbum>, Map<IAgent, Float>>(proposedSongs.size());
      for (BaseSong<BaseArtist, BaseAlbum> song : proposedSongs) {
        agentVotesBySong.put(song, new HashMap<IAgent, Float>(agents.size()));
      }
      for (Map.Entry<IAgent, List<SongVote>> entry : agentVotes.entrySet()) {
        for (SongVote vote : entry.getValue()) {
          Map<IAgent, Float> agentVote = agentVotesBySong.get(vote.getSong());
          agentVote.put(entry.getKey(), vote.getVote());
        }
      }

      // Add long time not listened songs
      addLongTimeNotListenedSongProposals();

      /*// Order the next songs by rating
      Collections.sort(proposedSongs, new Comparator<BaseSong<BaseArtist, BaseAlbum>>() {

      	@Override
      	public int compare(BaseSong<BaseArtist, BaseAlbum> left, BaseSong<BaseArtist, BaseAlbum> right) {
      		Double leftVote = songVotes.get(left);
      		Double rightVote = songVotes.get(right);

      		return rightVote.compareTo(leftVote);
      	}
      });*/

      // Randomize the song proposals by their vote
      proposedSongs = getShuffledSongsAtWeightedRandom(songVotes);

      // FIXME @sämy: this is only for debugging
      StringBuffer proposalsSb = new StringBuffer();
      proposalsSb.append("song ident:overall vote");
      for (IAgent agent : agents) {
        proposalsSb.append(':');
        proposalsSb.append(agent.getIdentifier());
        proposalsSb.append(String.format(" (%.2f)", agentWeights.get(agent)));
      }
      proposalsSb.append('|');
      for (BaseSong<BaseArtist, BaseAlbum> song : proposedSongs) {
        proposalsSb.append(song); // song ident
        proposalsSb.append(':');
        proposalsSb.append(String.format("%.3f", songVotes.get(song))); // overall vote

        Map<IAgent, Float> agentVotes2 = agentVotesBySong.get(song);
        for (IAgent agent : agents) {
          proposalsSb.append(String.format(":%.3f", agentVotes2.get(agent)));
        }
        proposalsSb.append('|');
      }
      Log.d(getTag(), "Proposals: " + proposalsSb.toString());
      // end debug only

      // never ever call dbDataPortal.setTransactionSucessful() !!
      dbDataPortal
          .endTransaction(); // ROLLBACK the transaction. Things below that line get written
                             // persistently to the db!

      // Write a log entry about this run
      NextSongCalculationLogEntry.Builder log =
          NextSongCalculationLogEntry.createInstance()
              .setPredictionCase(getCalculationCase())
              .setCurrentSong((meSongId != null) ? meSongId : 0)
              .setProposalTimes(proposalTimes)
              .setVoteTimes(voteTimes);
      logManager.addLogEntry(log.build());

      // Write the time used in the different parts to the debug log
      timingLogger.dumpToLog();
    } catch (AbortException e) {
      // Just ignore it, we want to land here
    } finally {
      if (dbDataPortal.inTransaction()) {
        dbDataPortal.endTransaction(); // never ever call dbDataPortal.setTransactionSucessful() !!
      }
    }
  }