import java.io.File;
import java.util.ArrayList;

import comp202.fall2007.a5.comparator.ComparatorFactory;
import comp202.fall2007.a5.filter.FilterFactory;
import comp202.fall2007.a5.filter.SongFilter;
import comp202.fall2007.a5.filter.StringMatchType;
import comp202.fall2007.a5.util.Song;

/**
 * A <code>MusicCollection</code> object represents a collection of
 * <code>Song</code>s. It differs from a <code>Playlist</code> in two major
 * ways:
 * <ol>
 * <li>it does not impose an order on the <code>Song</code>s it contains</li>
 * <li>it allows itself to be queried to retrieve information about the
 * <code>Song</code>s it contains, or to retrieve all <code>Song</code>s
 * that it contains and that also meet certain conditions.</li>
 * </ol>
 * <p>
 * 
 * <p>
 * Unlike a <code>Playlist</code>, a <code>MusicCollection</code> does not
 * support the inclusion of the same <code>Song</code> multiple times. The
 * primary key used to uniquely identify <code>Song</code>s in a
 * <code>MusicCollection</code> is the full (absolute) path of the file
 * associated with each <code>Song</code>. If an attempt is made to add a
 * <code>Song</code> to a <code>MusicCollection</code>, and the path of
 * this <code>Song</code> is the same as the path of a <code>Song</code>
 * already included in the <code>MusicCollection</code>, the
 * <code>MusicCollection</code> should not change as a result.
 * </p>
 * 
 * </p>
 * Also, the <code>MusicCollection</code> class does <b>NOT</b> support the
 * addition of <code>null</code> elements to a <code>MusicCollection</code>
 * object. If an attempt is made to add a <code>null</code> element to a
 * <code>MusicCollection</code>, the latter should not change as a result.
 * </p>
 * 
 * <p>
 * A <code>MusicCollection</code> <b>MUST</b> be able to store and manage an
 * arbitrary number of <code>Song</code>s, limited only by the memory
 * available to the Java Virtual Machine. In other words, the implementation of
 * the <code>MusicCollection</code> class <b>MUST NOT</b> not impose an
 * artificial limit to the number of <code>Song</code>s a
 * <code>MusicCollection</code> can store and manage.
 * </p>
 */
public class MusicCollection {
	private ArrayList<Song> songs;
	private FilterFactory factory;

	// *** Constructors ***

	/**
	 * Creates a new empty <code>MusicCollection</code>.
	 */
	public MusicCollection() {
		this.songs = new ArrayList<Song>();
		this.factory = new FilterFactory();
	}

	// *** PRIVATE HELPER METHODS ***

	private ArrayList<String> extractAlbums(SongFilter filter) {
		ArrayList<String> albumTitles;
		Playlist songSet;
		String title;
		String last;
		Song song;
		int numberSongs;

		songSet = this.getSongs(filter);
		songSet.sort(ComparatorFactory.ASCENDING_ALBUM_COMPARATOR);

		numberSongs = songSet.getSize();

		albumTitles = new ArrayList<String>();
		last = null;
		for (int i = 0; i < numberSongs; i++) {
			song = songSet.get(i);
			title = song.getAlbum();
			if ((last == null) || (!title.equalsIgnoreCase(last))) {
				albumTitles.add(title);
				last = title;
			}
		}

		return albumTitles;
	}

	// *** PUBLIC METHODS ***

	/**
	 * <p>
	 * Makes this <code>MusicCollection</code> empty, so that it contains no
	 * <code>Songs</code>.
	 * </p>
	 */
	public void clear() {
		this.songs.clear();
	}

