import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Scanner;

import comp202.fall2007.a5.comparator.SongComparator;
import comp202.fall2007.a5.util.Song;
import comp202.fall2007.a5.util.SongFactory;

/**
 * <p>
 * A <code>Playlist</code> is an ordered sequence of <code>Song</code>s.
 * Its main use is as input to a music player; a player will play all the
 * <code>Song</code>s in a <code>Playlist</code> in the order in which they
 * appear within the <code>Playlist</code>. Because of this, a
 * <code>Playlist</code> can contain the same <code>Song</code> more than
 * once, at different positions within the <code>Playlist</code>.
 * </p>
 * 
 * <p>
 * <code>PlayList</code>s do <b>NOT</b> support the addition of
 * <code>null</code> elements. If an attempt is made to add a
 * <code>null</code> element to a <code>Playlist</code>, the latter should
 * not change as a result.
 * </p>
 * 
 * <p>
 * Like most of the Java container classes, <code>Playlist</code>s are
 * indexed from <code>0</code> to the number of elements (that is,
 * <code>Song</code>s) they contain, minus <code>1</code>. That list, if
 * the <code>Playlist</code> contains 6 <code>Song</code>s, the first
 * <code>Song</code> will be at index <code>0</code>, the second
 * <code>Song</code> will be at index <code>1</code>, and so on, while the
 * last <code>Song</code> will be at index <code>(6 - 1)</code> (that is,
 * <code>5</code>).
 * </p>
 * 
 * <p>
 * A <code>Playlist</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>Playlist</code> class <b>MUST NOT</b> not impose an artificial
 * limit to the number of <code>Song</code>s a <code>Playlist</code> can
 * store and manage.
 * </p>
 */
public class Playlist {
	private ArrayList<Song> list;
	private SongFactory factory;

	// *** CONSTRUCTORS ***

	/**
	 * Creates a new empty <code>Playlist</code>.
	 */
	public Playlist() {
		this.list = new ArrayList<Song>();
		this.factory = new SongFactory();
	}

	/**
	 * <p>
	 * Creates a new <code>Playlist</code> containing the same
	 * <code>Song</code>s as those contained in the specified
	 * <code>Playlist</code>, in the same order.
	 * 
	 * <p>
	 * Subsequent changes to the newly created <code>Playlist</code> <b>MUST
	 * NOT</b> affect the specified <code>Playlist</code>, and subsequent
	 * changes to the specified <code>Playlist</code> <b>MUST NOT</b> affect
	 * the newly-created <code>Playlist</code> either.
	 * </p>
	 * 
	 * @param playlist
	 *            A <code>Playlist</code> which contains the <code>Song</code>s
	 *            to be stored in the newly-created <code>Playlist</code>.
	 */
	public Playlist(Playlist playlist) {
		this();
		this.list.addAll(playlist.list);
	}

	// *** PRIVATE HELPER METHODS ***

	private int computeMaxPosition(SongComparator comparator, int size) {
		Song max;
		Song otherSong;
		int position;

		if (size == 0) {
			position = -1;
		} else {
			position = 0;
			max = this.list.get(position);

			for (int i = 1; i < size; i++) {
				otherSong = this.list.get(i);
				if (comparator.compareTo(max, otherSong) < 0) {
					max = otherSong;
					position = i;
				}
			}
		}
		return position;
	}

	private void sort(SongComparator comparator, int sizeToSort) {
		Song holder;
		int maxPosition;

		if (sizeToSort > 1) {
			maxPosition = this.computeMaxPosition(comparator, sizeToSort);

			holder = this.list.get(sizeToSort - 1);
			this.list.set(sizeToSort - 1, this.list.get(maxPosition));
			this.list.set(maxPosition, holder);

			this.sort(comparator, sizeToSort - 1);
		}
	}

	// *** PUBLIC METHODS ***

	/**
	 * <p>
	 * Makes this <code>Playlist</code> object empty, so that it contains no
	 * <code>Song</code>s. After calling this method on a
	 * <code>Playlist</code>, calling <code>isEmpty()</code> on the same
	 * <code>Playlist</code> <b>MUST</b> return <code>true</code>, and
	 * calling <code>getSize()</code> on the same <code>PlayList</code>
	 * <b>MUST</b> return <code>0</code>.
	 * </p>
	 */
	public void clear() {
		this.list.clear();
	}

