/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2013, Enno Gottschalk <mrmaffen@googlemail.com>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Tomahawk is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
package org.tomahawk.libtomahawk.collection;
import org.tomahawk.libtomahawk.resolver.Query;
import org.tomahawk.tomahawk_android.utils.IdGenerator;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;
/**
* A {@link Playlist} is a {@link org.tomahawk.libtomahawk.collection.Playlist} created by the user
* and stored in the database
*/
public class Playlist extends Cacheable implements AlphaComparable {
private static final String TAG = Playlist.class.getSimpleName();
private String mName = "";
private CollectionCursor<PlaylistEntry> mCursor = null;
private List<PlaylistEntry> mAddedEntries = new ArrayList<>();
private Map<PlaylistEntry, Index> mCachedEntries = new HashMap<>();
private List<Index> mIndex = new ArrayList<>();
private List<Index> mShuffledIndex = new ArrayList<>();
private static class Index {
protected Index(int internalIndex, boolean fromMergedItems) {
this.internalIndex = internalIndex;
this.fromMergedItems = fromMergedItems;
}
int internalIndex;
boolean fromMergedItems;
}
private String mId;
private String mHatchetId;
private String mCurrentRevision = "";
private String[] mTopArtistNames;
private long mCount = -1;
private boolean mIsFilled;
private String mUserId;
/**
* Construct a new empty {@link Playlist}.
*/
protected Playlist(String id) {
super(Playlist.class, id);
mId = id;
}
public Playlist copy(Playlist destination) {
destination.mName = mName;
destination.mCursor = mCursor.copy();
for (PlaylistEntry entry : mAddedEntries) {
destination.mAddedEntries.add(entry);
}
for (PlaylistEntry key : mCachedEntries.keySet()) {
destination.mCachedEntries.put(key, mCachedEntries.get(key));
}
for (Index index : mIndex) {
destination.mIndex.add(index);
}
for (Index index : mShuffledIndex) {
destination.mShuffledIndex.add(index);
}
destination.mHatchetId = mHatchetId;
destination.mCurrentRevision = mCurrentRevision;
if (mTopArtistNames != null) {
destination.mTopArtistNames = mTopArtistNames.clone();
} else {
destination.mTopArtistNames = null;
}
destination.mCount = mCount;
destination.mIsFilled = mIsFilled;
destination.mUserId = mUserId;
return destination;
}
/**
* Returns the {@link Playlist} with the given parameters. If none exists in our static {@link
* ConcurrentHashMap} yet, construct and add it.
*
* @return {@link Playlist} with the given parameters
*/
public static Playlist get(String id) {
Cacheable cacheable = get(Playlist.class, id);
return cacheable != null ? (Playlist) cacheable : new Playlist(id);
}
/**
* Create a {@link Playlist} from a list of {@link PlaylistEntry}s.
*
* @return a reference to the constructed {@link Playlist}
*/
public static Playlist fromEntryList(String id, String name, String currentRevision,
List<PlaylistEntry> entries) {
CollectionCursor<PlaylistEntry> cursor =
new CollectionCursor<>(entries, PlaylistEntry.class);
return fromCursor(id, name, currentRevision, cursor);
}
/**
* Create a {@link Playlist} from a list of {@link PlaylistEntry}s.
*
* @return a reference to the constructed {@link Playlist}
*/
public static Playlist fromEmptyList(String id, String name) {
CollectionCursor<PlaylistEntry> cursor =
new CollectionCursor<>(new ArrayList<PlaylistEntry>(), PlaylistEntry.class);
return fromCursor(id, name, null, cursor);
}
/**
* Create a {@link Playlist} from a list of {@link org.tomahawk.libtomahawk.resolver.Query}s.
*
* @return a reference to the constructed {@link Playlist}
*/
public static Playlist fromQueryList(String id, String name, String currentRevision,
List<Query> queries) {
List<PlaylistEntry> entries = new ArrayList<>();
for (Query query : queries) {
entries.add(PlaylistEntry.get(id, query,
IdGenerator.getLifetimeUniqueStringId()));
}
CollectionCursor<PlaylistEntry> cursor =
new CollectionCursor<>(entries, PlaylistEntry.class);
return fromCursor(id, name, currentRevision, cursor);
}
/**
* Create a {@link Playlist} from a {@link CollectionCursor} containing {@link PlaylistEntry}s.
*
* @return a reference to the constructed {@link Playlist}
*/
private static Playlist fromCursor(String id, String name, String currentRevision,
CollectionCursor<PlaylistEntry> cursor) {
Playlist pl = Playlist.get(id);
pl.setName(name);
pl.setCurrentRevision(currentRevision);
pl.setCursor(cursor);
return pl;
}
public void setCursor(CollectionCursor<PlaylistEntry> cursor) {
mCursor = cursor;
initIndex();
}
private void initIndex() {
mAddedEntries.clear();
mCachedEntries.clear();
mIndex.clear();
mShuffledIndex.clear();
for (int i = 0; i < mCursor.size(); i++) {
mIndex.add(new Index(i, false));
}
}
/**
* Get the {@link org.tomahawk.libtomahawk.collection.Playlist} by providing its cache key. Only
* use this for playlists that are not stored in the database!
*/
public static Playlist getByKey(String id) {
return (Playlist) get(Playlist.class, id);
}
public String getId() {
return mId;
}
public String getHatchetId() {
return mHatchetId;
}
public void setHatchetId(String hatchetId) {
mHatchetId = hatchetId;
}
public void setCurrentRevision(String currentRevision) {
mCurrentRevision = currentRevision == null ? "" : currentRevision;
}
public String getCurrentRevision() {
return mCurrentRevision;
}
public String[] getTopArtistNames() {
return mTopArtistNames;
}
public void setTopArtistNames(String[] topArtistNames) {
mTopArtistNames = topArtistNames;
}
public void updateTopArtistNames(boolean getMostRecentArtists) {
String[] results;
if (getMostRecentArtists) {
List<String> artistNames = new ArrayList<>();
for (int i = 0; i < size() && i < 5; i++) {
artistNames.add(getArtistName(i));
}
results = artistNames.toArray(new String[artistNames.size()]);
} else {
final HashMap<String, Integer> countMap = new HashMap<>();
for (int i = 0; i < size(); i++) {
String artistName = getArtistName(i);
if (countMap.containsKey(artistName)) {
countMap.put(artistName, countMap.get(artistName) + 1);
} else {
countMap.put(artistName, 1);
}
}
results = new String[0];
if (countMap.size() > 0) {
PriorityQueue<String> topArtistNames = new PriorityQueue<>(countMap.size(),
new Comparator<String>() {
@Override
public int compare(String lhs, String rhs) {
return countMap.get(lhs) >= countMap.get(rhs) ? -1 : 1;
}
}
);
topArtistNames.addAll(countMap.keySet());
results = topArtistNames.toArray(new String[topArtistNames.size()]);
}
}
mTopArtistNames = results;
}
/**
* @return this {@link Playlist}'s name
*/
public String getName() {
return mName;
}
/**
* Set the name of this {@link Playlist}
*
* @param name the name to be set
*/
public void setName(String name) {
mName = name == null ? "" : name;
}
private PlaylistEntry getEntry(Index index) {
PlaylistEntry entry;
if (index.fromMergedItems) {
entry = mAddedEntries.get(index.internalIndex);
} else {
entry = mCursor.get(index.internalIndex);
}
mCachedEntries.put(entry, index);
return entry;
}
/**
* Return the current count of entries in the {@link Playlist}
*/
public int size() {
return mIndex.size();
}
/**
* Return all PlaylistEntries in the {@link Playlist}. This is a very costly operation and
* should only be done if absolutely necessary. Consider using {@link #getEntryAtPos(int)}.
*/
public List<PlaylistEntry> getEntries() {
return getEntries(false);
}
/**
* Return all PlaylistEntries in the {@link Playlist}. This is a very costly operation and
* should only be done if absolutely necessary. Consider using {@link #getEntryAtPos(int)}.
*/
public List<PlaylistEntry> getEntries(boolean shuffled) {
List<PlaylistEntry> entries = new ArrayList<>();
List<Index> indexList = shuffled ? mShuffledIndex : mIndex;
for (Index index : indexList) {
PlaylistEntry entry = getEntry(index);
entries.add(entry);
mCachedEntries.put(entry, index);
}
return entries;
}
/**
* Add the given {@link Query} to this {@link Playlist}
*
* @param position the position at which to insert the given {@link Query}
* @param query the {@link Query} to add
* @return the {@link PlaylistEntry} that got created and added to this {@link Playlist}
*/
public PlaylistEntry addQuery(int position, Query query) {
PlaylistEntry entry = PlaylistEntry.get(mId, query,
IdGenerator.getLifetimeUniqueStringId());
mAddedEntries.add(entry);
Index index = new Index(mAddedEntries.size() - 1, true);
mIndex.add(position, index);
mCachedEntries.put(entry, index);
return entry;
}
/**
* Remove the given {@link Query} from this playlist
*/
public boolean deleteEntry(PlaylistEntry entry) {
Index index = mCachedEntries.get(entry);
if (index == null) {
Log.d(TAG, "deleteEntry - couldn't find cached PlaylistEntry.");
}
return mIndex.remove(index);
}
public long getCount() {
return mCount;
}
public void setCount(long count) {
mCount = count;
}
public boolean isFilled() {
return mIsFilled;
}
public void setFilled(boolean isFilled) {
mIsFilled = isFilled;
}
public PlaylistEntry getEntryAtPos(int position) {
return getEntryAtPos(position, false);
}
public PlaylistEntry getEntryAtPos(int position, boolean shuffled) {
List<Index> indexList = shuffled ? mShuffledIndex : mIndex;
if (position < 0 || position >= indexList.size()) {
return null;
}
Index index = indexList.get(position);
return getEntry(index);
}
public int getIndexOfEntry(PlaylistEntry entry) {
return getIndexOfEntry(entry, false);
}
public int getIndexOfEntry(PlaylistEntry entry, boolean shuffled) {
Index index = mCachedEntries.get(entry);
List<Index> indexList = shuffled ? mShuffledIndex : mIndex;
return indexList.indexOf(index);
}
public boolean containsEntry(PlaylistEntry entry) {
return getIndexOfEntry(entry) >= 0;
}
public String getUserId() {
return mUserId;
}
public void setUserId(String userId) {
mUserId = userId;
}
public boolean allFromOneArtist() {
if (size() < 2) {
return true;
}
String artistname = getArtistName(0);
for (int i = 1; i < size(); i++) {
String artistNameToCompare = getArtistName(i);
if (!artistNameToCompare.equals(artistname)) {
return false;
}
artistname = artistNameToCompare;
}
return true;
}
public String getArtistName(int position) {
Index index = mIndex.get(position);
if (index.fromMergedItems) {
return mAddedEntries.get(index.internalIndex).getArtist().getName();
} else {
return mCursor.getArtistName(index.internalIndex);
}
}
/**
* Shuffle this {@link Playlist}'s tracks. This method ensures that there's always a minimum
* amount of tracks in sequence that have the same artist.
*
* @param currentIndex the track at this position will be put at the top of the resulting
* shuffled list of tracks.
*/
public void buildShuffledIndex(int currentIndex) throws IndexOutOfBoundsException {
mShuffledIndex.clear();
if (currentIndex >= 0) {
// Add the current entry to the top of shuffled index
mShuffledIndex.add(mIndex.get(currentIndex));
}
List<String> artistNames = new ArrayList<>();
Map<String, List<Integer>> artistsTrackIndexes = new HashMap<>();
for (int i = 0; i < size(); i++) {
if (i != currentIndex) { // Don't add the currently playing track
String artistName = getArtistName(i);
if (artistsTrackIndexes.get(artistName) == null) {
artistsTrackIndexes.put(artistName, new ArrayList<Integer>());
artistNames.add(artistName);
}
artistsTrackIndexes.get(artistName).add(i);
}
}
Collections.shuffle(artistNames);
while (artistNames.size() > 0) {
for (int i = 0; i < artistNames.size(); i++) {
String artistName = artistNames.get(i);
// Now we can get the list of track indexes
List<Integer> indexes = artistsTrackIndexes.get(artistName);
int randomPos = (int) (Math.random() * indexes.size());
// Add the randomly picked track index to our shuffled index
int shuffledIndex = indexes.remove(randomPos);
mShuffledIndex.add(mIndex.get(shuffledIndex));
if (indexes.size() == 0) {
artistNames.remove(i);
}
}
}
}
@Override
public String toString() {
return getClass().getSimpleName() + "( id: " + getId() + ", hatchetId: " + mHatchetId
+ ", name: " + getName() + ", size: " + size() + " )@"
+ Integer.toHexString(hashCode());
}
}