	/**
	 * <p>
	 * Returns an <code>ArrayList</code> of <code>String</code>s containing
	 * the names of the all artists who have produced at least one of the
	 * <code>Song</code>s currently stored in this
	 * <code>MusicCollection</code>, as given by the values of the artist
	 * attribute of the <code>Song</code>s stored in this
	 * <code>MusicCollection</code>.
	 * </p>
	 * 
	 * <p>
	 * The name of each artist is included <b>ONLY</b> once in the returned
	 * <code>ArrayList</code>, regardless of how many <code>Song</code>s
	 * stored in this <code>MusicCollection</code> this artist has produced.
	 * Also, equality is determined by comparing the artist attributes of
	 * <code>Song</code>s. Finally, equality is determined in a case-<b>IN</b>sensitive
	 * manner.
	 * </p>
	 * 
	 * <p>
	 * For example, artist names <code>"Joe Bleau"</code> and
	 * <code>"JOE BLEAU"</code> are considered to be the same for the purposes
	 * of determining the uniqueness of an artist's name, and only one of them
	 * should be included in the <code>ArrayList</code> of artist names
	 * returned by this method.
	 * </p>
	 * 
	 * <p>
	 * However, <code>"Joe Bleau"</code> and
	 * <code>"Joe Bleau and Jane Doe"</code> are considered to be different
	 * artists for the purpose of determining the uniqueness of an artist's
	 * name, and both should be included in the <code>ArrayList</code> of
	 * artist names returned by this method.
	 * </p>
	 * 
	 * <p>
	 * The artist names <b>MUST</b> appear within the returned
	 * <code>ArrayList</code> in case-<b>IN</b>sensitive lexicographical
	 * order. Subsequent changes to the returned <code>ArrayList</code>
	 * <b>MUST NOT</b> affect the state of this <code>MusicCollection</code>.
	 * Likewise, subsequent changes to the state of this
	 * <code>MusicCollection</code> <b>MUST NOT</b> have any impact on the
	 * returned <code>ArrayList</code>.
	 * </p>
	 * 
	 * @return An <code>ArrayList</code> of <code>String</code>s containing
	 *         the names of all artists who have produced at least one song
	 *         currently stored in this <code>MusicCollection</code>.
	 */
	public ArrayList<String> getAllArtists() {
		Playlist songSet;
		ArrayList<String> allArtists;
		String artistName;
		String last;
		Song song;
		int numberSongs;

		songSet = new Playlist();
		songSet.addAll(this.songs);
		songSet.sort(ComparatorFactory.ASCENDING_ARTIST_COMPARATOR);
		numberSongs = songSet.getSize();

		allArtists = new ArrayList<String>();
		last = null;
		for (int i = 0; i < numberSongs; i++) {
			song = songSet.get(i);
			artistName = song.getArtist();
			if ((last == null) || (!artistName.equalsIgnoreCase(last))) {
				allArtists.add(artistName);
				last = artistName;
			}
		}
		return allArtists;
	}

	/**
	 * <p>
	 * Returns an <code>ArrayList</code> of <code>String</code>s containing
	 * the titles of all albums represented by <code>Song</code>s in this
	 * <code>MusicCollection</code>. In other words, for an album title <i>t</i>
	 * to be included in the returned <code>ArrayList</code>, there must be
	 * at least one <code>Song</code> in this <code>MusicCollection</code>
	 * whose album attribute is equal to this album title <i>t</i>.
	 * </p>
	 * 
	 * <p>
	 * The title of each album is included <b>ONLY</b> once in the returned
	 * <code>ArrayList</code>, regardless of how many <code>Song</code>s
	 * stored in this <code>MusicCollection</code> have a given album title as
	 * their album title attribute. Also, equality is determined in a case-<b>IN</b>sensitive
	 * manner. For example, album names <code>"Joe Bleau: Greatest Hits"</code>
	 * and <code>"JOE BLEAU: GREATEST HITS"</code> are considered to be the
	 * same for the purposes of determining the uniqueness of an album's title,
	 * and only one of them should be included in the <code>ArrayList</code>
	 * returned by this method.
	 * </p>
	 * 
	 * <p>
	 * The album titles <b>MUST</b> appear within the returned
	 * <code>ArrayList</code> in case-<b>IN</b>sensitive lexicographical
	 * order. Subsequent changes to the returned <code>ArrayList</code>
	 * <b>MUST NOT</b> affect the state of this <code>MusicCollection</code>.
	 * Likewise, subsequent changes to the state of this
	 * <code>MusicCollection</code> <b>MUST NOT</b> have any impact on the
	 * returned <code>ArrayList</code>.
	 * </p>
	 * 
	 * @return An <code>ArrayList</code> of <code>String</code>s containing
	 *         the titles of all albums which include at least one song
	 *         currently stored in this <code>MusicCollection</code>.
	 */
	public ArrayList<String> getAllAlbums() {
		return extractAlbums(this.factory.createEverythingFilter());
	}