	/**
	 * <p>
	 * Adds the specified <code>Song</code> to the end of this
	 * <code>Playlist</code> object. If the specified <code>Song</code> is
	 * <code>null</code>, it <b>MUST NOT</b> be added to the
	 * <code>Playlist</code> and, therefore, the <code>Playlist</code>
	 * <b>MUST NOT</b> change.
	 * </p>
	 * 
	 * <p>
	 * This method returns <code>true</code> if this <code>Playlist</code>
	 * object has changed as a result of calling this method, <code>false</code>
	 * otherwise. Because a <code>Playlist</code> supports duplicate elements
	 * and its size is limited only by the memory available to the Java Virtual
	 * Machine, the only way a <code>Playlist</code> cannot change when this
	 * method is called is if the specified <code>Song</code> is
	 * <code>null</code>.
	 * </p>
	 * 
	 * @param song
	 *            The <code>Song</code> to be added to the end of this
	 *            <code>Playlist</code>.
	 * @return <code>true</code> if this <code>Playlist</code> object has
	 *         changed as a result of calling this method, <code>false</code>
	 *         otherwise.
	 */
	public boolean add(Song song) {
		return (song != null) ? this.list.add(song) : false;
	}

	/**
	 * <p>
	 * Adds all <code>Song</code>s contained in the specified
	 * <code>Playlist</code> to the end of this <code>Playlist</code>, in
	 * the order in which they appear in the specified <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * This method returns <code>true</code> if this <code>Playlist</code>
	 * object has changed as a result of calling this method, <code>false</code>
	 * otherwise. Because a <code>Playlist</code> cannot contain
	 * <code>null</code> elements, supports duplicate elements, and its size
	 * is limited only by the memory available to the Java Virtual Machine, the
	 * only way a <code>Playlist</code> cannot change when this method is
	 * called is if the specified <code>Playlist</code> contains no
	 * <code>Song</code>s (that is, calling <code>isEmpty()</code> on the
	 * specified <code>Playlist</code> returns <code>true</code>).
	 * </p>
	 * 
	 * <p>
	 * Subsequent changes to this <code>Playlist</code> <b>MUST NOT</b>
	 * affect the specified <code>Playlist</code>, and subsequent changes to
	 * the specified <code>Playlist</code> <b>MUST NOT</b> affect this
	 * <code>Playlist</code> either.
	 * </p>
	 * 
	 * @param playList
	 *            The <code>Playlist</code> containing the <code>Song</code>s
	 *            to be added to this <code>Playlist</code>.
	 * @return <code>true</code> if this <code>Playlist</code> object has
	 *         changed as a result of calling this method, <code>false</code>
	 *         otherwise.
	 */
	public boolean addAll(Playlist playList) {
		return this.list.addAll(playList.list);
	}

	/**
	 * <p>
	 * Adds all <code>Song</code>s contained in the specified
	 * <code>ArrayList</code> to the end of this <code>Playlist</code>, in
	 * the order in which they appear in the specified <code>ArrayList</code>.
	 * <code>null</code> elements contained in the specified
	 * <code>ArrayList</code> <b>MUST NOT</b> be added to this
	 * <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * This method returns <code>true</code> if this <code>Playlist</code>
	 * object has changed as a result of calling this method, <code>false</code>
	 * otherwise. Because a <code>Playlist</code> cannot contain
	 * <code>null</code> elements, supports duplicate elements, and its size
	 * is limited only by the memory available to the Java Virtual Machine, the
	 * only way a <code>Playlist</code> cannot change when this method is
	 * called is if the specified <code>ArrayList</code> contains no
	 * <code>Song</code>s (that is, calling the <code>size()</code> method
	 * on it returns <code>0</code>), or all the <code>Song</code>s it
	 * contains are in fact <code>null</code> elements.
	 * </p>
	 * 
	 * <p>
	 * Subsequent changes to this <code>Playlist</code> <b>MUST NOT</b>
	 * affect the specified <code>ArrayList</code>, and subsequent changes to
	 * the specified <code>ArrayList</code> <b>MUST NOT</b> affect this
	 * <code>Playlist</code> either.
	 * </p>
	 * 
	 * @param songList
	 *            The <code>ArrayList</code> containing the <code>Song</code>s
	 *            to be added to this <code>Playlist</code>.
	 * @return <code>true</code> if this <code>Playlist</code> object has
	 *         changed as a result of calling this method, <code>false</code>
	 *         otherwise.
	 */
	public boolean addAll(ArrayList<Song> songList) {
		boolean result;
		boolean addResult;

		result = false;
		for (Song song : songList) {
			if (song != null) {
				addResult = this.list.add(song);
				result = result || addResult;
			}
		}
		return result;
	}

