Source: lib/queue.js

var _ = require('lodash')
var sort = require('./sort')

/**
 * Create a queue of playlist entries.
 * @constructor
 * @param {Array} [arr] - An array of playlist entries.
 */
function Queue (arr) {
  /**
   * Array of entries.
   */
  this.queue = []

  if (arr) {
    this.queue = arr
  }
}

/**
 * Add an entry to the end of the queue.
 * @param {Track | Album | Artist} entry -
 * The entry to add.
 */
Queue.prototype.add = function (entry) {
  this.queue.push(entry)
}

/**
 * Group entries and interleave them.
 * @param {Function} fn - A grouping function.
 * Takes an entry as input and returns a grouping key,
 * a string, as output.
 * @return {Queue} - Itself.
 */
Queue.prototype.alternate = function (fn) {
  this.groupBy(fn)
  return this.interleave()
}

/**
 * Concatenate with another queue.
 * @param {Queue} queue - Another queue to append to this queue.
 * @return {Queue} - A new queue containing all the entries
 * from this queue followed by all the entries from the other queue.
 */
Queue.prototype.concat = function (queue) {
  return new Queue(this.toArray().concat(queue.toArray()))
}

/**
 * Whether the queue contains an entry.
 * @param {Track | Album | Artist} entry -
 * The entry to check for.
 * @return {boolean} - `true` if the queue contains `entry`,
 * `false` otherwise.
 */
Queue.prototype.contains = function (obj) {
  return this.indexOf(obj) >= 0
}

/**
 * Remove duplicate entries.
 * If a track occurs more than once, the version with
 * the highest Spotify popularity is preferred.
 * @return {Promise | Queue} - Itself.
 */
Queue.prototype.dedup = function () {
  var self = this
  var result = new Queue()
  return self.forEachPromise(function (entry) {
    if (!result.contains(entry)) {
      result.add(entry)
      return Promise.resolve(entry)
    } else {
      var idx = self.indexOf(entry)
      var other = result.get(idx)
      if (entry.equals(other)) {
        return Promise.resolve(other)
      } else {
        return other.getPopularity().then(function (otherPopularity) {
          return entry.getPopularity().then(function (entryPopularity) {
            if (entryPopularity > otherPopularity) {
              Queue.set(idx, entry)
            }
          })
        })
      }
    }
  }).then(function () {
    self.queue = result.toArray()
    return self
  })
}

/**
 * Dispatch all entries in sequence.
 * Ensure that only one entry is dispatched at a time.
 * @return {Promise | Queue} A queue of results.
 */
Queue.prototype.dispatch = function () {
  return this.forEachPromise(function (entry) {
    return entry.dispatch()
  })
}

/**
 * Filter the queue by a predicate.
 * @param {Function} fn - A predicate function.
 * Takes the current entry as input and returns
 * `true` if it passes the test, `false` otherwise.
 * @return {Queue} - A new queue.
 */
Queue.prototype.filter = function (fn) {
  return new Queue(this.toArray().filter(fn))
}

/**
 * Transform a nested queue into a flat queue.
 * `[1, [2, [3, [4]], 5]] => [1, 2, 3, 4, 5]`.
 * @return {Queue} - Itself.
 */
Queue.prototype.flatten = function () {
  this.queue = _.flattenDeep(this.toArray())
  return this
}

/**
 * Iterate over the queue.
 * @param {Function} fn - An iterator function.
 * Takes the current entry as input and returns
 * the modified value as output.
 * @return {Queue} - Itself.
 */
Queue.prototype.forEach = function (fn) {
  this.queue.forEach(fn)
  return this
}

/**
 * Similar to Queue.forEach(), but with promises.
 * Each iteration must return a promise
 * (e.g., by invoking a promise-returning method).
 *
 * The execution is strictly sequential to prevent overloading
 * the server. (If parallel execution is needed, use a library
 * function like Promise.all() instead.)
 *
 * @param {Function} fn - An iterator function.
 * Takes an entry as input and returns a promise.
 * @return {Promise | Queue} A promise that invokes
 * each promise in sequence. The promise's value is
 * a queue of the values of each invoked promise.
 */
Queue.prototype.forEachPromise = function (fn) {
  var result = new Queue()
  var ready = Promise.resolve(null)
  this.queue.forEach(function (entry) {
    ready = ready.then(function () {
      return fn(entry)
    }).then(function (value) {
      result.add(value)
    }).catch(function () { })
  })
  return ready.then(function () {
    return result
  })
}

/**
 * Get a playlist entry.
 * @param {integer} idx - The index of the entry.
 * The indices start at 0.
 */
