# Source: playlist-selectors.js

    import Config from './config';
    import Playlist from './playlist';

    // Utilities

    /**
     * Returns the CSS value for the specified property on an element
     * using `getComputedStyle`. Firefox has a long-standing issue where
     * getComputedStyle() may return null when running in an iframe with
     * `display: none`.
     *
     * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
     * @param {HTMLElement} el the htmlelement to work on
     * @param {string} the proprety to get the style for
     */
    const safeGetComputedStyle = function(el, property) {
      let result;

      if (!el) {
        return '';
      }

      result = window.getComputedStyle(el);
      if (!result) {
        return '';
      }

      return result[property];
    };

    /**
     * Resuable stable sort function
     *
     * @param {Playlists} array
     * @param {Function} sortFn Different comparators
     * @function stableSort
     */
    const stableSort = function(array, sortFn) {
      let newArray = array.slice();

      array.sort(function(left, right) {
        let cmp = sortFn(left, right);

        if (cmp === 0) {
          return newArray.indexOf(left) - newArray.indexOf(right);
        }
        return cmp;
      });
    };

    /**
     * A comparator function to sort two playlist object by bandwidth.
     *
     * @param {Object} left a media playlist object
     * @param {Object} right a media playlist object
     * @return {Number} Greater than zero if the bandwidth attribute of
     * left is greater than the corresponding attribute of right. Less
     * than zero if the bandwidth of right is greater than left and
     * exactly zero if the two are equal.
     */
    export const comparePlaylistBandwidth = function(left, right) {
      let leftBandwidth;
      let rightBandwidth;

      if (left.attributes && left.attributes.BANDWIDTH) {
        leftBandwidth = left.attributes.BANDWIDTH;
      }
      leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
      if (right.attributes && right.attributes.BANDWIDTH) {
        rightBandwidth = right.attributes.BANDWIDTH;
      }
      rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;

      return leftBandwidth - rightBandwidth;
    };

    /**
     * A comparator function to sort two playlist object by resolution (width).
     * @param {Object} left a media playlist object
     * @param {Object} right a media playlist object
     * @return {Number} Greater than zero if the resolution.width attribute of
     * left is greater than the corresponding attribute of right. Less
     * than zero if the resolution.width of right is greater than left and
     * exactly zero if the two are equal.
     */
    export const comparePlaylistResolution = function(left, right) {
      let leftWidth;
      let rightWidth;

      if (left.attributes &&
          left.attributes.RESOLUTION &&
          left.attributes.RESOLUTION.width) {
        leftWidth = left.attributes.RESOLUTION.width;
      }

      leftWidth = leftWidth || window.Number.MAX_VALUE;

      if (right.attributes &&
          right.attributes.RESOLUTION &&
          right.attributes.RESOLUTION.width) {
        rightWidth = right.attributes.RESOLUTION.width;
      }

      rightWidth = rightWidth || window.Number.MAX_VALUE;

      // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
      // have the same media dimensions/ resolution
      if (leftWidth === rightWidth &&
          left.attributes.BANDWIDTH &&
          right.attributes.BANDWIDTH) {
        return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
      }
      return leftWidth - rightWidth;
    };

    /**
     * Chooses the appropriate media playlist based on bandwidth and player size
     *
     * @param {Object} master
     *        Object representation of the master manifest
     * @param {Number} playerBandwidth
     *        Current calculated bandwidth of the player
     * @param {Number} playerWidth
     *        Current width of the player element
     * @param {Number} playerHeight
     *        Current height of the player element
     * @return {Playlist} the highest bitrate playlist less than the
     * currently detected bandwidth, accounting for some amount of
     * bandwidth variance
     */
    const simpleSelector = function(master, playerBandwidth, playerWidth, playerHeight) {
      // convert the playlists to an intermediary representation to make comparisons easier
      let sortedPlaylistReps = master.playlists.map((playlist) => {
        let width;
        let height;
        let bandwidth;

        if (playlist.attributes) {
          width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
          height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
          bandwidth = playlist.attributes.BANDWIDTH;
        }

        bandwidth = bandwidth || window.Number.MAX_VALUE;

        return {
          bandwidth,
          width,
          height,
          playlist
        };
      });

      stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth);

      // filter out any playlists that have been excluded due to
      // incompatible configurations or playback errors
      sortedPlaylistReps = sortedPlaylistReps.filter(
        (rep) => Playlist.isEnabled(rep.playlist)
      );

      // filter out any variant that has greater effective bitrate
      // than the current estimated bandwidth
      let bandwidthPlaylistReps = sortedPlaylistReps.filter(
        (rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth
      );

      let highestRemainingBandwidthRep =
        bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];

      // get all of the renditions with the same (highest) bandwidth
      // and then taking the very first element
      let bandwidthBestRep = bandwidthPlaylistReps.filter(
        (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth
      )[0];

      // filter out playlists without resolution information
      let haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height);

      // sort variants by resolution
      stableSort(haveResolution, (left, right) => left.width - right.width);

      // if we have the exact resolution as the player use it
      let resolutionBestRepList = haveResolution.filter(
        (rep) => rep.width === playerWidth && rep.height === playerHeight
      );

      highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
      // ensure that we pick the highest bandwidth variant that have exact resolution
      let resolutionBestRep = resolutionBestRepList.filter(
        (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth
      )[0];

      let resolutionPlusOneList;
      let resolutionPlusOneSmallest;
      let resolutionPlusOneRep;

      // find the smallest variant that is larger than the player
      // if there is no match of exact resolution
      if (!resolutionBestRep) {
        resolutionPlusOneList = haveResolution.filter(
          (rep) => rep.width > playerWidth || rep.height > playerHeight
        );

        // find all the variants have the same smallest resolution
        resolutionPlusOneSmallest = resolutionPlusOneList.filter(
          (rep) => rep.width === resolutionPlusOneList[0].width &&
                   rep.height === resolutionPlusOneList[0].height
        );

        // ensure that we also pick the highest bandwidth variant that
        // is just-larger-than the video player
        highestRemainingBandwidthRep =
          resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
        resolutionPlusOneRep = resolutionPlusOneSmallest.filter(
          (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth
        )[0];
      }

      // fallback chain of variants
      return (
        resolutionPlusOneRep ||
        resolutionBestRep ||
        bandwidthBestRep ||
        sortedPlaylistReps[0]
      ).playlist;
    };

    // Playlist Selectors

    /**
     * Chooses the appropriate media playlist based on the most recent
     * bandwidth estimate and the player size.
     *
     * Expects to be called within the context of an instance of HlsHandler
     *
     * @return {Playlist} the highest bitrate playlist less than the
     * currently detected bandwidth, accounting for some amount of
     * bandwidth variance
     */
    export const lastBandwidthSelector = function() {
      return simpleSelector(this.playlists.master,
                            this.systemBandwidth,
                            parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10),
                            parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
    };

    /**
     * Chooses the appropriate media playlist based on an
     * exponential-weighted moving average of the bandwidth after
     * filtering for player size.
     *
     * Expects to be called within the context of an instance of HlsHandler
     *
     * @param {Number} decay - a number between 0 and 1. Higher values of
     * this parameter will cause previous bandwidth estimates to lose
     * significance more quickly.
     * @return {Function} a function which can be invoked to create a new
     * playlist selector function.
     * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
     */
    export const movingAverageBandwidthSelector = function(decay) {
      let average = -1;

      if (decay < 0 || decay > 1) {
        throw new Error('Moving average bandwidth decay must be between 0 and 1.');
      }

      return function() {
        if (average < 0) {
          average = this.systemBandwidth;
        }

        average = decay * this.systemBandwidth + (1 - decay) * average;
        return simpleSelector(this.playlists.master,
                              average,
                              parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10),
                              parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
      };
    };

    /**
     * Chooses the appropriate media playlist based on the potential to rebuffer
     *
     * @param {Object} settings
     *        Object of information required to use this selector
     * @param {Object} settings.master
     *        Object representation of the master manifest
     * @param {Number} settings.currentTime
     *        The current time of the player
     * @param {Number} settings.bandwidth
     *        Current measured bandwidth
     * @param {Number} settings.duration
     *        Duration of the media
     * @param {Number} settings.segmentDuration
     *        Segment duration to be used in round trip time calculations
     * @param {Number} settings.timeUntilRebuffer
     *        Time left in seconds until the player has to rebuffer
     * @param {Number} settings.currentTimeline
     *        The current timeline segments are being loaded from
     * @param {SyncController} settings.syncController
     *        SyncController for determining if we have a sync point for a given playlist
     * @return {Object|null}
     *         {Object} return.playlist
     *         The highest bandwidth playlist with the least amount of rebuffering
     *         {Number} return.rebufferingImpact
     *         The amount of time in seconds switching to this playlist will rebuffer. A
     *         negative value means that switching will cause zero rebuffering.
     */
    export const minRebufferMaxBandwidthSelector = function(settings) {
      const {
        master,
        currentTime,
        bandwidth,
        duration,
        segmentDuration,
        timeUntilRebuffer,
        currentTimeline,
        syncController
      } = settings;

      const bandwidthPlaylists =
        master.playlists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH'));

      const rebufferingEstimates = bandwidthPlaylists.map((playlist) => {
        const syncPoint = syncController.getSyncPoint(playlist,
                                                      duration,
                                                      currentTimeline,
                                                      currentTime);
        // If there is no sync point for this playlist, switching to it will require a
        // sync request first. This will double the request time
        const numRequests = syncPoint ? 1 : 2;
        const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration,
                                                                        bandwidth,
                                                                        playlist);
        const rebufferingImpact = (requestTimeEstimate * numRequests) - timeUntilRebuffer;

        return {
          playlist,
          rebufferingImpact
        };
      });

      const noRebufferingPlaylists = rebufferingEstimates.filter(
        (estimate) => estimate.rebufferingImpact <= 0);

      // Sort by bandwidth DESC
      stableSort(noRebufferingPlaylists,
        (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist));

      if (noRebufferingPlaylists.length) {
        return noRebufferingPlaylists[0];
      }

      stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact);

      return rebufferingEstimates[0] || null;
    };