	/**
	 * <p>
	 * Adds all <code>Song</code>s contained in the specified array of
	 * <code>Song</code>s to the end of this <code>Playlist</code>, in the
	 * order in which they appear in the specified array. <code>null</code>
	 * elements contained in the specified array <b>MUST NOT</b> be added to
	 * this <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * This method returns <code>true</code> if this <code>Playlist</code>
	 * object has changed as a result of calling this method, <code>false</code>
	 * otherwise. Because a <code>Playlist</code> cannot contain
	 * <code>null</code> elements, supports duplicate elements, and its size
	 * is limited only by the memory available to the Java Virtual Machine, the
	 * only way a <code>Playlist</code> cannot change when this method is
	 * called is if the specified array is <code>null</code>, contains no
	 * <code>Song</code>s (that is, its <code>length</code> is
	 * <code>0</code>, or all the <code>Song</code>s it contains are in
	 * fact <code>null</code> elements.
	 * </p>
	 * 
	 * <p>
	 * Subsequent changes to this <code>Playlist</code> <b>MUST NOT</b>
	 * affect the specified array of <code>Song</code>s, and subsequent
	 * changes to the specified array of <code>Song</code>s <b>MUST NOT</b>
	 * affect this <code>Playlist</code> either.
	 * </p>
	 * 
	 * @param songArray
	 *            The array of <code>Song</code>s containing the
	 *            <code>Song</code>s to be added to this
	 *            <code>Playlist</code>.
	 * @return <code>true</code> if this <code>Playlist</code> object has
	 *         changed as a result of calling this method, <code>false</code>
	 *         otherwise.
	 */
	public boolean addAll(Song[] songArray) {
		boolean result;
		boolean addResult;

		result = false;
		if (songArray != null) {
			for (Song song : songArray) {
				if (song != null) {
					addResult = this.list.add(song);
					result = result || addResult;
				}
			}
		}
		return result;
	}

	/**
	 * <p>
	 * Inserts the specified <song>Song</code> at the specified position in
	 * this list, and shifts the <code>Song</code> currently at that position
	 * (if any) and any subsequent elements to the right (in other words, it
	 * adds one to their indices). If the specified <code>Song</code> is
	 * <code>null</code>, this method does nothing and <code>Playlist</code>
	 * does not change.
	 * </p>
	 * 
	 * <p>
	 * For example, if a <code>Song</code> is inserted at position <code>3</code>
	 * of a <code>Playlist</code> containing 6 <code>Song</code>s, the
	 * <code>Song</code> currently at position <code>3</code> is moved to
	 * position <code>4</code>, the <code>Song</code> currently at position
	 * <code>4</code> is moved to position <code>5</code>, and a new
	 * position <code>6</code> is created to store the <code>Song</code>
	 * currently at position <code>5</code>.
	 * </p>
	 * 
	 * <p>
	 * Note that valid positions for this method include the position equal to
	 * the size of the <code>Playlist</code>. In this case, the specified
	 * <code>Song</code> is added at the end of the <code>Playlist</code>;
	 * this particular case is equivalent to calling the <code>add(Song song)</code>
	 * method.
	 * </p>
	 * 
	 * @param index
	 *            The position in the <code>Playlist</code> in which the
	 *            specified <code>Song</code> is to be added.
	 * @param song
	 *            The <code>Song</code> to be added to the <code>Playlist</code>.
	 * @throws IndexOutOfBoundsException
	 *             if <code>index</code> is out of range (less than <code>0</code>
	 *             or greater than the size of this <code>Playlist</code>)
	 */
	public void add(int index, Song song) {
		if (song != null) {
			this.list.add(index, song);
		}
	}