	/**
	 * <p>
	 * Returns an <code>ArrayList</code> of <code>String</code>s containing
	 * the titles of all albums to which the specified artist has contributed at
	 * least one of the <code>Song</code>s stored in this
	 * <code>MusicCollection</code>, as given by the values of the artist and
	 * album attributes of the <code>Song</code>s stored in this
	 * <code>MusicCollection</code>.
	 * </p>
	 * 
	 * <p>
	 * For example, if artist "Joe Bleau" has contributed 2 songs to the album
	 * titled <i>Tribute to Jane Doe</i> (assuming that a least one of these
	 * songs is stored as a <code>Song</code> object in this
	 * <code>MusicCollection</code>) and 10 songs to the album <i>Joe Bleau:
	 * Greatest Hits</i> (again assuming that at least one of these songs is
	 * stored as a <code>Song</code> object in this
	 * <code>MusicCollection</code>), then the returned
	 * <code>ArrayList</code> should contain
	 * <code>"Tribute to Jane Doe"</code> and
	 * <code>"Joe Bleau: Greatest Hits"</code>.
	 * </p>
	 * 
	 * <p>
	 * The title of each album is included <b>ONLY</b> once in the returned
	 * <code>ArrayList</code>, regardless of how many <code>Song</code>s
	 * stored in this <code>MusicCollection</code> this album contains. Also,
	 * equality is determined in a case-<b>IN</b>sensitive manner. For
	 * example, album names <code>"Joe Bleau: Greatest Hits"</code> and
	 * <code>"JOE BLEAU: GREATEST HITS"</code> are considered to be the same
	 * for the purposes of determining the uniqueness of an artist's name, and
	 * only one of them should be included in the <code>ArrayList</code>
	 * returned by this method.
	 * </p>
	 * 
	 * <p>
	 * The album titles <b>MUST</b> appear within the returned
	 * <code>ArrayList</code> in case-<b>IN</b>sensitive lexicographical
	 * order. Subsequent changes to the returned <code>ArrayList</code>
	 * <b>MUST NOT</b> affect the state of this <code>MusicCollection</code>.
	 * Likewise, subsequent changes to the state of this
	 * <code>MusicCollection</code> <b>MUST NOT</b> have any impact on the
	 * returned <code>ArrayList</code>.
	 * </p>
	 * 
	 * @param artistName
	 *            The artist whose whose album titles should be returned.
	 * @return An <code>ArrayList</code> of <code>String</code> containing
	 *         the titles of all albums to which the specified artist has
	 *         contributed at least one of the <code>Song</code>s currently
	 *         stored in this <code>MusicCollection</code>.
	 */
	public ArrayList<String> getContributedAlbums(String artistName) {
		return extractAlbums(this.factory.createArtistFilter(artistName,
				StringMatchType.EQUALS));
	}

	/**
	 * <p>
	 * Returns a <code>Playlist</code> containing all the <code>Song</code>s
	 * stored in this <code>MusicCollection</code> whose album attributes are
	 * equal to the specified album title.
	 * </p>
	 * 
	 * <p>
	 * The comparison is performed in a case-<b>IN</b>sensitive manner. The
	 * <code>Song</code>s in the returned <code>Playlist</code> <b>MUST</b>
	 * be sorted in increasing order by disc number. If two <code>Song</code>s
	 * have the same disc number, the song with the smaller track number <b>MUST</b>
	 * occur before the other in the returned <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * Subsequent changes to the returned <code>Playlist</code> <b>MUST NOT</b>
	 * affect the state of this <code>MusicCollection</code>. Likewise,
	 * subsequent changes to the state of this <code>MusicCollection</code>
	 * <b>MUST NOT</b> have any impact on the returned <code>Playlist</code>.
	 * </p>
	 * 
	 * @param albumTitle
	 *            The title of the album.
	 * @return A <code>Playlist</code> containing all the <code>Song</code>s
	 *         stored in this <code>MusicCollection</code> whose album
	 *         attributes are equal to <code>albumTitle</code>.
	 */
	public Playlist getSongs(String albumTitle) {
		Playlist songSet;

		songSet = this.getSongs(this.factory.createAlbumFilter(albumTitle,
				StringMatchType.EQUALS));
		songSet.sort(ComparatorFactory.ASCENDING_DISC_TRACK_COMPARATOR);
		return songSet;
	}

