text: require('source-map-support').install();
  
  const events = require('events');
  const util = require('util');
  
  const dbus = require('dbus-next');
  dbus.setBigIntCompat(true);
  const PlayerInterface = require('./interfaces/player');
  const RootInterface = require('./interfaces/root');
  const PlaylistsInterface = require('./interfaces/playlists');
  const TracklistInterface = require('./interfaces/tracklist');
  const types = require('./interfaces/types');
  const constants = require('./constants');
  
  const MPRIS_PATH = '/org/mpris/MediaPlayer2';
  
  function lcfirst(str) {
    return str[0].toLowerCase()+str.substr(1);
  }
  
  /**
   * Construct a new Player and export it on the DBus session bus.
   *
   * For more information about the properties of this class, see [the MPRIS DBus Interface Specification](https://specifications.freedesktop.org/mpris-spec/latest/).
   *
   * Method Call Events
   * ------------------
   *
   * The Player is an `EventEmitter` that emits events when the corresponding
   * methods are called on the DBus interface over the wire.
   *
   * The Player emits events whenever the corresponding methods on the DBus
   * interface are called.
   *
   * * `raise` - Brings the media player's user interface to the front using any appropriate mechanism available.
   * * `quit` - Causes the media player to stop running.
   * * `next` - Skips to the next track in the tracklist.
   * * `previous` - Skips to the previous track in the tracklist.
   * * `pause` - Pauses playback.
   * * `playPause` - Pauses playback.  If playback is already paused, resumes playback. If playback is stopped, starts playback.
   * * `stop` - Stops playback.
   * * `play` - Starts or resumes playback.
   * * `seek` - Seeks forward in the current track by the specified number of microseconds. With event data `offset`.
   * * `position` - Sets the current track position in microseconds. With event data `{ trackId, position }`.
   * * `open` - Opens the Uri given as an argument. With event data `{ uri }`.
   * * `volume` - Sets the volume of the player. With event data `volume` (between 0.0 and 1.0).
   * * `shuffle` - Sets whether shuffle is enabled on the player. With event data `shuffleStatus` (boolean).
   * * `rate` - Sets the playback rate of the player. A value of 1.0 is the normal rate. With event data `rate`.
   * * `loopStatus` - Sets the loop status of the player to either 'None', 'Track', or 'Playlist'. With event data `loopStatus`.
   * * `activatePlaylist` -  Starts playing the given playlist. With event data `playlistId`.
   *
   * The Player may also emit an `error` event with the underlying Node `Error`
   * as the event data. After receiving this event, the Player may be
   * disconnected.
   *
   * ```
   * player.on('play', () => {
   *   realPlayer.play();
   * });
   *
   * player.on('shuffle', (enableShuffle) => {
   *   realPlayer.setShuffle(enableShuffle);
   *   player.shuffle = enableShuffle;
   * });
   * ```
   *
   * Player Properties
   * -----------------
   *
   * Player properties (documented below) should be kept up to date to reflect
   * the state of your real player. These properties can be gotten by the client
   * through the `org.freedesktop.DBus.Properties` interface which will return
   * the value currently set on the player. Setting these properties on the
   * player to a different value will emit the `PropertiesChanged` signal on the
   * properties interface to notify clients that properties of the player have
   * changed.
   *
   * ```
   * realPlayer.on('shuffle:changed', (shuffleEnabled) => {
   *   player.shuffle = shuffleEnabled;
   * });
   *
   * realPlayer.on('play', () => {
   *   player.playbackStatus = 'Playing';
   * });
   * ```
   *
   * Player Position
   * ---------------
   *
   * Clients can get the position of your player by getting the `Position`
   * property of the `org.mpris.MediaPlayer2.Player` interface. Since position
   * updates continuously, {@link Player#getPosition} is implemented as a getter
   * you can override on your Player. This getter will be called when a client
   * requests the position and should return the position of your player for the
   * client in microseconds.
   *
   * ```
   * player.getPosition() {
   *   return realPlayer.getPositionInMicroseconds();
   * }
   * ```
   *
   * When your real player seeks to a new location, such as when someone clicks
   * on the time bar, you can notify clients of the new position by calling the
   * {@link Player#seeked} method. This will raise the `Seeked` signal on the
   * `org.mpris.MediaPlayer2.Player` interface with the given current time of the
   * player in microseconds.
   *
   * ```
   * realPlayer.on('seeked', (positionInMicroseconds) => {
   *   player.seeked(positionInMicroseconds);
   * });
   * ```
   *
   * Clients can request to set position using the `Seek` and `SetPosition`
   * methods of the `org.mpris.MediaPlayer2.Player` interface. These requests are
   * implemented as events on the Player similar to the other requests.
   *
   * ```
   * player.on('seek', (offset) => {
   *   // note that offset may be negative
   *   let currentPosition = realPlayer.getPositionInMicroseconds();
   *   let newPosition = currentPosition + offset;
   *   realPlayer.setPosition(newPosition);
   * });
   *
   * player.on('position', (event) => {
   *   // check that event.trackId is the current track before continuing.
   *   realPlayer.setPosition(event.position);
   * });
   * ```
   *
   * @class Player
   * @param {Object} options - Options for the player
   * @param {String} options.name - Name on the bus to export to as `org.mpris.MediaPlayer2.{name}`.
   * @param {String} options.identity - Identity for the player to display on the root media player interface.
   * @param {Array} options.supportedMimeTypes - Mime types this player can open with the `org.mpris.MediaPlayer2.Open` method.
   * @param {Array} options.supportedInterfaces - The interfaces this player supports. Can include `'player'`, `'playlists'`, and `'trackList'`.
   * @property {String} identity - A friendly name to identify the media player to users.
   * @property {Boolean} fullscreen - Whether the media player is occupying the fullscreen.
   * @property {Array} supportedUriSchemes - The URI schemes supported by the media player.
   * @property {Array} supportedMimeTypes - The mime-types supported by the media player.
   * @property {Boolean} canQuit - Whether the player can quit.
   * @property {Boolean} canRaise - Whether the player can raise.
   * @property {Boolean} canSetFullscreen - Whether the player can be set to fullscreen.
   * @property {Boolean} hasTrackList - Indicates whether the /org/mpris/MediaPlayer2 object implements the org.mpris.MediaPlayer2.TrackList interface.
   * @property {String} desktopEntry - The basename of an installed .desktop file which complies with the Desktop entry specification, with the ".desktop" extension stripped.
   * @property {String} playbackStatus - The current playback status. May be "Playing", "Paused" or "Stopped".
   * @property {String} loopStatus - The current loop/repeat status. May be "None", "Track", or "Playlist".
   * @property {Boolean} shuffle - Whether the player is shuffling.
   * @property {Object} metadata - The metadata of the current element. If there is a current track, this must have a "mpris:trackid" entry (of D-Bus type "o") at the very least, which contains a D-Bus path that uniquely identifies this track.
   * @property {Double} volume - The volume level.
   * @property {Boolean} canControl - Whether the media player may be controlled over this interface.
   * @property {Boolean} canPause - Whether playback can be paused using Pause or PlayPause.
   * @property {Boolean} canPlay - Whether playback can be started using Play or PlayPause.
   * @property {Boolean} canSeek - Whether the client can control the playback position using Seek and SetPosition.
   * @property {Boolean} canGoNext - Whether the client can call the Next method on this interface and expect the current track to change.
   * @property {Boolean} canGoPrevious - Whether the client can call the Previous method on this interface and expect the current track to change.
   * @property {Double} rate - The current playback rate.
   * @property {Double} minimumRate - The minimum value which the Rate property can take.
   * @property {Double} maximumRate - The maximum value which the Rate property can take.
   * @property {Array} playlists - The current playlists set by {@link Player#setPlaylists}. (Not a DBus property).
   * @property {String} activePlaylist - The id of the currently-active playlist.
   */
  function Player(opts) {
    if (!(this instanceof Player)) {
      return new Player(opts);
    }
  
    events.EventEmitter.call(this);
    this.name = opts.name;
    this.supportedInterfaces = opts.supportedInterfaces || ['player'];
    this._tracks = [];
    this.init(opts);
  }
  util.inherits(Player, events.EventEmitter);
  
  Player.prototype.init = function(opts) {
    this.serviceName = `org.mpris.MediaPlayer2.${this.name}`;
    dbus.validators.assertBusNameValid(this.serviceName);
  
    this._bus = dbus.sessionBus();
  
    this._bus.on('error', (err) => {
      this.emit('error', err);
    });
  
    this.interfaces = {};
  
    this._addRootInterface(this._bus, opts);
  
    if (this.supportedInterfaces.indexOf('player') >= 0) {
      this._addPlayerInterface(this._bus);
    }
    if (this.supportedInterfaces.indexOf('trackList') >= 0) {
      this._addTracklistInterface(this._bus);
    }
    if (this.supportedInterfaces.indexOf('playlists') >= 0) {
      this._addPlaylistsInterface(this._bus);
    }
  
    let exportInterfaces = (name) => {
        for (let k of Object.keys(this.interfaces)) {
          let iface = this.interfaces[k];
          name.export(MPRIS_PATH, iface);
        }
    };
  
    let emitError = (err) => {
      this.emit('error', err);
    };
  
    this._bus.requestName(this.serviceName, dbus.DBUS_NAME_FLAG_DO_NOT_QUEUE)
      .then(exportInterfaces)
      .catch((err) => {
        if (err instanceof dbus.NameExistsError) {
          // if the name exists, try to take a name for the instance per the spec.
          // https://specifications.freedesktop.org/mpris-spec/latest/#Bus-Name-Policy
          this.serviceName = `${this.serviceName}.instance${process.pid}`;
          this._bus.requestName(this.serviceName)
            .then(exportInterfaces)
            .catch(emitError);
        } else {
          emitError(err);
        }
      });
  };
  
  Player.prototype._addRootInterface = function(bus, opts) {
    this.interfaces.root = new RootInterface(this, opts);
    this._addEventedPropertiesList(this.interfaces.root,
      ['Identity', 'Fullscreen', 'SupportedUriSchemes', 'SupportedMimeTypes',
      'CanQuit', 'CanRaise', 'CanSetFullscreen', 'HasTrackList',
      'DesktopEntry']);
  };
  
  Player.prototype._addPlayerInterface = function(bus) {
    this.interfaces.player = new PlayerInterface(this);
    let eventedProps = ['PlaybackStatus', 'LoopStatus', 'Rate', 'Shuffle',
      'Metadata', 'Volume', 'CanControl', 'CanPause', 'CanPlay', 'CanSeek',
      'CanGoNext', 'CanGoPrevious', 'MinimumRate', 'MaximumRate'];
    this._addEventedPropertiesList(this.interfaces.player, eventedProps);
  };
  
  Player.prototype._addTracklistInterface = function(bus) {
    this.interfaces.tracklist = new TracklistInterface(this);
    this._addEventedPropertiesList(this.interfaces.tracklist, ['CanEditTracks']);
  
    Object.defineProperty(this, 'tracks', {
      get: function() {
        return this._tracks;
      },
      set: function(value) {
        this._tracks = value;
        this.interfaces.tracklist.TrackListReplaced(value);
      },
      enumerable: true,
      configurable: true
    });
  };
  
  Player.prototype._addPlaylistsInterface = function(bus) {
    this.interfaces.playlists = new PlaylistsInterface(this);
    this._addEventedPropertiesList(this.interfaces.playlists,
      ['PlaylistCount', 'ActivePlaylist']);
  }
  
  /**
   * Get a valid object path with the `subpath` as the basename which is suitable
   * for use as an id.
   *
   * @name Player#objectPath
   * @function
   * @param {String} subpath - The basename of this path
   * @returns {String} - A valid object path that can be used as an id.
   */
  Player.prototype.objectPath = function(subpath) {
    let path = `/org/node/mediaplayer/${this.name}`;
    if (subpath) {
      path += `/${subpath}`;
    }
    return path;
  };
  
  Player.prototype._addEventedProperty = function(iface, name) {
    let that = this;
  
    let localName = lcfirst(name);
  
    Object.defineProperty(this, localName, {
      get: function() {
        let value = iface[name];
        if (name === 'ActivePlaylist') {
          return types.playlistToPlain(value);
        } else if (name === 'Metadata') {
          return types.metadataToPlain(value);
        }
        return value;
      },
      set: function(value) {
        iface.setProperty(name, value);
      },
      enumerable: true,
      configurable: true
    });
  };
  
  Player.prototype._addEventedPropertiesList = function(iface, props) {
    for (let i = 0; i < props.length; i++) {
      this._addEventedProperty(iface, props[i]);
    }
  };
  
  /**
   * Gets the position of this player. This method is intended to be overridden
   * by the user to return the position of the player in microseconds.
   *
   * @name Player#getPosition
   * @function
   * @returns {Integer} - The current position of the player in microseconds.
   */
  Player.prototype.getPosition = function() {
    return 0;
  }
  
  /**
   * Emits the `Seeked` DBus signal to listening clients with the given position.
   *
   * @name Player#seeked
   * @function
   * @param {Integer} position - The position in microseconds.
   */
  Player.prototype.seeked = function(position) {
    let seekTo = Math.floor(position || 0);
    if (isNaN(seekTo)) {
      throw new Error(`seeked expected a number (got ${position})`);
    }
    this.interfaces.player.Seeked(seekTo);
  };
  
  Player.prototype.getTrackIndex = function(trackId) {
    for (let i = 0; i < this.tracks.length; i++) {
      let track = this.tracks[i];
  
      if (track['mpris:trackid'] === trackId) {
        return i;
      }
    }
  
    return -1;
  };
  
  Player.prototype.getTrack = function(trackId) {
    return this.tracks[this.getTrackIndex(trackId)];
  };
  
  Player.prototype.addTrack = function(track) {
    this.tracks.push(track);
    this.interfaces.tracklist.setTracks(this.tracks);
  
    let afterTrack = '/org/mpris/MediaPlayer2/TrackList/NoTrack';
    if (this.tracks.length > 2) {
      afterTrack = this.tracks[this.tracks.length - 2]['mpris:trackid'];
    }
    that.interfaces.tracklist.TrackAdded(afterTrack);
  };
  
  Player.prototype.removeTrack = function(trackId) {
    let i = this.getTrackIndex(trackId);
    this.tracks.splice(i, 1);
    this.interfaces.tracklist.setTracks(this.tracks);
  
    that.interfaces.tracklist.TrackRemoved(trackId);
  };
  
  /**
   * Get the index of a playlist entry in the `playlists` list property of the
   * player from the given id.
   *
   * @name Player#getPlaylistIndex
   * @function
   * @param {String} playlistId - The id for the playlist entry.
   */
  Player.prototype.getPlaylistIndex = function(playlistId) {
    for (let i = 0; i < this.playlists.length; i++) {
      let playlist = this.playlists[i];
  
      if (playlist.Id === playlistId) {
        return i;
      }
    }
  
    return -1;
  };
  
  /**
   * Set the list of playlists advertised to listeners on the bus. Each playlist
   * must have string members `Id`, `Name`, and `Icon`.
   *
   * @name Player#setPlaylists
   * @function
   * @param {Array} playlists - A list of playlists.
   */
  Player.prototype.setPlaylists = function(playlists) {
    this.playlists = playlists;
    this.playlistCount = playlists.length;
  
    let that = this;
    this.playlists.forEach(function(playlist) {
      that.interfaces.playlists.PlaylistChanged(playlist);
    });
  };
  
  /**
   * Set the playlist identified by `playlistId` to be the currently active
   * playlist.
   *
   * @name Player#setActivePlaylist
   * @function
   * @param {String} playlistId - The id of the playlist to activate.
   */
  Player.prototype.setActivePlaylist = function(playlistId) {
    this.interfaces.playlists.setActivePlaylistId(playlistId);
  };
  
  /**
   * Enumerated value for the `playbackStatus` property of the player to indicate
   * a track is currently playing.
   *
   * @name Player#PLAYBACK_STATUS_PLAYING
   * @static
   * @constant
   */
  Player.PLAYBACK_STATUS_PLAYING = constants.PLAYBACK_STATUS_PLAYING;
  
  /**
   * Enumerated value for the `playbackStatus` property of the player to indicate
   * a track is currently paused.
   *
   * @name Player#PLAYBACK_STATUS_PAUSED
   * @static
   * @constant
   */
  Player.PLAYBACK_STATUS_PAUSED = constants.PLAYBACK_STATUS_PAUSED;
  
  /**
   * Enumerated value for the `playbackStatus` property of the player to indicate
   * there is no track currently playing.
   *
   * @name Player#PLAYBACK_STATUS_STOPPED
   * @static
   * @constant
   */
  Player.PLAYBACK_STATUS_STOPPED = constants.PLAYBACK_STATUS_STOPPED;
  
  /**
   * Enumerated value for the `loopStatus` property of the player to indicate
   * playback will stop when there are no more tracks to play.
   *
   * @name Player#LOOP_STATUS_NONE
   * @static
   * @constant
   */
  Player.LOOP_STATUS_NONE = constants.LOOP_STATUS_NONE;
  
  /**
   * Enumerated value for the `loopStatus` property of the player to indicate the
   * current track will start again from the beginning once it has finished
   * playing.
   *
   * @name Player#LOOP_STATUS_TRACK
   * @static
   * @constant
   */
  Player.LOOP_STATUS_TRACK = constants.LOOP_STATUS_TRACK;
  
  /**
   * Enumerated value for the `loopStatus` property of the player to indicate the
   * playback loops through a list of tracks.
   *
   * @name Player#LOOP_STATUS_PLAYLIST
   * @static
   * @constant
   */
  Player.LOOP_STATUS_PLAYLIST = constants.LOOP_STATUS_PLAYLIST;
  
  module.exports = Player;
  