	/**
	 * <p>
	 * Returns <code>true</code> if this <code>Playlist</code> is empty,
	 * that is, it contains no <code>Song</code>s.
	 * </p>
	 * 
	 * @return <code>true</code> if this <code>Playlist</code> is empty,
	 *         <code>false</code> otherwise.
	 */
	public boolean isEmpty() {
		return this.list.isEmpty();
	}

	/**
	 * <p>
	 * Returns the <code>Song</code> at the specified position in this
	 * <code>Playlist</code>.
	 * </p>
	 * 
	 * @param index
	 *            The position in the <code>Playlist</code> of the
	 *            <code>Song</code> to return.
	 * @return The <code>Song</code> at the position specified by
	 *         <code>index</code> in this <code>Playlist</code>.
	 * @throws IndexOutOfBoundsException
	 *             if <code>index</code> is out of range (less than
	 *             <code>0</code> or greater than or equal to the size of this
	 *             <code>Playlist</code>).
	 */
	public Song get(int index) {
		return this.list.get(index);
	}

	/**
	 * <p>
	 * Removes the <code>Song</code> element at the specified position in this
	 * list, and shifts any subsequent <code>Song</code> (if any) to the left
	 * (that is, it subtracts one from their indices).
	 * </p>
	 * 
	 * <p>
	 * For example, if a <code>Song</code> is removed from position
	 * <code>3</code> of a <code>Playlist</code> containing 6
	 * <code>Song</code>s, the <code>Song</code> currently at position
	 * <code>4</code> is moved to position <code>3</code>, and the
	 * <code>Song</code> currently at position <code>5</code> is moved to
	 * position <code>4</code>.
	 * </p>
	 * 
	 * @param index
	 *            The position in this <code>Playlist</code> containing the
	 *            <code>Song</code> to be removed.
	 * @return The <code>Song</code> that was removed from the list.
	 * @throws IndexOutOfBoundsException
	 *             if <code>index</code> is out of range (less than
	 *             <code>0</code> or greater than or equal to the size of this
	 *             <code>Playlist</code>).
	 */
	public Song remove(int index) {
		return this.list.remove(index);
	}

	/**
	 * <p>
	 * Moves the <code>Song</code> at the specified position in the
	 * <code>Playlist</code> a specified number of positions towards the
	 * beginning of the <code>Playlist</code>. Any <code>Song</code>s
	 * occurring in positions between the specified position and the position to
	 * which the <code>Song</code> at the specified position is to be moved
	 * will be shifted one position towards the end of the <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * For example, if the <code>Song</code> at position <code>4</code> of a
	 * <code>Playlist</code> containing 6 <code>Songs</code> is moved 2
	 * positions towards the beginning of the <code>Playlist</code>, the
	 * <code>Song</code> at position <code>4</code> is moved into position
	 * <code>2</code>, the <code>Song</code> formerly in position
	 * <code>2</code> is moved into position <code>3</code>, and the
	 * <code>Song</code> formerly into position <code>3</code> is moved into
	 * position <code>4</code>.
	 * </p>
	 * 
	 * @param index
	 *            The position in the <code>Playlist</code> of the
	 *            <code>Song</code> to be moved.
	 * @param numberPositions
	 *            The number of positions the <code>Song</code> at
	 *            <code>index</code> should be moved.
	 * @throws IndexOutOfBoundsException
	 *             if either <code>index</code> or the position to which the
	 *             <code>Song</code> should move is out of range (less than
	 *             <code>0</code> or greater than or equal to the size of this
	 *             <code>Playlist</code>).
	 * @throws IllegalArgumentException
	 *             if <code>numberPositions</code> is <code>0</code> or
	 *             negative.
	 */
	public void moveUp(int index, int numberPositions) {
		Song holder;
		int destinationIndex;

		if (numberPositions <= 0) {
			throw new IllegalArgumentException("" + numberPositions);
		}
		
		// Necessary to preserve atomicity of operation
		destinationIndex = index - numberPositions;
		if (destinationIndex < 0) {
			throw new ArrayIndexOutOfBoundsException(destinationIndex);
		}
		

		holder = this.list.remove(index);
		this.list.add(destinationIndex, holder);
	}

