# Source: new-segment-loader.js

    import window from 'global/window';
    import videojs from 'video.js';
    import SourceUpdater from './source-updater';
    import Config from './config';
    import {inspect as inspectSegment} from 'mux.js/lib/tools/ts-inspector.js';
    import Ranges from './ranges';
    import {getMediaIndexForTime_} from './playlist';

    // milliseconds
    const GET_SOME_VIDEO_DELAY = 500;

    const abortXhr = (xhr) => {
      if (!xhr) {
        return;
      }

      // Prevent error handler from running.
      xhr.onreadystatechange = null;
      xhr.abort();
    };

    /**
     * Turns segment byterange into a string suitable for use in
     * HTTP Range requests
     */
    const byterangeStr = function(byterange) {
      let byterangeStart;
      let byterangeEnd;

      // `byterangeEnd` is one less than `offset + length` because the HTTP range
      // header uses inclusive ranges
      byterangeEnd = byterange.offset + byterange.length - 1;
      byterangeStart = byterange.offset;
      return 'bytes=' + byterangeStart + '-' + byterangeEnd;
    };

    /**
     * Defines headers for use in the xhr request for a particular segment.
     */
    const segmentXhrHeaders = function(segment) {
      let headers = {};

      if ('byterange' in segment) {
        headers.Range = byterangeStr(segment.byterange);
      }
      return headers;
    };

    export default class NewSegmentLoader extends videojs.EventTarget {
      constructor({mediaSource, currentTime, bandwidth, hls, mimeType, setCurrentTime}) {
        super();

        // TODO bandwidth and tracking

        this.currentTime_ = currentTime;
        this.setCurrentTime_ = setCurrentTime;
        this.mediaSource_ = mediaSource;
        this.xhr_ = hls.xhr;

        // start paused
        this.pause();

        this.getSomeVideoInterval = window.setInterval(
          this.getSomeVideo.bind(this), GET_SOME_VIDEO_DELAY);
      }

      mimeType(mimeType) {
        this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, mimeType);
        this.clearEverything();
        this.getSomeVideo();
      }

      playlist(playlist, xhrOptions) {
        this.playlist_ = playlist;
        this.xhrOptions_ = xhrOptions;
      }

      expired(expiredTime) {
        this.expired_ = expiredTime;
      }

      pause() {
        this.paused_ = true;
        this.clearEverything();
      }

      resume() {
        this.paused_ = false;
      }

      clearEverything() {
        this.alreadyGettingSomeVideo_ = false;
        abortXhr(this.keyXhr_);
        abortXhr(this.segmentXhr_);
        this.segmentInfo_ = null;
      }

      /**
       * set an error on the segment loader
       *
       * @param {Error} error the error to set on the SegmentLoader
       * @return {Error} the error that was set or that is currently set
       */
      error(error) {
        if (error) {
          this.error_ = error;
        }
        return this.error_;
      }

      buffered() {
        return this.sourceUpdater_.buffered();
      }

      plentyOfBuffer() {
        let buffered = this.buffered();
        let endOfBuffer = buffered.length ? buffered.end(buffered.length - 1) : 0;
        let currentTime = this.currentTime_();

        if (currentTime > endOfBuffer) {
          return false;
        }

        let currentBuffered = Ranges.findRange(buffered, currentTime);

        if (currentBuffered.length === 0) {
          return false;
        }

        return currentBuffered.end(0) - currentTime >= Config.GOAL_BUFFER_LENGTH;
      }

      getSomeVideo() {
        if (this.paused_ ||
            this.alreadyGettingSomeVideo_ ||
            !this.sourceUpdater_ ||
            !this.playlist_ ||
            !this.playlist_.segments.length ||
            this.plentyOfBuffer()) {
          return;
        }

        this.alreadyGettingSomeVideo_ = true;

        let mediaIndex = this.getNextMediaIndex({
          playlist: this.playlist_,
          currentTime: this.currentTime_(),
          buffered: this.buffered(),
        });

        console.log(mediaIndex);

        if (mediaIndex < 0) {
          return;
        }

        let segment = this.playlist_.segments[mediaIndex];

        this.requestSegment({
          segment,
          mediaIndex,
          playlist: this.playlist_,
        });
      }

      requestSegment(segmentInfo) {
        if (segmentInfo.segment.key) {
          let keyRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
            uri: segmentInfo.segment.key.resolvedUri,
            responseType: 'arraybuffer'
          });

          this.keyXhr_ = this.xhr_(keyRequestOptions, this.handleResponse.bind(this));
        }

        let segmentRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
          uri: segmentInfo.segment.resolvedUri,
          responseType: 'arraybuffer',
          headers: segmentXhrHeaders(segmentInfo.segment)
        });

        this.segmentXhr_ = this.xhr_(segmentRequestOptions, this.handleResponse.bind(this));

        this.segmentInfo_ = segmentInfo;
      }

      handleResponse(err, request) {
        if (!this.segmentXhr_ ||
            (request !== this.segmentXhr_ && request !== this.keyXhr_) ||
            this.paused_) {
          return;
        }

        // if a request times out, reset bandwidth tracking
        if (request.timedout) {
          this.clearEverything();
          this.trigger('progress');
          return;
        }

        // trigger an event for other errors
        if (!request.aborted && err) {
          this.pause();
          this.error({
            status: request.status,
            message: request === this.keyXhr_ ?
              'HLS key request error at URL: ' + this.segmentInfo_.segment.key.resolvedUri :
              'HLS segment request error at URL: ' + this.segmentInfo_.segment.resolvedUri,
            code: 2,
            xhr: request,
            err: err
          });
          this.trigger('error');
          return;
        }

        // stop processing if the request was aborted
        if (!request.response) {
          this.clearEverything();
          return;
        }

        if (request === this.segmentXhr_) {
          // the segment request is no longer outstanding
          this.segmentXhr_ = null;

          // calculate the download bandwidth based on segment request
          this.roundTrip = request.roundTripTime;
          this.bandwidth = request.bandwidth;
          this.mediaBytesTransferred += request.bytesReceived || 0;
          this.mediaRequests += 1;
          this.mediaTransferDuration += request.roundTripTime || 0;

          let encrypted = this.segmentInfo_.segment.key;

          this.segmentInfo_[encrypted ? 'encryptedBytes' : 'bytes'] =
            new Uint8Array(request.response);
        }

        if (request === this.keyXhr_) {
          keyXhrRequest = this.xhr_.segmentXhr;
          // the key request is no longer outstanding
          this.xhr_.keyXhr = null;

          if (request.response.byteLength !== 16) {
            this.abort_();
            this.error({
              status: request.status,
              message: 'Invalid HLS key at URL: ' + this.segmentInfo_.segment.key.uri,
              code: 2,
              xhr: request
            });
            this.state = 'READY';
            this.pause();
            return this.trigger('error');
          }

          view = new DataView(request.response);
          this.segmentInfo_.segment.key.bytes = new Uint32Array([
            view.getUint32(0),
            view.getUint32(4),
            view.getUint32(8),
            view.getUint32(12)
          ]);

          // if the media sequence is greater than 2^32, the IV will be incorrect
          // assuming 10s segments, that would be about 1300 years
          this.segmentInfo_.segment.key.iv = this.segmentInfo_.segment.key.iv ||
            new Uint32Array([
              0,
              0,
              0,
              this.segmentInfo_.mediaIndex + this.segmentInfo_.playlist.mediaSequence
            ]);
        }

        if (!this.segmentXhr_ && !this.keyXhr_) {
          this.processResponse(this.segmentInfo_);
        }
      }

      processResponse(segmentInfo) {
        if (segmentInfo.segment.key) {
          let decryptedSegmentInfo = segmentInfo;

          // this is an encrypted segment
          // incrementally decrypt the segment
          /* eslint-disable no-new, handle-callback-err */
          new Decrypter(
            segmentInfo.encryptedBytes, segmentInfo.segment.key.bytes,
            segmentInfo.segment.key.iv, (err, bytes) => {
            // err always null

            if (this.segmentInfo_ !== decryptedSegmentInfo) {
              return;
            }

            segmentInfo.bytes = bytes;
            this.appendSegment(segmentInfo);
          });
          /* eslint-enable */
        } else {
          this.appendSegment(segmentInfo);
        }
      }

      updateTimestampOffset(segment) {
        if (this.playlist_ !== this.bufferPlaylist_ ||
            this.playlist_.discontinuityStarts[segment.uri]) {
          console.log('Setting timestamp offset to: ' + segment.timeInfo.start);
          this.sourceUpdater_.timestampOffset(segment.timeInfo.start);
          this.bufferPlaylist_ = this.playlist_;
        }
      }

      appendSegment(segmentInfo) {
        let timeInfo = inspectSegment(segmentInfo.bytes, this.inspectCache_);

        if (timeInfo.video && timeInfo.video.length === 2) {
          timeInfo.start = timeInfo.video[0].dts;
          timeInfo.end = timeInfo.video[1].dts;
          this.inspectCache_ = timeInfo.end;
        } else if (timeInfo.audio && timeInfo.audio.length === 2) {
          timeInfo.start = timeInfo.audio[0].dts;
          timeInfo.end = timeInfo.audio[1].dts;
          this.inspectCache_ = timeInfo.end;
        }

        segmentInfo.segment.timeInfo = timeInfo;

        console.log(timeInfo);

        this.updateTimestampOffset(segmentInfo.segment);

        this.sourceUpdater_.appendBuffer(segmentInfo.bytes, () => {
          this.trigger('progress');

          // TODO check end of stream

          this.alreadyGettingSomeVideo_ = false;

          this.getSomeVideo();
        });
      }

      destroy() {
        this.clearEverything();
        window.clearInterval(this.getSomeVideoInterval);
      }

      findNextNonBuffered({playlist, buffered, index}) {
        for (; index <= playlist.segments.length; index++) {
          let segment = playlist.segments[index];

          if (!segment.timeInfo) {
            return index;
          }

          let midpoint = (segment.timeInfo.end - segment.timeInfo.start) / 2;

          if (!Ranges.findRange(buffered, midpoint)) {
            return index;
          }
        }

        return index;
      }

      // TODO remove
      bufferStr(buffer) {
        let bufferStr = ''

        for (let i = 0; i < buffer.length; i++) {
          bufferStr += ` ${buffer.start(i)} => ${buffer.end(i)}`;
        }

        return bufferStr;
      }

      indexForTime(playlist, time, expired) {
        let index = playlist.segments.findIndex((segment) => {
          return segment.timeInfo &&
            segment.timeInfo.video &&
            segment.timeInfo.start <= time &&
            segment.timeInfo.end >= time;
        });

        if (index === -1) {
          return getMediaIndexForTime_(playlist, time, expired);
        }
        return index;
      }

      getNextMediaIndex({playlist, currentTime, buffered}) {
        console.log('Getting with: ', {
          currentTime,
          seg: playlist.segments[0],
          buffered: this.bufferStr(buffered)
        });

        let currentBuffered = Ranges.findRange(buffered, currentTime);

        if (currentBuffered.length > 0) {
          // playing, grab next non buffered range from current buffered range
          let currentMediaIndex = playlist.segments.findIndex((segment) => {
            return segment.timeInfo &&
              segment.timeInfo.start <= currentTime &&
              segment.timeInfo.end >= currentTime;
          });

          if (currentMediaIndex < 0) {
            return 0;
          }

          console.log('Finding with ' + currentMediaIndex);
          return this.findNextNonBuffered({playlist, buffered, index: currentMediaIndex});
        }

        let index = this.indexForTime(playlist, currentTime, this.expired_);

        // find CLOSEST non buffered range
        let jump = 1;

        console.log('Jumping with ' + index);
        while (index < playlist.segments.length && index >= 0) {
          let segment = playlist.segments[index];

          if (!segment.timeInfo) {
            return index;
          }

          let midpoint = (segment.timeInfo.end - segment.timeInfo.start) / 2;

          if (!Ranges.findRange(buffered, midpoint)) {
            console.log('Jumped: ' + jump);
            return index;
          }

          index += jump;
          jump = -1 * (jump + 1);
        }

        console.log('OUT');
        return -1;
      }
    }