/**
   * 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);
    }
  }
  @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() !!
      }
    }
  }