	/**
	 * <p>
	 * Moves the <code>Song</code> at the specified position in the
	 * <code>Playlist</code> a specified number of positions towards the end
	 * of the <code>Playlist</code>. Any <code>Song</code>s occurring in
	 * positions between the specified position and the position to which the
	 * <code>Song</code> at the specified position is to be moved will be
	 * shifted one position towards the beginning of the <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * For example, if the <code>Song</code> at position <code>2</code> of a
	 * <code>Playlist</code> containing 6 <code>Songs</code> is moved 2
	 * positions towards the end of the <code>Playlist</code>, the
	 * <code>Song</code> at position <code>2</code> is moved into position
	 * <code>4</code>, the <code>Song</code> formerly in position
	 * <code>4</code> is moved into position <code>3</code>, and the
	 * <code>Song</code> formerly into position <code>3</code> is moved into
	 * position <code>2</code>.
	 * </p>
	 * 
	 * @param index
	 *            The position in the <code>Playlist</code> of the
	 *            <code>Song</code> to be moved.
	 * @param numberPositions
	 *            The number of positions the <code>Song</code> at
	 *            <code>index</code> should be moved.
	 * @throws IndexOutOfBoundsException
	 *             if either <code>index</code> or the position the
	 *             <code>Song</code> should move is out of range (less than
	 *             <code>0</code> or greater than or equal to the size of this
	 *             <code>Playlist</code>).
	 * @throws IllegalArgumentException
	 *             if <code>numberPositions</code> is <code>0</code> or
	 *             negative.
	 * 
	 */
	public void moveDown(int index, int numberPositions) {
		Song holder;
		int destinationIndex;

		if (numberPositions <= 0) {
			throw new IllegalArgumentException("" + numberPositions);
		}
		
		// Necessary to preserve atomicity of operation
		destinationIndex = index + numberPositions;
		if (destinationIndex >= this.list.size()) {
			throw new ArrayIndexOutOfBoundsException(destinationIndex);
		}

		holder = this.list.remove(index);
		this.list.add(destinationIndex, holder);
	}

	/**
	 * <p>
	 * Loads the <code>Songs</code> whose paths are stored in the file given
	 * by the specified <code>File</code> (the playlist file) into the
	 * <code>Playlist</code>.
	 * </p>
	 * 
	 * <p>
	 * This method works as follows:
	 * <ol>
	 * <li>First, this <code>Playlist</code> is cleared (that is, made
	 * empty).</li>
	 * <li>Then, the playlist file is read, line by line. Each non-empty line
	 * in the playlist file represents the full (absolute) path of a file
	 * containing a digital audio track (empty lines are ignored). The specified
	 * <code>MusicCollection</code> is queried to see if it contains a
	 * <code>Song</code> associated to the file whose path was read from the
	 * playlist file.
	 * <ul>
	 * <li>If the specified <code>MusicCollection</code> does contain such a
	 * <code>Song</code>, the latter is added to the end of this
	 * <code>Playlist</code>.</li>
	 * <li>If the specified <code>MusicCollection</code> does not contain
	 * such a <code>Song</code>, an attempt is made to create a
	 * <code>Song</code> object associated to the file whose path was read
	 * from the playlist file by calling the <code>createSong()</code> method
	 * on a <code>SongFactory</code> object.</li>
	 * If the attempt is successful, the <code>Song</code> object is added to
	 * the specified <code>MusicCollection</code>, as well as to the end of
	 * this <code>Playlist</code>. Otherwise, the path of the file is added
	 * to an <code>ArrayList</code> of <code>String</code> objects
	 * containing all paths present in the playlist file for which a
	 * <code>Song</code> object could not be obtained.</li>
	 * </ul>
	 * </li>
	 * <li>When all the lines have been read from the playlist file, the latter
	 * is closed.</li>
	 * </ol>
	 * </p>
	 * 
	 * <p>
	 * Note that after the paths have been read from the playlist file, the
	 * <code>Song</code>s <b>MUST</b> appear in the <code>Playlist</code>
	 * in the same order as their paths appear in the playlist file.
	 * </p>
	 * 
	 * @param path
	 *            The path of a playlist file
	 * @param collection
	 *            A <code>MusicCollection</code> from which <code>Song</code>s
	 *            are to be retrieved, and to which <code>Song</code>s are to
	 *            be added.
	 * @return The list of all the paths for which a corresponding
	 *         <code>Song</code> object could not be obtained. The list is
	 *         empty if a <code>Song</code> object was successfully obtained
	 *         for all paths in the playlist file.
	 * @throws IOException
	 *             if there is an error reading the playlist file (for example,
	 *             it does not exist).
	 */
	public ArrayList<String> load(File path, MusicCollection collection)
			throws IOException {
		ArrayList<String> loadingErrors;
		Scanner reader;
		String songPath;
		Song song;

		reader = new Scanner(path);
		this.clear();
		loadingErrors = new ArrayList<String>();
		while (reader.hasNextLine()) {
			songPath = reader.nextLine();
			if (songPath.length() > 0) {
				song = collection.getSong(new File(songPath));
				if (song == null) {
					try {
						song = this.factory.createSong(songPath);
						collection.add(song);
					} catch (IOException exception) {
						loadingErrors.add(songPath);
					}
				}
				if (song != null) {
					this.list.add(song);
				}
			}
		}
		reader.close();

		return loadingErrors;
	}