	/**
	 * <p>
	 * Returns a <code>Playlist</code> containing all the <code>Song</code>s
	 * stored in this <code>MusicCollection</code> whose artist attributes are
	 * equal to the specified artist name, and whose album attributes are equal
	 * to the specified album title.
	 * </p>
	 * 
	 * <p>
	 * The comparison is performed in a case-<b>IN</b>sensitive manner. The
	 * <code>Song</code>s in the returned <code>Playlist</code> <b>MUST</b>
	 * be sorted in increasing order by disc number. If two <code>Song</code>s
	 * have the same disc number, the song with the smaller track number <b>MUST</b>
	 * occur before the other in the <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * Subsequent changes to the returned <code>Playlist</code> <b>MUST NOT</b>
	 * affect the state of this <code>MusicCollection</code>. Likewise,
	 * subsequent changes to the state of this <code>MusicCollection</code>
	 * <b>MUST NOT</b> have any impact on the returned <code>Playlist</code>.
	 * </p>
	 * 
	 * @param artistName
	 *            The name of the artist.
	 * @param albumTitle
	 *            The title of the album.
	 * @return A <code>Playlist</code> containing all the <code>Song</code>s
	 *         stored in this <code>MusicCollection</code> whose artist
	 *         attributes are equal to <code>artistName</code>, and whose
	 *         album attributes are equal to <code>albumTitle</code>.
	 */
	public Playlist getSongs(String artistName, String albumTitle) {
		ArrayList<SongFilter> filterList;
		SongFilter filter;
		Playlist songSet;

		filterList = new ArrayList<SongFilter>();
		filterList.add(this.factory.createAlbumFilter(albumTitle,
				StringMatchType.EQUALS));
		filterList.add(this.factory.createArtistFilter(artistName,
				StringMatchType.EQUALS));
		filter = this.factory.createAndFilter(filterList);

		songSet = this.getSongs(filter);
		songSet.sort(ComparatorFactory.ASCENDING_DISC_TRACK_COMPARATOR);
		return songSet;
	}

	/**
	 * <p>
	 * Returns a <code>Playlist</code> containing all the <code>Song</code>s
	 * stored in this <code>MusicCollection</code> that are accepted by the
	 * specified <code>SongFilter</code>. In other words, this method returns
	 * a <code>Playlist</code> containing all the <code>Song</code>s
	 * currently stored in this <code>MusicCollection</code> for which the
	 * <code>accept()</code> method defined in the <code>SongFilter</code>
	 * class returns <code>true</code> when invoked on the specified
	 * <code>SongFilter</code> object.
	 * </p>
	 * 
	 * <p>
	 * The order in which the <code>Song</code>s appear in the returned
	 * <code>Playlist</code> is undefined (that is, the <code>Song</code>s
	 * can appear in any order).
	 * </p>
	 * 
	 * <p>
	 * Subsequent changes to the returned <code>Playlist</code> <b>MUST NOT</b>
	 * affect the state of this <code>MusicCollection</code>. Likewise,
	 * subsequent changes to the state of this <code>MusicCollection</code>
	 * <b>MUST NOT</b> have any impact on the returned <code>Playlist</code>.
	 * </p>
	 * 
	 * @param filter
	 *            A <code>SongFilter</code> object; <code>Song</code>s
	 *            accepted by this <code>SongFilter</code> are included in the
	 *            returned <code>PlayList</code>.
	 * @return A <code>Playlist</code> containing all <code>Song</code>s
	 *         currently stored in this <code>MusicCollection</code> that are
	 *         accepted by <code>filter</code>.
	 */
	public Playlist getSongs(SongFilter filter) {
		Playlist songSubset;

		songSubset = new Playlist();
		for (Song song : this.songs) {
			if (filter.accept(song)) {
				songSubset.add(song);
			}
		}
		return songSubset;
	}

