content: /*
   This file is part of Airsonic.
  
   Airsonic 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.
  
   Airsonic 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 Airsonic.  If not, see <http://www.gnu.org/licenses/>.
  
   Copyright 2016 (C) Airsonic Authors
   Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
   */
  package org.airsonic.player.domain;
  
  import org.apache.commons.lang.StringUtils;
  
  import java.util.*;
  
  /**
   * A play queue is a list of music files that are associated to a remote player.
   *
   * @author Sindre Mehus
   */
  public class PlayQueue {
  
      private List<MediaFile> files = new ArrayList<MediaFile>();
      private boolean repeatEnabled;
      private String name = "(unnamed)";
      private Status status = Status.PLAYING;
  
      private RandomSearchCriteria randomSearchCriteria;
      private InternetRadio internetRadio;
  
      /**
       * The index of the current song, or -1 is the end of the playlist is reached.
       * Note that both the index and the playlist size can be zero.
       */
      private int index = 0;
  
      /**
       * Used for undo functionality.
       */
      private List<MediaFile> filesBackup = new ArrayList<MediaFile>();
      private int indexBackup = 0;
  
      /**
       * Returns the user-defined name of the playlist.
       *
       * @return The name of the playlist, or <code>null</code> if no name has been assigned.
       */
      public synchronized String getName() {
          return name;
      }
  
      /**
       * Sets the user-defined name of the playlist.
       *
       * @param name The name of the playlist.
       */
      public synchronized void setName(String name) {
          this.name = name;
      }
  
      /**
       * Returns the current song in the playlist.
       *
       * @return The current song in the playlist, or <code>null</code> if no current song exists.
       */
      public synchronized MediaFile getCurrentFile() {
          if (index == -1 || index == 0 && size() == 0) {
              setStatus(Status.STOPPED);
              return null;
          } else {
              MediaFile file = files.get(index);
  
              // Remove file from playlist if it doesn't exist.
              if (!file.exists()) {
                  files.remove(index);
                  index = Math.max(0, Math.min(index, size() - 1));
                  return getCurrentFile();
              }
  
              return file;
          }
      }
  
      /**
       * Returns all music files in the playlist.
       *
       * @return All music files in the playlist.
       */
      public synchronized List<MediaFile> getFiles() {
          return files;
      }
  
      /**
       * Returns the music file at the given index.
       *
       * @param index The index.
       * @return The music file at the given index.
       * @throws IndexOutOfBoundsException If the index is out of range.
       */
      public synchronized MediaFile getFile(int index) {
          return files.get(index);
      }
  
      /**
       * Skip to the next song in the playlist.
       */
      public synchronized void next() {
          index++;
  
          // Reached the end?
          if (index >= size()) {
              index = isRepeatEnabled() ? 0 : -1;
          }
      }
  
      /**
       * Returns the number of songs in the playlists.
       *
       * @return The number of songs in the playlists.
       */
      public synchronized int size() {
          return files.size();
      }
  
      /**
       * Returns whether the playlist is empty.
       *
       * @return Whether the playlist is empty.
       */
      public synchronized boolean isEmpty() {
          return files.isEmpty();
      }
  
      /**
       * Returns the index of the current song.
       *
       * @return The index of the current song, or -1 if the end of the playlist is reached.
       */
      public synchronized int getIndex() {
          return index;
      }
  
      /**
       * Sets the index of the current song.
       *
       * @param index The index of the current song.
       */
      public synchronized void setIndex(int index) {
          makeBackup();
          this.index = Math.max(0, Math.min(index, size() - 1));
          setStatus(Status.PLAYING);
      }
  
      /**
       * Adds one or more music file to the playlist.
       *
       * @param mediaFiles The music files to add.
       * @param index Where to add them.
       */
      public synchronized void addFilesAt(Iterable<MediaFile> mediaFiles, int index) {
          makeBackup();
          for (MediaFile mediaFile : mediaFiles) {
              files.add(index, mediaFile);
              index++;
          }
          setStatus(Status.PLAYING);
      }
  
      /**
       * Adds one or more music file to the playlist.
       *
       * @param append     Whether existing songs in the playlist should be kept.
       * @param mediaFiles The music files to add.
       */
      public synchronized void addFiles(boolean append, Iterable<MediaFile> mediaFiles) {
          makeBackup();
          if (!append) {
              index = 0;
              files.clear();
          }
          for (MediaFile mediaFile : mediaFiles) {
              files.add(mediaFile);
          }
          setStatus(Status.PLAYING);
      }
  
      /**
       * Convenience method, equivalent to {@link #addFiles(boolean, Iterable)}.
       */
      public synchronized void addFiles(boolean append, MediaFile... mediaFiles) {
          addFiles(append, Arrays.asList(mediaFiles));
      }
  
      /**
       * Removes the music file at the given index.
       *
       * @param index The playlist index.
       */
      public synchronized void removeFileAt(int index) {
          makeBackup();
          index = Math.max(0, Math.min(index, size() - 1));
          if (this.index > index) {
              this.index--;
          }
          files.remove(index);
  
          this.index = Math.max(0, Math.min(this.index, size() - 1));
      }
  
      /**
       * Clears the playlist.
       */
      public synchronized void clear() {
          makeBackup();
          files.clear();
          setRandomSearchCriteria(null);
          setInternetRadio(null);
          index = 0;
      }
  
      /**
       * Shuffles the playlist.
       */
      public synchronized void shuffle() {
          makeBackup();
          MediaFile currentFile = getCurrentFile();
          Collections.shuffle(files);
          if (currentFile != null) {
              Collections.swap(files, files.indexOf(currentFile), 0);
              index = 0;
          }
      }
  
      /**
       * Sorts the playlist according to the given sort order.
       */
      public synchronized void sort(final SortOrder sortOrder) {
          makeBackup();
          MediaFile currentFile = getCurrentFile();
  
          Comparator<MediaFile> comparator = (a, b) -> {
              switch (sortOrder) {
                  case TRACK:
                      Integer trackA = a.getTrackNumber();
                      Integer trackB = b.getTrackNumber();
                      if (trackA == null) {
                          trackA = 0;
                      }
                      if (trackB == null) {
                          trackB = 0;
                      }
                      return trackA.compareTo(trackB);
  
                  case ARTIST:
                      String artistA = StringUtils.trimToEmpty(a.getArtist());
                      String artistB = StringUtils.trimToEmpty(b.getArtist());
                      return artistA.compareTo(artistB);
  
                  case ALBUM:
                      String albumA = StringUtils.trimToEmpty(a.getAlbumName());
                      String albumB = StringUtils.trimToEmpty(b.getAlbumName());
                      return albumA.compareTo(albumB);
                  default:
                      return 0;
              }
          };
  
          files.sort(comparator);
          if (currentFile != null) {
              index = files.indexOf(currentFile);
          }
      }
  
      /**
       * Rearranges the playlist using the provided indexes.
       */
      public synchronized void rearrange(int[] indexes) {
          makeBackup();
          if (indexes == null || indexes.length != size()) {
              return;
          }
  
          MediaFile[] newFiles = new MediaFile[files.size()];
          for (int i = 0; i < indexes.length; i++) {
              newFiles[i] = files.get(indexes[i]);
          }
          for (int i = 0; i < indexes.length; i++) {
              if (index == indexes[i]) {
                  index = i;
                  break;
              }
          }
  
          files.clear();
          files.addAll(Arrays.asList(newFiles));
      }
  
      /**
       * Moves the song at the given index one step up.
       *
       * @param index The playlist index.
       */
      public synchronized void moveUp(int index) {
          moveDown(index - 1);
      }
  
      /**
       * Moves the song at the given index one step down.
       *
       * @param index The playlist index.
       */
      public synchronized void moveDown(int index) {
          makeBackup();
          if (index < 0 || index >= size() - 1) {
              return;
          }
          Collections.swap(files, index, index + 1);
  
          if (this.index == index) {
              this.index++;
          } else if (this.index == index + 1) {
              this.index--;
          }
      }
  
      /**
       * Returns whether the playlist is repeating.
       *
       * @return Whether the playlist is repeating.
       */
      public synchronized boolean isRepeatEnabled() {
          return repeatEnabled;
      }
  
      /**
       * Sets whether the playlist is repeating.
       *
       * @param repeatEnabled Whether the playlist is repeating.
       */
      public synchronized void setRepeatEnabled(boolean repeatEnabled) {
          this.repeatEnabled = repeatEnabled;
      }
  
      /**
       * Returns whether the play queue is in shuffle radio mode.
       *
       * @return Whether the play queue is a shuffle radio mode.
       */
      public synchronized boolean isShuffleRadioEnabled() {
          return this.randomSearchCriteria != null;
      }
  
      /**
       * Returns whether the play queue is a internet radio mode.
       *
       * @return Whether the play queue is a internet radio mode.
       */
      public synchronized boolean isInternetRadioEnabled() {
          return this.internetRadio != null;
      }
  
      /**
       * Revert the last operation.
       */
      public synchronized void undo() {
          List<MediaFile> filesTmp = new ArrayList<MediaFile>(files);
          int indexTmp = index;
  
          index = indexBackup;
          files = filesBackup;
  
          indexBackup = indexTmp;
          filesBackup = filesTmp;
      }
  
      /**
       * Returns the playlist status.
       *
       * @return The playlist status.
       */
      public synchronized Status getStatus() {
          return status;
      }
  
      /**
       * Sets the playlist status.
       *
       * @param status The playlist status.
       */
      public synchronized void setStatus(Status status) {
          this.status = status;
          if (index == -1) {
              index = Math.max(0, Math.min(index, size() - 1));
          }
      }
  
      /**
       * Sets the current internet radio
       *
       * @param internetRadio An internet radio, or <code>null</code> if this is not an internet radio playlist
       */
      public void setInternetRadio(InternetRadio internetRadio) {
          this.internetRadio = internetRadio;
      }
  
      /**
       * Gets the current internet radio
       *
       * @return The current internet radio, or <code>null</code> if this is not an internet radio playlist
       */
      public InternetRadio getInternetRadio() {
          return internetRadio;
      }
  
      /**
       * Returns the criteria used to generate this random playlist
       *
       * @return The search criteria, or <code>null</code> if this is not a random playlist.
       */
      public synchronized RandomSearchCriteria getRandomSearchCriteria() {
          return randomSearchCriteria;
      }
  
      /**
       * Sets the criteria used to generate this random playlist
       *
       * @param randomSearchCriteria The search criteria, or <code>null</code> if this is not a random playlist.
       */
      public synchronized void setRandomSearchCriteria(RandomSearchCriteria randomSearchCriteria) {
          this.randomSearchCriteria = randomSearchCriteria;
      }
  
      /**
       * Returns the total length in bytes.
       *
       * @return The total length in bytes.
       */
      public synchronized long length() {
          long length = 0;
          for (MediaFile mediaFile : files) {
              length += mediaFile.getFileSize();
          }
          return length;
      }
  
      private void makeBackup() {
          filesBackup = new ArrayList<MediaFile>(files);
          indexBackup = index;
      }
  
      /**
       * Playlist status.
       */
      public enum Status {
          PLAYING,
          STOPPED
      }
  
      /**
       * Playlist sort order.
       */
      public enum SortOrder {
          TRACK,
          ARTIST,
          ALBUM
      }
  }
  