	/**
	 * <p>
	 * Writes the full (absolute) path of each <code>Song</code> contained in
	 * this <code>Playlist</code> to the file whose path is given by the
	 * specified <code>File</code> (the playlist file). If the playlist file
	 * already exists, it is overwritten.
	 * </p>
	 * 
	 * <p>
	 * In the playlist file, the path of each <code>Song</code> is written on
	 * a separate line. The paths of the <code>Song</code>s <b>MUST</b> be
	 * written to the file in the order that they appear in this
	 * <code>Playlist</code>. It is acceptable to write empty lines to the
	 * file.
	 * </p>
	 * 
	 * <p>
	 * Also note that this <code>Playlist</code> <b>MUST NOT</b> change as a
	 * result of calling this method. The playlist file <code>MUST</code> be
	 * closed once the paths of all the <code>Song</code>s in this
	 * <code>Playlist</code> have been written.
	 * </p>
	 * 
	 * @param path
	 *            The path of the playlist file to which the paths of the
	 *            <code>Song</code>s contained in this <code>Playlist</code>
	 *            will be saved.
	 * @throws IOException
	 *             if there is an error writing to the playlist file.
	 */
	public void save(File path) throws IOException {
		PrintStream writer;
		Song song;
		int numberSongs;

		writer = new PrintStream(path);
		numberSongs = this.list.size();
		for (int i = 0; i < numberSongs; i++) {
			song = this.list.get(i);
			writer.println(song.getFilePath().getAbsolutePath());
		}
		writer.close();
	}

	/**
	 * <p>
	 * Returns the number of <code>Song</code>s in this <code>Playlist</code>.
	 * </p>
	 * 
	 * @return the number of <code>Song</code>s in this <code>Playlist</code>.
	 */
	public int getSize() {
		return this.list.size();
	}

	/**
	 * <p>
	 * Sorts the <code>Song</code>s in this <code>Playlist</code> so that
	 * they appear in the order implemented by the specified
	 * <code>SongComparator</code> object.
	 * </p>
	 * 
	 * <p>
	 * For example, if the specified <code>SongComparator</code> object
	 * implements an order based on the album in which each <code>Song</code>
	 * appears (lexicographically increasing, lexicographically decreasing, or
	 * any other order), then after the method is called, the <code>Song</code>s
	 * in this <code>Playlist</code> will appear in that order.
	 * </p>
	 * 
	 * @param comparator
	 *            The <code>SongComparator</code> performing the comparisons
	 *            according to which the <code>Song</code>s contained in this
	 *            <code>Playlist</code> will be sorted.
	 */
	public void sort(SongComparator comparator) {
		this.sort(comparator, this.list.size());
	}
}