	/**
	 * <p>
	 * Returns the <code>Song</code> contained in this
	 * <code>MusicCollection</code> whose path attribute is equal to the
	 * specified <code>File</code>, as defined by the <code>equals()</code>
	 * method of the <code>File</code> class, if such a <code>Song</code>
	 * exists within the <code>MusicCollection</code>. If this
	 * <code>MusicCollection</code> contains no such object, this method
	 * returns <code>null</code>.
	 * </p>
	 * 
	 * @param path
	 *            The path of a <code>Song</code> potentially stored in this
	 *            <code>MusicCollection</code>.
	 * @return The <code>Song</code> object contained in this
	 *         <code>MusicCollection</code> whose path attribute is equal to
	 *         <code>path</code> if one exists, <code>null</code> otherwise.
	 */
	public Song getSong(File path) {
		Playlist oneSong;

		oneSong = this.getSongs(this.factory.createPathFilter(path));
		return (oneSong.isEmpty() ? null : oneSong.get(0));
	}

	/**
	 * <p>
	 * Adds the specified <code>Song</code> to this
	 * <code>MusicCollection</code>.
	 * </p>
	 * 
	 * <p>
	 * If the specified <code>Song</code> is <code>null</code>, or if this
	 * <code>MusicCollection</code> contains a <code>Song</code> whose path
	 * attribute is equal to the path attribute of the specified
	 * <code>Song</code>, then this <code>Song</code> <b>MUST NOT</b> be
	 * added to this <code>MusicCollection</code>, and the latter therefore
	 * does not change.
	 * </p>
	 * 
	 * @param song
	 *            The <code>Song</code> to be added to this
	 *            <code>MusicCollection</code>.
	 */
	public void add(Song song) {
		if ((song != null) && (this.getSong(song.getFilePath()) == null)) {
			this.songs.add(song);
		}
	}

	/**
	 * Adds the <code>Song</code>s contained in the specified
	 * <code>ArrayList</code> into this <code>MusicCollection</code>.
	 * 
	 * <p>
	 * If the specified <code>ArrayList</code> contains <code>null</code>
	 * elements, these <code>null</code> elements <b>MUST NOT</b> be added to
	 * this <code>MusicCollection</code>. Likewise, if the specified
	 * <code>ArrayList</code> contains <code>Song</code>s whose path
	 * attributes are equal to the path attribute of <code>Song</code>s
	 * contained this <code>MusicCollection</code>, these <code>Song</code>s
	 * <b>MUST NOT</b> be added to this <code>MusicCollection</code>.
	 * However, non-<code>null</code> <code>Song</code>s contained in the
	 * specified <code>ArrayList</code> whose path attributes are not equal to
	 * the path attributes of <code>Song</code>s currently stored in this
	 * <code>MusicCollection</code> <b>MUST</b> be added to the latter.
	 * </p>
	 * 
	 * @param songList
	 *            An <code>ArrayList</code> of <code>Song</code>s to be
	 *            added to this <code>MusicCollection</code>.
	 */
	public void addAll(ArrayList<Song> songList) {
		for (Song song : songList) {
			this.add(song);
		}
	}

	/**
	 * Adds the <code>Song</code> objects contained in the specified array of
	 * <code>Song</code>s into this <code>MusicCollection</code>.
	 * 
	 * <p>
	 * If the specified array contains <code>null</code> elements, these
	 * <code>null</code> elements <b>MUST NOT</b> be added to this
	 * <code>MusicCollection</code>. Likewise, if the specified array
	 * contains <code>Song</code>s whose path attributes are equal to the
	 * path attribute of <code>Song</code>s contained this
	 * <code>MusicCollection</code>, these <code>Song</code>s <b>MUST NOT</b>
	 * be added to this <code>MusicCollection</code>. However, non-<code>null</code> <code>Song</code>s
	 * contained in the specified array whose path attributes are not equal to
	 * the path attributes of <code>Song</code>s currently stored in this
	 * <code>MusicCollection</code> <b>MUST</b> be added to the latter.
	 * </p>
	 * 
	 * @param songArray
	 *            An array of <code>Song</code>s to be added to this
	 *            <code>MusicCollection</code>.
	 */
	public void addAll(Song[] songArray) {
		for (Song song : songArray) {
			this.add(song);
		}
	}
}