Queue.prototype.get = function (idx) {
  return this.queue[idx]
}

/**
 * Group entries.
 * @param {Function} fn - A grouping function.
 * Takes an entry as input and returns a grouping key,
 * a string, as output.
 * @return {Queue} - Itself.
 */
Queue.prototype.group = function (fn) {
  this.groupBy(fn)
  return this.flatten()
}

/**
 * Group entries.
 * @param {Function} fn - A grouping function.
 * Takes an entry as input and returns a grouping key,
 * a string, as output.
 * @return {Queue} - Itself.
 */
Queue.prototype.groupBy = function (fn) {
  var result = _.groupBy(this.queue, fn)
  this.queue = _.forEach(result, _.identity)
  return this
}

/**
 * Get the index of an entry.
 * @param {Object} obj - The entry to find.
 * @return The index of `obj`, or `-1` if not found.
 * The indices start at 0.
 */
Queue.prototype.indexOf = function (obj) {
  for (var i in this.queue) {
    var entry = this.queue[i]
    if (entry === obj ||
        (entry && entry.similarTo &&
         obj && obj.similarTo &&
         entry.similarTo(obj))) {
      return i
    }
  }
  return -1
}

/**
 * Interleave a nested queue into a flat queue.
 * @return {Queue} - Itself.
 */
Queue.prototype.interleave = function () {
  this.queue = _.compact(_.flatten(_.zip.apply(null, this.toArray())))
  return this
}

/**
 * Whether the playlist is empty.
 * @return {boolean} - `true` if empty, `false` otherwise.
 */
Queue.prototype.isEmpty = function () {
  return this.size() <= 0
}

/**
 * Map a function over the queue.
 * @param {Function} fn - An iterator function.
 * Takes the current entry as input and returns
 * the modified value as output.
 * @return {Queue} - A new queue.
 */
Queue.prototype.map = function (fn) {
  return new Queue(this.toArray().map(fn))
}

/**
 * Reverse the order of the queue.
 * @return {Queue} - Itself.
 */
Queue.prototype.reverse = function () {
  this.queue.reverse()
  return this
}

/**
 * Set a playlist entry.
 * @param {integer} idx - The index of the entry.
 * The indices start at 0. If out of bounds,
 * the entry is added at the end.
 * @param {Object} entry - The entry to add.
 */
Queue.prototype.set = function (idx, entry) {
  if (idx < 0 || idx >= this.size()) {
    this.add(entry)
  } else {
    this.queue[idx] = entry
  }
}

/**
 * The playlist size.
 * @return {integer} - The number of entries.
 */
Queue.prototype.size = function () {
  return this.queue.length
}

/**
 * Slice a queue.
 * @param {integer} start - The index of the first element.
 * @param {integer} end - The index of the last element (not included).
 * @return {Queue} - A new queue containing all elements
 * from `start` (inclusive) to `end` (exclusive).
 */
Queue.prototype.slice = function (start, end) {
  return new Queue(this.toArray().slice(start, end))
}

/**
 * Sort the queue.
 * @param {Function} fn - A sorting function.
 * Takes two entries as input and returns
 * `-1` if the first entry is less than the second,
 * `1` if the first entry is greater than the second, and
 * `0` if the entries are equal.
 * @return {Queue} - Itself.
 */
Queue.prototype.sort = function (fn) {
  sort(this.queue, fn)
  return this
}

/**
 * Order the playlist entries by Last.fm playcount.
 * @return {Queue} - Itself.
 */
Queue.prototype.orderByLastfm = function () {
  return this.sort(sort.lastfm)
}

/**
 * Order the playlist entries by Spotify popularity.
 * @return {Queue} - Itself.
 */
Queue.prototype.orderByPopularity = function () {
  return this.sort(sort.popularity)
}

/**
 * Remove the first element from the queue.
 * @return {Object} - The first element, or `undefined`
 * if the queue is empty.
 */
Queue.prototype.shift = function () {
  return this.queue.shift()
}

/**
 * Shuffle the elements in the queue.
 * Uses the Fisher-Yates algorithm.
 * @return {Queue} - Itself.
 */
Queue.prototype.shuffle = function () {
  this.queue = _.shuffle(this.toArray())
  return this
}

/**
 * Convert queue to array.
 * @return {Array} An array of playlist entries.
 */
Queue.prototype.toArray = function () {
  var result = []
  for (var i in this.queue) {
    var entry = this.queue[i]
    if (entry instanceof Queue) {
      entry = entry.toArray()
    }
    result.push(entry)
  }
  return result
}

module.exports = Queue