'use strict';

var AuthenticationRequest = require('./authentication-request'),
  WebApiRequest = require('./webapi-request'),
  HttpManager = require('./http-manager');

function SpotifyWebApi(credentials) {
  this._credentials = credentials || {};
}

SpotifyWebApi.prototype = {
  setCredentials: function(credentials) {
    for (var key in credentials) {
      if (credentials.hasOwnProperty(key)) {
        this._credentials[key] = credentials[key];
      }
    }
  },

  getCredentials: function() {
    return this._credentials;
  },

  resetCredentials: function() {
    this._credentials = null;
  },

  setClientId: function(clientId) {
    this._setCredential('clientId', clientId);
  },

  setClientSecret: function(clientSecret) {
    this._setCredential('clientSecret', clientSecret);
  },

  setAccessToken: function(accessToken) {
    this._setCredential('accessToken', accessToken);
  },

  setRefreshToken: function(refreshToken) {
    this._setCredential('refreshToken', refreshToken);
  },

  setRedirectURI: function(redirectUri) {
    this._setCredential('redirectUri', redirectUri);
  },

  getRedirectURI: function() {
    return this._getCredential('redirectUri');
  },

  getClientId: function() {
    return this._getCredential('clientId');
  },

  getClientSecret: function() {
    return this._getCredential('clientSecret');
  },

  getAccessToken: function() {
    return this._getCredential('accessToken');
  },

  getRefreshToken: function() {
    return this._getCredential('refreshToken');
  },

  resetClientId: function() {
    this._resetCredential('clientId');
  },

  resetClientSecret: function() {
    this._resetCredential('clientSecret');
  },

  resetAccessToken: function() {
    this._resetCredential('accessToken');
  },

  resetRefreshToken: function() {
    this._resetCredential('refreshToken');
  },

  resetRedirectURI: function() {
    this._resetCredential('redirectUri');
  },

  _setCredential: function(credentialKey, value) {
    this._credentials = this._credentials || {};
    this._credentials[credentialKey] = value;
  },

  _getCredential: function(credentialKey) {
    if (!this._credentials) {
      return;
    } else {
      return this._credentials[credentialKey];
    }
  },

  _resetCredential: function(credentialKey) {
    if (!this._credentials) {
      return;
    } else {
      this._credentials[credentialKey] = null;
    }
  },

  /**
   * Look up a track.
   * @param {string} trackId The track's ID.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getTrack('3Qm86XLflmIXVm1wcwkgDK').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the track. Not returned if a callback is given.
   */
  getTrack: function(trackId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/tracks/' + trackId)
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Look up several tracks.
   * @param {string[]} trackIds The IDs of the artists.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the artists. Not returned if a callback is given.
   */
  getTracks: function(trackIds, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/tracks')
      .withQueryParameters(
        {
          ids: trackIds.join(',')
        },
        options
      )
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Look up an album.
   * @param {string} albumId The album's ID.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAlbum('0sNOF9WDwhWunNAHPD3Baj').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the album. Not returned if a callback is given.
   */
  getAlbum: function(albumId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/albums/' + albumId)
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Look up several albums.
   * @param {string[]} albumIds The IDs of the albums.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAlbums(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the albums. Not returned if a callback is given.
   */
  getAlbums: function(albumIds, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/albums')
      .withQueryParameters(
        {
          ids: albumIds.join(',')
        },
        options
      )
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Look up an artist.
   * @param {string} artistId The artist's ID.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example api.getArtist('1u7kkVrr14iBvrpYnZILJR').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the artist. Not returned if a callback is given.
   */
  getArtist: function(artistId, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/artists/' + artistId)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Look up several artists.
   * @param {string[]} artistIds The IDs of the artists.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the artists. Not returned if a callback is given.
   */
  getArtists: function(artistIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/artists')
      .withQueryParameters({
        ids: artistIds.join(',')
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Search for music entities of certain types.
   * @param {string} query The search query.
   * @param {string[]} types An array of item types to search across.
   * Valid types are: 'album', 'artist', 'playlist', 'track', 'show', and 'episode'.
   * @param {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example search('Abba', ['track', 'playlist'], { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  search: function(query, types, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/search/')
      .withQueryParameters(
        {
          type: types.join(','),
          q: query
        },
        options
      )
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Search for an album.
   * @param {string} query The search query.
   * @param {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example searchAlbums('Space Oddity', { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  searchAlbums: function(query, options, callback) {
    return this.search(query, ['album'], options, callback);
  },

  /**
   * Search for an artist.
   * @param {string} query The search query.
   * @param {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example searchArtists('David Bowie', { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  searchArtists: function(query, options, callback) {
    return this.search(query, ['artist'], options, callback);
  },

  /**
   * Search for a track.
   * @param {string} query The search query.
   * @param {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example searchTracks('Mr. Brightside', { limit : 3, offset : 2 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  searchTracks: function(query, options, callback) {
    return this.search(query, ['track'], options, callback);
  },

  /**
   * Search for playlists.
   * @param {string} query The search query.
   * @param {Object} options The possible options.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example searchPlaylists('workout', { limit : 1, offset : 0 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  searchPlaylists: function(query, options, callback) {
    return this.search(query, ['playlist'], options, callback);
  },

  /**
   * Get an artist's albums.
   * @param {string} artistId The artist's ID.
   * @options {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getArtistAlbums('0oSGxfWSnnOXhD2fKuz2Gy', { album_type : 'album', country : 'GB', limit : 2, offset : 5 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the albums
   *          for the given artist. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  getArtistAlbums: function(artistId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/artists/' + artistId + '/albums')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the tracks of an album.
   * @param albumId the album's ID.
   * @options {Object} [options] The possible options, e.g. limit.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *                    tracks in the album. The result is paginated. If the promise is rejected.
   *                    it contains an error object. Not returned if a callback is given.
   */
  getAlbumTracks: function(albumId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/albums/' + albumId + '/tracks')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get an artist's top tracks.
   * @param {string} artistId The artist's ID.
   * @param {string} country The country/territory where the tracks are most popular. (format: ISO 3166-1 alpha-2)
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          artist's top tracks in the given country. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  getArtistTopTracks: function(artistId, country, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/artists/' + artistId + '/top-tracks')
      .withQueryParameters({
        country: country
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get related artists.
   * @param {string} artistId The artist's ID.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getArtistRelatedArtists('0oSGxfWSnnOXhD2fKuz2Gy').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          related artists. If the promise is rejected, it contains an error object. Not returned if a callback is given.
   */
  getArtistRelatedArtists: function(artistId, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/artists/' + artistId + '/related-artists')
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get information about a user.
   * @param userId The user ID.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getUser('thelinmichael').then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object
   *          containing information about the user. If the promise is
   *          rejected, it contains an error object. Not returned if a callback is given.
   */
  getUser: function(userId, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/users/' + encodeURIComponent(userId))
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get information about the user that has signed in (the current user).
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getMe().then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object
   *          containing information about the user. The amount of information
   *          depends on the permissions given by the user. If the promise is
   *          rejected, it contains an error object. Not returned if a callback is given.
   */
  getMe: function(callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me')
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get a user's playlists.
   * @param {string} userId An optional id of the user. If you know the Spotify URI it is easy
   * to find the id (e.g. spotify:user:<here_is_the_id>). If not provided, the id of the user that granted
   * the permissions will be used.
   * @param {Object} [options] The options supplied to this request.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getUserPlaylists('thelinmichael').then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
   *          a list of playlists. If rejected, it contains an error object. Not returned if a callback is given.
   */
  getUserPlaylists: function(userId, options, callback) {
    var path;
    if (typeof userId === 'string') {
      path = '/v1/users/' + encodeURIComponent(userId) + '/playlists';
    } else if (typeof userId === 'object') {
      callback = options;
      options = userId;
      path = '/v1/me/playlists';
    } /* undefined */ else {
      path = '/v1/me/playlists';
    }

    return WebApiRequest.builder(this.getAccessToken())
      .withPath(path)
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get a playlist.
   * @param {string} playlistId The playlist's ID.
   * @param {Object} [options] The options supplied to this request.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getPlaylist('3EsfV6XzCHU8SPNdbnFogK').then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
   *          the playlist. If rejected, it contains an error object. Not returned if a callback is given.
   */
  getPlaylist: function(playlistId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId)
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get tracks in a playlist.
   * @param {string} playlistId The playlist's ID.
   * @param {Object} [options] Optional options, such as fields.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getPlaylistTracks('3ktAYNcRHpazJ9qecm3ptn').then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object that containing
   * the tracks in the playlist. If rejected, it contains an error object. Not returned if a callback is given.
   */
  getPlaylistTracks: function(playlistId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/tracks')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Create a playlist.
   * @param {string} [name] The name of the playlist.
   * @param {Object} [options] The possible options, being description, collaborative and public.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example createPlaylist('My playlist', {''description': 'My description', 'collaborative' : false, 'public': true}).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing information about the
   *          created playlist. If rejected, it contains an error object. Not returned if a callback is given.
   */
  createPlaylist: function(name, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/playlists')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters({
        name : name,
      }, options)
      .build()
      .execute(HttpManager.post, callback);
  },

  /**
   * Follow a playlist.
   * @param {string} playlistId The playlist's ID
   * @param {Object} [options] The possible options, currently only public.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  followPlaylist: function(playlistId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/followers')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(options)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Unfollow a playlist.
   * @param {string} playlistId The playlist's ID
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  unfollowPlaylist: function(playlistId, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/followers')
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Change playlist details.
   * @param {string} playlistId The playlist's ID
   * @param {Object} [options] The possible options, e.g. name, public.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example changePlaylistDetails('3EsfV6XzCHU8SPNdbnFogK', {name: 'New name', public: true}).then(...)
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  changePlaylistDetails: function(playlistId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId)
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(options)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Replace the image used to represent a specific playlist.
   * @param {string} playlistId The playlist's ID
   * @param {string} base64URI Base64 encoded JPEG image data, maximum payload size is 256 KB
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example uploadCustomPlaylistCoverImage('3EsfV6XzCHU8SPNdbnFogK', 'longbase64uri').then(...)
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  uploadCustomPlaylistCoverImage: function(playlistId, base64URI, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/images')
      .withHeaders({ 'Content-Type': 'image/jpeg' })
      .withBodyParameters(base64URI)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Add tracks to a playlist.
   * @param {string} playlistId The playlist's ID
   * @param {string[]} tracks URIs of the tracks to add to the playlist.
   * @param {Object} [options] Options, position being the only one.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example addTracksToPlaylist('3EsfV6XzCHU8SPNdbnFogK',
              '["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"]').then(...)
   * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  addTracksToPlaylist: function(playlistId, tracks, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withQueryParameters(options)
      .withBodyParameters({
        uris: tracks
      })
      .build()
      .execute(HttpManager.post, callback);
  },

  /**
   * Remove tracks from a playlist.
   * @param {string} playlistId The playlist's ID
   * @param {Object[]} tracks An array of objects containing a property called uri with the track URI (String), and
   * an optional property called positions (int[]), e.g. { uri : "spotify:track:491rM2JN8KvmV6p0oDDuJT", positions : [0, 15] }
   * @param {Object} options Options, snapshot_id being the only one.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  removeTracksFromPlaylist: function(playlistId, tracks, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(
        {
          tracks: tracks
        }, 
        options
      )
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Remove tracks from a playlist by position instead of specifying the tracks' URIs.
   * @param {string} playlistId The playlist's ID
   * @param {int[]} positions The positions of the tracks in the playlist that should be removed
   * @param {string} snapshot_id The snapshot ID, or version, of the playlist. Required
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  removeTracksFromPlaylistByPosition: function(
    playlistId,
    positions,
    snapshotId,
    callback
  ) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters({
        positions: positions,
        snapshot_id: snapshotId
      })
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Replace tracks in a playlist.
   * @param {string} playlistId The playlist's ID
   * @param {Object[]} uris An array of track URIs (strings)
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns an empty object. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  replaceTracksInPlaylist: function(playlistId, uris, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters({
        uris: uris
      })
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Reorder tracks in a playlist.
   * @param {string} playlistId The playlist's ID
   * @param {int} rangeStart The position of the first track to be reordered.
   * @param {int} insertBefore The position where the tracks should be inserted.
   * @param {Object} options Optional parameters, i.e. range_length and snapshot_id.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  reorderTracksInPlaylist: function(
    playlistId,
    rangeStart,
    insertBefore,
    options,
    callback
  ) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/playlists/' + playlistId + '/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(
        {
          range_start: rangeStart,
          insert_before: insertBefore
        },
        options
      )
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Get audio features for a single track identified by its unique Spotify ID.
   * @param {string} trackId The track ID
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAudioFeaturesForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object
   *          containing information about the audio features. If the promise is
   *          rejected, it contains an error object. Not returned if a callback is given.
   */
  getAudioFeaturesForTrack: function(trackId, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/audio-features/' + trackId)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get audio analysis for a single track identified by its unique Spotify ID.
   * @param {string} trackId The track ID
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAudioAnalysisForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object
   *          containing information about the audio analysis. If the promise is
   *          rejected, it contains an error object. Not returned if a callback is given.
   */
  getAudioAnalysisForTrack: function(trackId, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/audio-analysis/' + trackId)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get audio features for multiple tracks identified by their unique Spotify ID.
   * @param {string[]} trackIds The track IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAudioFeaturesForTracks(['38P3Q4QcdjQALGF2Z92BmR', '2HO2bnoMrpnZUbUqiilLHi']).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object
   *          containing information about the audio features for the tracks. If the promise is
   *          rejected, it contains an error object. Not returned if a callback is given.
   */
  getAudioFeaturesForTracks: function(trackIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/audio-features')
      .withQueryParameters({
        ids: trackIds.join(',')
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Create a playlist-style listening experience based on seed artists, tracks and genres.
   * @param {Object} [options] The options supplied to this request.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getRecommendations({ min_energy: 0.4, seed_artists: ['6mfK6Q2tzLMEchAr0e9Uzu', '4DYFVNKZ1uixa6SQTvzQwJ'], min_popularity: 50 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
   *          a list of tracks and a list of seeds. If rejected, it contains an error object. Not returned if a callback is given.
   */
  getRecommendations: function(options, callback) {
    var _opts = {};
    var optionsOfTypeArray = ['seed_artists', 'seed_genres', 'seed_tracks'];
    for (var option in options) {
      if (options.hasOwnProperty(option)) {
        if (
          optionsOfTypeArray.indexOf(option) !== -1 &&
          Object.prototype.toString.call(options[option]) === '[object Array]'
        ) {
          _opts[option] = options[option].join(',');
        } else {
          _opts[option] = options[option];
        }
      }
    }

    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/recommendations')
      .withQueryParameters(_opts)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve a list of available genres seed parameter values for recommendations.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getAvailableGenreSeeds().then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
   *          a list of available genres to be used as seeds for recommendations.
   *          If rejected, it contains an error object. Not returned if a callback is given.
   */
  getAvailableGenreSeeds: function(callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/recommendations/available-genre-seeds')
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve the tracks that are saved to the authenticated users Your Music library.
   * @param {Object} [options] Options, being market, limit, and/or offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains
   *          playlist track objects. Not returned if a callback is given.
   */
  getMySavedTracks: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/tracks')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library.
   * @param {string[]} trackIds The track IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
   * of the returned array's elements correspond to the track ID in the request.
   * The boolean value of true indicates that the track is part of the user's library, otherwise false.
   * Not returned if a callback is given.
   */
  containsMySavedTracks: function(trackIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/tracks/contains')
      .withQueryParameters({
        ids: trackIds.join(',')
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Remove a track from the authenticated user's Your Music library.
   * @param {string[]} trackIds The track IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error.
   * Not returned if a callback is given.
   */
  removeFromMySavedTracks: function(trackIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters({ ids: trackIds })
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Add a track from the authenticated user's Your Music library.
   * @param {string[]} trackIds The track IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given.
   */
  addToMySavedTracks: function(trackIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/tracks')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters({ ids: trackIds })
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Remove an album from the authenticated user's Your Music library.
   * @param {string[]} albumIds The album IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error.
   * Not returned if a callback is given.
   */
  removeFromMySavedAlbums: function(albumIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/albums')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(albumIds)
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Add an album from the authenticated user's Your Music library.
   * @param {string[]} albumIds The track IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given.
   */
  addToMySavedAlbums: function(albumIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/albums')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(albumIds)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Retrieve the albums that are saved to the authenticated users Your Music library.
   * @param {Object} [options] Options, being market, limit, and/or offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains
   *          playlist album objects. Not returned if a callback is given.
   */
  getMySavedAlbums: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/albums')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Check if one or more albums is already saved in the current Spotify user’s “Your Music” library.
   * @param {string[]} albumIds The album IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
   * of the returned array's elements correspond to the album ID in the request.
   * The boolean value of true indicates that the album is part of the user's library, otherwise false.
   * Not returned if a callback is given.
   */
  containsMySavedAlbums: function(albumIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/albums/contains')
      .withQueryParameters({
        ids: albumIds.join(',')
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the current user's top artists based on calculated affinity.
   * @param {Object} [options] Options, being time_range, limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of artists,
   *          otherwise an error. Not returned if a callback is given.
   */
  getMyTopArtists: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/top/artists')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the current user's top tracks based on calculated affinity.
   * @param {Object} [options] Options, being time_range, limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
   *          otherwise an error. Not returned if a callback is given.
   */
  getMyTopTracks: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/top/tracks')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the Current User's Recently Played Tracks
   * @param {Object} [options] Options, being type, after, limit, before.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of play history objects,
   *          otherwise an error. Not returned if a callback is given. Note that the response will be empty
   *          in case the user has enabled private session.
   */
  getMyRecentlyPlayedTracks: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/recently-played')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Add track or episode to device queue
   * @param {string} [uri] uri of the track or episode to add
   * @param {Object} [options] Options, being device_id.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
   *          otherwise an error. Not returned if a callback is given.
   */
  addToQueue: function(uri, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/queue')
      .withQueryParameters(
        {
          uri: uri
        },
        options
      )
      .build()
      .execute(HttpManager.post, callback);
  },


  /** 
   * Get the Current User's Available Devices
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into an array of device objects,
   *          otherwise an error. Not returned if a callback is given.
   */
  getMyDevices: function(callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/devices')
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the Current User's Currently Playing Track.
   * @param {Object} [options] Options, being market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
   *          otherwise an error. Not returned if a callback is given.
   */
  getMyCurrentPlayingTrack: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/currently-playing')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get Information About The User's Current Playback State
   * @param {Object} [options] Options, being market and additional_types.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
   *          otherwise an error. Not returned if a callback is given.
   */
  getMyCurrentPlaybackState: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Transfer a User's Playback
   * @param {string[]} [deviceIds] An _array_ containing a device ID on which playback should be started/transferred. 
   * (NOTE: The API is currently only supporting a single device ID.)
   * @param {Object} [options] Options, the only one being 'play'.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  transferMyPlayback: function(deviceIds, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(
        {
          device_ids: deviceIds,
        },
        options
      )
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Starts o Resumes the Current User's Playback
   * @param {Object} [options] Options, being device_id, context_uri, offset, uris, position_ms.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example play({context_uri: 'spotify:album:5ht7ItJgpBH7W6vJ5BqpPr'}).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  play: function(options, callback) {
    /*jshint camelcase: false */
    var _options = options || {};
    var queryParams = _options.device_id
      ? { device_id: _options.device_id }
      : null;
    var postData = {};
    ['context_uri', 'uris', 'offset', 'position_ms'].forEach(function(field) {
      if (field in _options) {
        postData[field] = _options[field];
      }
    });
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/play')
      .withQueryParameters(queryParams)
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(postData)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Pauses the Current User's Playback
   * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example pause().then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  pause: function(options, callback) {
    return (
      WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/pause')
        /*jshint camelcase: false */
        .withQueryParameters(
          options && options.device_id ? { device_id: options.device_id } : null
        )
        .withHeaders({ 'Content-Type': 'application/json' })
        .build()
        .execute(HttpManager.put, callback)
    );
  },

  /**
   * Skip the Current User's Playback To Previous Track
   * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example skipToPrevious().then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  skipToPrevious: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/previous')
      .withQueryParameters(
        options && options.device_id ? { device_id: options.device_id } : null
      )
      .build()
      .execute(HttpManager.post, callback);
  },

  /**
   * Skip the Current User's Playback To Next Track
   * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example skipToNext().then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  skipToNext: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/next')
      .withQueryParameters(
        options && options.device_id ? { device_id: options.device_id } : null
      )
      .build()
      .execute(HttpManager.post, callback);
  },

  /**
   * Seeks to the given position in the user’s currently playing track.
   *
   * @param {number} positionMs The position in milliseconds to seek to. Must be a positive number.
   * @param {Object} options Options, being device_id. If left empty will target the user's currently active device.
   * @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first
   * one is the error object (null if no error), and the second is the value if the request succeeded.
   * @return {Object} Null if a callback is provided, a `Promise` object otherwise
   */
  seek: function(positionMs, options, callback) {
    var params = {
      /* jshint camelcase: false */
      position_ms: positionMs
    };
    if (options && 'device_id' in options) {
      /* jshint camelcase: false */
      params.device_id = options.device_id;
    }
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/seek')
      .withQueryParameters(params)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Set Repeat Mode On The Current User's Playback
   * @param {string} [state] State (track, context, or off)
   * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example setRepeat('context', {}).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  setRepeat: function(state, options, callback) {
    var params = {
      state: state
    };
    if (options && 'device_id' in options) {
      /* jshint camelcase: false */
      params.device_id = options.device_id;
    }
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/repeat')
      .withQueryParameters(params)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Set Shuffle Mode On The Current User's Playback
   * @param {boolean} [state] State 
   * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example setShuffle({state: 'false'}).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an empty response,
   *          otherwise an error. Not returned if a callback is given.
   */
  setShuffle: function(state, options, callback) {
    var params = {
      state: state
    };
    if (options && 'device_id' in options) {
      /* jshint camelcase: false */
      params.device_id = options.device_id;
    }
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/shuffle')
      .withQueryParameters(params)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Set the volume for the user’s current playback device.
   * @param {number} volumePercent The volume to set. Must be a value from 0 to 100.
   * @param {Object} options Options, being device_id. If left empty will target the user's currently active device.
   * @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first
   * one is the error object (null if no error), and the second is the value if the request succeeded.
   * @return {Object} Null if a callback is provided, a `Promise` object otherwise
   */
  setVolume: function(volumePercent, options, callback) {
    var params = {
      /* jshint camelcase: false */
      volume_percent: volumePercent
    };
    if (options && 'device_id' in options) {
      /* jshint camelcase: false */
      params.device_id = options.device_id;
    }
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/player/volume')
      .withQueryParameters(params)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Add the current user as a follower of one or more other Spotify users.
   * @param {string[]} userIds The IDs of the users to be followed.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example followUsers(['thelinmichael', 'wizzler']).then(...)
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  followUsers: function(userIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following')
      .withQueryParameters({
        ids: userIds.join(','),
        type: 'user'
      })
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Add the current user as a follower of one or more artists.
   * @param {string[]} artistIds The IDs of the artists to be followed.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example followArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...)
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  followArtists: function(artistIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following')
      .withQueryParameters({
        ids: artistIds.join(','),
        type: 'artist'
      })
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Remove the current user as a follower of one or more other Spotify users.
   * @param {string[]} userIds The IDs of the users to be unfollowed.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example unfollowUsers(['thelinmichael', 'wizzler']).then(...)
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  unfollowUsers: function(userIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following')
      .withQueryParameters({
        ids: userIds.join(','),
        type: 'user'
      })
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Remove the current user as a follower of one or more artists.
   * @param {string[]} artistIds The IDs of the artists to be unfollowed.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example unfollowArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...)
   * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  unfollowArtists: function(artistIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following')
      .withQueryParameters({
        ids: artistIds.join(','),
        type: 'artist'
      })
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Check to see if the current user is following one or more other Spotify users.
   * @param {string[]} userIds The IDs of the users to check if are followed by the current user.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example isFollowingUsers(['thelinmichael', 'wizzler']).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
   *          of the returned array's elements correspond to the users IDs in the request.
   *          The boolean value of true indicates that the user is following that user, otherwise is not.
   *          Not returned if a callback is given.
   */
  isFollowingUsers: function(userIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following/contains')
      .withQueryParameters({
        ids: userIds.join(','),
        type: 'user'
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the current user's followed artists.
   * @param {Object} [options] Options, being after and limit.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains
   * album objects. Not returned if a callback is given.
   */
  getFollowedArtists: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following')
      .withQueryParameters(
        {
          type: 'artist'
        },
        options
      )
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Check if users are following a playlist.
   * @param {string} userId The playlist's owner's user ID
   * @param {string} playlistId The playlist's ID
   * @param {String[]} User IDs of the following users
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns an array of booleans. If rejected,
   * it contains an error object. Not returned if a callback is given.
   */
  areFollowingPlaylist: function(userId, playlistId, followerIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath(
        '/v1/users/' +
          encodeURIComponent(userId) +
          '/playlists/' +
          playlistId +
          '/followers/contains'
      )
      .withQueryParameters({
        ids: followerIds.join(',')
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Check to see if the current user is following one or more artists.
   * @param {string[]} artistIds The IDs of the artists to check if are followed by the current user.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example isFollowingArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...)
   * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
   *          of the returned array's elements correspond to the artists IDs in the request.
   *          The boolean value of true indicates that the user is following that artist, otherwise is not.
   *          Not returned if a callback is given.
   */
  isFollowingArtists: function(artistIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/following/contains')
      .withQueryParameters({
        ids: artistIds.join(','),
        type: 'artist'
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve new releases
   * @param {Object} [options] Options, being country, limit and/or offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains
   * album objects. Not returned if a callback is given.
   */
  getNewReleases: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/browse/new-releases')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve featured playlists
   * @param {Object} [options] Options, being country, locale, timestamp, limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains
   * featured playlists. Not returned if a callback is given.
   */
  getFeaturedPlaylists: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/browse/featured-playlists')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve a list of categories used to tag items in Spotify (e.g. in the 'Browse' tab)
   * @param {Object} [options] Options, being country, locale, limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object of categories.
   * Not returned if a callback is given.
   */
  getCategories: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/browse/categories')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve a category.
   * @param {string} categoryId The id of the category to retrieve.
   * @param {Object} [options] Options, being country, locale.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a category object.
   * Not returned if a callback is given.
   */
  getCategory: function(categoryId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/browse/categories/' + categoryId)
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Retrieve playlists for a category.
   * @param {string} categoryId The id of the category to retrieve playlists for.
   * @param {Object} [options] Options, being country, limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to a paging object containing simple playlists.
   * Not returned if a callback is given.
   */
  getPlaylistsForCategory: function(categoryId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/browse/categories/' + categoryId + '/playlists')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get a show.
   * @param {string} showId The show's ID.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getShow('3Qm86XLflmIXVm1wcwkgDK').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the show. Not returned if a callback is given.
   */
  getShow: function(showId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
    .withPath('/v1/shows/' + showId)
    .withQueryParameters(options)
    .build()
    .execute(HttpManager.get, callback);
  },

  /**
   * Look up several shows.
   * @param {string[]} showIds The IDs of the shows.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getShows(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the shows. Not returned if a callback is given.
   */
  getShows: function(showIds, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/shows')
      .withQueryParameters(
        {
          ids: showIds.join(',')
        },
        options
      )
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Check if one or more shows is already saved in the current Spotify user’s “Your Music” library.
   * @param {string[]} showIds The show IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
   * of the returned array's elements correspond to the show ID in the request.
   * The boolean value of true indicates that the show is part of the user's library, otherwise false.
   * Not returned if a callback is given.
   */
  containsMySavedShows: function(showIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/shows/contains')
      .withQueryParameters({
        ids: showIds.join(',')
      })
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Remove an show from the authenticated user's Your Music library.
   * @param {string[]} showIds The show IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error.
   * Not returned if a callback is given.
   */
  removeFromMySavedShows: function(showIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/shows')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(showIds)
      .build()
      .execute(HttpManager.del, callback);
  },

  /**
   * Add a show from the authenticated user's Your Music library.
   * @param {string[]} showIds The show IDs
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given.
   */
  addToMySavedShows: function(showIds, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/shows')
      .withHeaders({ 'Content-Type': 'application/json' })
      .withBodyParameters(showIds)
      .build()
      .execute(HttpManager.put, callback);
  },

  /**
   * Retrieve the shows that are saved to the authenticated users Your Music library.
   * @param {Object} [options] Options, being market, limit, and/or offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains
   *          playlist show objects. Not returned if a callback is given.
   */
  getMySavedShows: function(options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/me/shows')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Get the episodes of an show.
   * @param showId the show's ID.
   * @options {Object} [options] The possible options, being limit, offset, and market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getShowEpisodes('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *                    episodes in the album. The result is paginated. If the promise is rejected.
   *                    it contains an error object. Not returned if a callback is given.
   */
  getShowEpisodes: function(showId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/shows/' + showId + '/episodes')
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Search for a show.
   * @param {string} query The search query.
   * @param {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example searchShows('Space Oddity', { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  searchShows: function(query, options, callback) {
    return this.search(query, ['show'], options, callback);
  },

  /**
   * Search for an episode.
   * @param {string} query The search query.
   * @param {Object} [options] The possible options, e.g. limit, offset.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example searchEpisodes('Space Oddity', { limit : 5, offset : 1 }).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing the
   *          search results. The result is paginated. If the promise is rejected,
   *          it contains an error object. Not returned if a callback is given.
   */
  searchEpisodes: function(query, options, callback) {
    return this.search(query, ['episode'], options, callback);
  },

 /**
   * Look up an episode.
   * @param {string} episodeId The episode's ID.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getEpisode('3Qm86XLflmIXVm1wcwkgDK').then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the episode. Not returned if a callback is given.
   */
  getEpisode: function(episodeId, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/episodes/' + episodeId)
      .withQueryParameters(options)
      .build()
      .execute(HttpManager.get, callback);
  },

  /**
   * Look up several episodes.
   * @param {string[]} episodeIds The IDs of the episodes.
   * @param {Object} [options] The possible options, currently only market.
   * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
   * @example getEpisodes(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
   * @returns {Promise|undefined} A promise that if successful, returns an object containing information
   *          about the episodes. Not returned if a callback is given.
   */
  getEpisodes: function(episodeIds, options, callback) {
    return WebApiRequest.builder(this.getAccessToken())
      .withPath('/v1/episodes')
      .withQueryParameters(
        {
          ids: episodeIds.join(',')
        },
        options
      )
      .build()
      .execute(HttpManager.get, callback);
  },
};

SpotifyWebApi._addMethods = function(methods) {
  for (var i in methods) {
    if (methods.hasOwnProperty(i)) {
      this.prototype[i] = methods[i];
    }
  }
};

module.exports = SpotifyWebApi;
