/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.media.BufferingObserver');

goog.require('shaka.log');
goog.require('shaka.util.MediaElementEvent');


/**
 * The buffering observer watches how much content has been buffered and raises
 * events when the state changes (enough => not enough or vice versa).
 *
 * @final
 */
shaka.media.BufferingObserver = class {
  /**
   * @param {number} thresholdWhenStarving
   * @param {number} thresholdWhenSatisfied
   */
  constructor(thresholdWhenStarving, thresholdWhenSatisfied) {
    const State = shaka.media.BufferingObserver.State;

    /** @private {shaka.media.BufferingObserver.State} */
    this.previousState_ = State.SATISFIED;

    /** @private {!Map<shaka.media.BufferingObserver.State, number>} */
    this.thresholds_ = new Map()
        .set(State.SATISFIED, thresholdWhenSatisfied)
        .set(State.STARVING, thresholdWhenStarving);

    /** @private {number} */
    this.lastRebufferTime_ = 0;

    /**
     * Last reported time from video element event. It is used to monitor
     * media progression. Resets to -1 on seeking and waiting events.
     * @private {number}
     */
    this.lastReportedTime_ = -1;
  }

  /**
   * @param {number} thresholdWhenStarving
   * @param {number} thresholdWhenSatisfied
   */
  setThresholds(thresholdWhenStarving, thresholdWhenSatisfied) {
    const State = shaka.media.BufferingObserver.State;
    this.thresholds_
        .set(State.SATISFIED, thresholdWhenSatisfied)
        .set(State.STARVING, thresholdWhenStarving);
  }

  /**
   * Update the observer by telling it how much content has been buffered (in
   * seconds) and if we are buffered to the end of the presentation. If the
   * controller believes the state has changed, it will return |true|.
   *
   * @param {number} bufferLead
   * @param {boolean} bufferedToEnd
   * @return {boolean}
   */
  update(bufferLead, bufferedToEnd) {
    const State = shaka.media.BufferingObserver.State;

    /**
     * Our threshold for how much we need before we declare ourselves as
     * starving is based on whether or not we were just starving. If we
     * were just starving, we are more likely to starve again, so we require
     * more content to be buffered than if we were not just starving.
     *
     * @type {number}
     */
    const threshold = this.thresholds_.get(this.previousState_);

    const newState =
        (bufferedToEnd || (bufferLead >= threshold && bufferLead > 0)) ?
        (State.SATISFIED) :
        (State.STARVING);

    return this.setState(newState);
  }

  /**
   * Set which state that the observer should think playback was in.
   *
   * @param {shaka.media.BufferingObserver.State} state
   * @return {boolean}
   */
  setState(state) {
    const State = shaka.media.BufferingObserver.State;

    // Return |true| only when the state has changed.
    const stateChanged = this.previousState_ !== state;
    this.previousState_ = state;
    if (stateChanged && state === State.SATISFIED) {
      this.lastRebufferTime_ = Date.now();
    }
    return stateChanged;
  }

  /**
   * Get the state that the observer last thought playback was in.
   *
   * @return {shaka.media.BufferingObserver.State}
   */
  getState() {
    return this.previousState_;
  }

  /**
   * Return the last time that the state went from |STARVING| to |SATISFIED|.
   * @return {number}
   */
  getLastRebufferTime() {
    return this.lastRebufferTime_;
  }

  /**
   * Reset the last rebuffer time to zero.
   */
  resetLastRebufferTime() {
    this.lastRebufferTime_ = 0;
  }

  /**
   * @param {!shaka.util.MediaElementEvent} event
   * @param {number} currentTime
   * @return {boolean}
   */
  reportEvent(event, currentTime) {
    shaka.log.v2('BufferingObserver got event', event, 'at time', currentTime);
    const previousTime = this.lastReportedTime_;
    this.lastReportedTime_ = currentTime;
    let state = undefined;
    switch (event) {
      case shaka.util.MediaElementEvent.SEEKING:
      case shaka.util.MediaElementEvent.WAITING:
        this.lastReportedTime_ = -1;
        state = shaka.media.BufferingObserver.State.STARVING;
        break;
      case shaka.util.MediaElementEvent.CAN_PLAY_THROUGH:
      case shaka.util.MediaElementEvent.PLAYING:
      case shaka.util.MediaElementEvent.SEEKED:
        state = shaka.media.BufferingObserver.State.SATISFIED;
        break;
      case shaka.util.MediaElementEvent.TIME_UPDATE:
        // Comparison with 0.01s precision to avoid false positives
        // on low precision platforms (i.e. Xbox w/ Legacy Edge).
        if (previousTime >= 0 && (currentTime - previousTime) > 0.01) {
          state = shaka.media.BufferingObserver.State.SATISFIED;
        }
        break;
    }
    return state !== undefined ? this.setState(state) : false;
  }
};

/**
 * Rather than using booleans to communicate what state we are in, we have this
 * enum.
 *
 * @enum {number}
 */
shaka.media.BufferingObserver.State = {
  STARVING: 0,
  SATISFIED: 1,
};
