package com.marverenic.music.model;
import android.annotation.SuppressLint;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.gson.annotations.SerializedName;
import com.marverenic.music.data.store.MusicStore;
import com.marverenic.music.data.store.PlayCountStore;
import com.marverenic.music.data.store.PlaylistStore;
import com.marverenic.music.model.playlistrules.AutoPlaylistRule;
import com.marverenic.music.model.playlistrules.AutoPlaylistRule.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import rx.Observable;
public class AutoPlaylist extends Playlist implements Parcelable {
/**
* Value representing an unlimited amount of song entries
*/
public static final int UNLIMITED_ENTRIES = -1;
/**
* How many items can be stored in this playlist. Default is unlimited
*/
@SerializedName("maximumEntries")
private final int mMaximumEntries;
/**
* The field to look at when truncating the playlist. Must be a member of {@link Field}.
* {@link Field#ID} will yield a random trim
*/
@SerializedName("truncateMethod")
private final int mTruncateMethod;
/**
* Whether to trim the playlist ascending (A-Z, oldest to newest, or 0-infinity).
* If false, sort descending (Z-A, newest to oldest, or infinity-0).
*/
@SerializedName("truncateAscending")
private final boolean mTruncateAscending;
/**
* Whether or not a song has to match all rules in order to appear in the playlist.
*/
@SerializedName("matchAllRules")
private final boolean mMatchAllRules;
/**
* The rules to match when building the playlist
*/
@SerializedName("rules")
private final List<AutoPlaylistRule> mRules;
/**
* The field to look at when sorting the playlist. Must be a member of {@link Field} and
* cannot be {@link Field#ID}
*/
@SerializedName("sortMethod")
private final int mSortMethod;
/**
* Whether to sort the playlist ascending (A-Z, oldest to newest, or 0-infinity).
* If false, sort descending (Z-A, newest to oldest, or infinity-0).
* Default is true.
*/
@SerializedName("sortAscending")
private final boolean mSortAscending;
/**
* AutoPlaylist Creator
* @param playlistId A unique ID for the Auto Playlist. Must be unique and not conflict with the
* MediaStore
* @param playlistName The name given to this playlist by the user
* @param maximumEntries The maximum number of songs this playlist should have. Use
* {@link AutoPlaylist#UNLIMITED_ENTRIES} for no limit. This limit will
* be applied after the list has been sorted. Any extra entries will be
* truncated.
* @param sortMethod The order the songs will be sorted (Must be one of
* {@link Field} and can't be ID
* @param sortAscending Whether to sort this playlist ascending (A-Z or 0-infinity) or not
* @param matchAllRules Whether or not all rules have to be matched for a song to appear in this
* playlist
* @param rules The rules that songs must follow in order to appear in this playlist
*/
private AutoPlaylist(long playlistId, String playlistName, int maximumEntries, int sortMethod,
int truncateMethod, boolean truncateAscending, boolean sortAscending,
boolean matchAllRules, List<AutoPlaylistRule> rules) {
super(playlistId, playlistName);
mMaximumEntries = maximumEntries;
mMatchAllRules = matchAllRules;
mRules = Collections.unmodifiableList(rules);
mTruncateMethod = truncateMethod;
mTruncateAscending = truncateAscending;
mSortMethod = sortMethod;
mSortAscending = sortAscending;
}
public Observable<List<Song>> generatePlaylist(MusicStore musicStore,
PlaylistStore playlistStore,
PlayCountStore playCountStore) {
if (getRules().isEmpty()) {
return Observable.just(Collections.emptyList());
}
Observable<List<Song>> filtered = null;
for (AutoPlaylistRule rule : getRules()) {
Observable<List<Song>> ruleEntries;
ruleEntries = rule.applyFilter(playlistStore, musicStore, playCountStore);
if (filtered == null) {
filtered = ruleEntries;
} else {
filtered = combineRules(filtered, ruleEntries);
}
}
// Perform the filter after play counts are refreshed
final Observable<List<Song>> finalFiltered = filtered;
Observable<List<Song>> matchingSongs = playCountStore.refresh()
.flatMap(ignored -> finalFiltered);
Observable<List<Song>> truncated = truncateFilteredSongs(matchingSongs, playCountStore);
return sortFilteredSongs(truncated, playCountStore);
}
private Observable<List<Song>> combineRules(Observable<List<Song>> result1,
Observable<List<Song>> result2) {
if (isMatchAllRules()) { // AND
return Observable.combineLatest(result1, result2, (songs, songs2) -> {
List<Song> merged = new ArrayList<>(songs);
merged.retainAll(songs2);
return merged;
});
} else { // OR
return Observable.combineLatest(result1, result2, (songs, songs2) -> {
Set<Song> mergedSet = new HashSet<>(songs);
mergedSet.addAll(songs2);
return new ArrayList<>(mergedSet);
});
}
}
private Observable<List<Song>> truncateFilteredSongs(Observable<List<Song>> filterResult,
PlayCountStore playCountStore) {
if (getMaximumEntries() < 0) {
return filterResult;
}
return filterResult
.map(filteredSongs -> {
sortSongListByField(filteredSongs, getTruncateMethod(), isSortAscending(),
playCountStore);
return filteredSongs;
}).map(sortedSongs -> {
if (sortedSongs.size() > getMaximumEntries()) {
return sortedSongs.subList(0, getMaximumEntries());
} else {
return sortedSongs;
}
});
}
private Observable<List<Song>> sortFilteredSongs(Observable<List<Song>> truncateResult,
PlayCountStore playCountStore) {
return truncateResult
.map(truncatedSongs -> {
sortSongListByField(truncatedSongs, getSortMethod(), isSortAscending(),
playCountStore);
return truncatedSongs;
});
}
private static void sortSongListByField(List<Song> songs, @Field int field, boolean ascending,
PlayCountStore playCountStore) {
if (field == AutoPlaylistRule.NAME) {
Collections.sort(songs);
if (!ascending) {
Collections.reverse(songs);
}
} else if (field == AutoPlaylistRule.ID) {
Collections.shuffle(songs);
} else {
Collections.sort(songs, getSortComparator(field, playCountStore));
if (ascending) {
Collections.reverse(songs);
}
}
}
@SuppressLint("SwitchIntDef")
private static Comparator<Song> getSortComparator(@Field int field,
PlayCountStore playCountStore) {
switch (field) {
case AutoPlaylistRule.YEAR:
return Song.YEAR_COMPARATOR;
case AutoPlaylistRule.DATE_ADDED:
return Song.DATE_ADDED_COMPARATOR;
case AutoPlaylistRule.DATE_PLAYED:
return Song.playDateComparator(playCountStore);
case AutoPlaylistRule.PLAY_COUNT:
return Song.playCountComparator(playCountStore);
case AutoPlaylistRule.SKIP_COUNT:
return Song.skipCountComparator(playCountStore);
}
return null;
}
public static final Parcelable.Creator<Parcelable> CREATOR =
new Parcelable.Creator<Parcelable>() {
public AutoPlaylist createFromParcel(Parcel in) {
return new AutoPlaylist(in);
}
public AutoPlaylist[] newArray(int size) {
return new AutoPlaylist[size];
}
};
private AutoPlaylist(Parcel in) {
super(in);
mMaximumEntries = in.readInt();
mMatchAllRules = in.readByte() == 1;
mRules = Collections.unmodifiableList(in.createTypedArrayList(AutoPlaylistRule.CREATOR));
mSortMethod = in.readInt();
mTruncateMethod = in.readInt();
mTruncateAscending= in.readByte() == 1;
mSortAscending = in.readByte() == 1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mMaximumEntries);
dest.writeByte((byte) ((mMatchAllRules) ? 1 : 0));
dest.writeTypedList(mRules);
dest.writeInt(mSortMethod);
dest.writeInt(mTruncateMethod);
dest.writeByte((byte) ((mTruncateAscending) ? 1 : 0));
dest.writeByte((byte) ((mSortAscending) ? 1 : 0));
}
public int getMaximumEntries() {
return mMaximumEntries;
}
@Field
public int getTruncateMethod() {
return mTruncateMethod;
}
public boolean isTruncateAscending() {
return mTruncateAscending;
}
public boolean isMatchAllRules() {
return mMatchAllRules;
}
public List<AutoPlaylistRule> getRules() {
return mRules;
}
@Field
public int getSortMethod() {
return mSortMethod;
}
public boolean isSortAscending() {
return mSortAscending;
}
public static class Builder implements Parcelable {
public static final long NO_ID = -1;
private long mId;
private String mName;
private int mMaximumEntries;
private int mTruncateMethod;
private boolean mTruncateAscending;
private boolean mMatchAllRules;
private List<AutoPlaylistRule> mRules;
private int mSortMethod;
private boolean mSortAscending;
public Builder() {
mId = NO_ID;
}
public Builder(AutoPlaylist from) {
mId = from.getPlaylistId();
mName = from.getPlaylistName();
mMaximumEntries = from.getMaximumEntries();
mTruncateMethod = from.getTruncateMethod();
mTruncateAscending = from.isTruncateAscending();
mMatchAllRules = from.isMatchAllRules();
mRules = new ArrayList<>(from.getRules());
mSortMethod = from.getSortMethod();
mSortAscending = from.isSortAscending();
}
protected Builder(Parcel in) {
mId = in.readLong();
mName = in.readString();
mMaximumEntries = in.readInt();
mTruncateMethod = in.readInt();
mTruncateAscending = in.readByte() != 0;
mMatchAllRules = in.readByte() != 0;
mRules = in.createTypedArrayList(AutoPlaylistRule.CREATOR);
mSortMethod = in.readInt();
mSortAscending = in.readByte() != 0;
}
public static final Creator<Builder> CREATOR = new Creator<Builder>() {
@Override
public Builder createFromParcel(Parcel in) {
return new Builder(in);
}
@Override
public Builder[] newArray(int size) {
return new Builder[size];
}
};
public long getId() {
return mId;
}
public Builder setId(long id) {
mId = id;
return this;
}
public String getName() {
return mName;
}
public Builder setName(String name) {
mName = name;
return this;
}
public int getMaximumEntries() {
return mMaximumEntries;
}
public Builder setMaximumEntries(int maximumEntries) {
mMaximumEntries = maximumEntries;
return this;
}
@Field
public int getTruncateMethod() {
return mTruncateMethod;
}
public Builder setTruncateMethod(int truncateMethod) {
mTruncateMethod = truncateMethod;
return this;
}
public boolean isTruncateAscending() {
return mTruncateAscending;
}
public Builder setTruncateAscending(boolean truncateAscending) {
mTruncateAscending = truncateAscending;
return this;
}
public boolean isMatchAllRules() {
return mMatchAllRules;
}
public Builder setMatchAllRules(boolean matchAllRules) {
mMatchAllRules = matchAllRules;
return this;
}
public List<AutoPlaylistRule> getRules() {
return mRules;
}
public Builder setRules(AutoPlaylistRule... rules) {
return setRules(new ArrayList<>(Arrays.asList(rules)));
}
public Builder setRules(List<AutoPlaylistRule> rules) {
mRules = rules;
return this;
}
@Field
public int getSortMethod() {
return mSortMethod;
}
public Builder setSortMethod(int sortMethod) {
mSortMethod = sortMethod;
return this;
}
public boolean isSortAscending() {
return mSortAscending;
}
public Builder setSortAscending(boolean sortAscending) {
mSortAscending = sortAscending;
return this;
}
public boolean isEqual(AutoPlaylist reference) {
return getId() == reference.getPlaylistId()
&& getName().equals(reference.getPlaylistName())
&& getMaximumEntries() == reference.getMaximumEntries()
&& getTruncateMethod() == reference.getTruncateMethod()
&& isTruncateAscending() == reference.isTruncateAscending()
&& isMatchAllRules() == reference.isMatchAllRules()
&& getRules().equals(reference.getRules())
&& getSortMethod() == reference.getSortMethod()
&& isSortAscending() == reference.isSortAscending();
}
public AutoPlaylist build() {
return new AutoPlaylist(mId, mName, mMaximumEntries, mSortMethod, mTruncateMethod,
mTruncateAscending, mSortAscending, mMatchAllRules, mRules);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(mId);
parcel.writeString(mName);
parcel.writeInt(mMaximumEntries);
parcel.writeInt(mTruncateMethod);
parcel.writeByte((byte) (mTruncateAscending ? 1 : 0));
parcel.writeByte((byte) (mMatchAllRules ? 1 : 0));
parcel.writeTypedList(mRules);
parcel.writeInt(mSortMethod);
parcel.writeByte((byte) (mSortAscending ? 1 : 0));
}
}
}