import defaultValue from "./defaultValue.js";
import defined from "./defined.js";
import destroyObject from "./destroyObject.js";
import Iso8601 from "./Iso8601.js";
import JulianDate from "./JulianDate.js";

/**
 * Synchronizes a video element with a simulation clock.
 *
 * @alias VideoSynchronizer
 * @constructor
 *
 * @param {Object} [options] Object with the following properties:
 * @param {Clock} [options.clock] The clock instance used to drive the video.
 * @param {HTMLVideoElement} [options.element] The video element to be synchronized.
 * @param {JulianDate} [options.epoch=Iso8601.MINIMUM_VALUE] The simulation time that marks the start of the video.
 * @param {Number} [options.tolerance=1.0] The maximum amount of time, in seconds, that the clock and video can diverge.
 *
 * @demo {@link https://sandcastle.cesium.com/index.html?src=Video.html|Video Material Demo}
 */
function VideoSynchronizer(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  this._clock = undefined;
  this._element = undefined;
  this._clockSubscription = undefined;
  this._seekFunction = undefined;
  this._lastPlaybackRate = undefined;

  this.clock = options.clock;
  this.element = options.element;

  /**
   * Gets or sets the simulation time that marks the start of the video.
   * @type {JulianDate}
   * @default Iso8601.MINIMUM_VALUE
   */
  this.epoch = defaultValue(options.epoch, Iso8601.MINIMUM_VALUE);

  /**
   * Gets or sets the amount of time in seconds the video's currentTime
   * and the clock's currentTime can diverge before a video seek is performed.
   * Lower values make the synchronization more accurate but video
   * performance might suffer.  Higher values provide better performance
   * but at the cost of accuracy.
   * @type {Number}
   * @default 1.0
   */
  this.tolerance = defaultValue(options.tolerance, 1.0);

  this._seeking = false;
  this._seekFunction = undefined;
  this._firstTickAfterSeek = false;
}

Object.defineProperties(VideoSynchronizer.prototype, {
  /**
   * Gets or sets the clock used to drive the video element.
   *
   * @memberof VideoSynchronizer.prototype
   * @type {Clock}
   */
  clock: {
    get: function () {
      return this._clock;
    },
    set: function (value) {
      var oldValue = this._clock;

      if (oldValue === value) {
        return;
      }

      if (defined(oldValue)) {
        this._clockSubscription();
        this._clockSubscription = undefined;
      }

      if (defined(value)) {
        this._clockSubscription = value.onTick.addEventListener(
          VideoSynchronizer.prototype._onTick,
          this
        );
      }

      this._clock = value;
    },
  },
  /**
   * Gets or sets the video element to synchronize.
   *
   * @memberof VideoSynchronizer.prototype
   * @type {HTMLVideoElement}
   */
  element: {
    get: function () {
      return this._element;
    },
    set: function (value) {
      var oldValue = this._element;

      if (oldValue === value) {
        return;
      }

      if (defined(oldValue)) {
        oldValue.removeEventListener("seeked", this._seekFunction, false);
      }

      if (defined(value)) {
        this._seeking = false;
        this._seekFunction = createSeekFunction(this);
        value.addEventListener("seeked", this._seekFunction, false);
      }

      this._element = value;
      this._seeking = false;
      this._firstTickAfterSeek = false;
    },
  },
});

/**
 * Destroys and resources used by the object.  Once an object is destroyed, it should not be used.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 */
VideoSynchronizer.prototype.destroy = function () {
  this.element = undefined;
  this.clock = undefined;
  return destroyObject(this);
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 *
 * @returns {Boolean} True if this object was destroyed; otherwise, false.
 */
VideoSynchronizer.prototype.isDestroyed = function () {
  return false;
};

VideoSynchronizer.prototype._trySetPlaybackRate = function (clock) {
  if (this._lastPlaybackRate === clock.multiplier) {
    return;
  }

  var element = this._element;
  try {
    element.playbackRate = clock.multiplier;
  } catch (error) {
    // Seek manually for unsupported playbackRates.
    element.playbackRate = 0.0;
  }
  this._lastPlaybackRate = clock.multiplier;
};

VideoSynchronizer.prototype._onTick = function (clock) {
  var element = this._element;
  if (!defined(element) || element.readyState < 2) {
    return;
  }

  var paused = element.paused;
  var shouldAnimate = clock.shouldAnimate;
  if (shouldAnimate === paused) {
    if (shouldAnimate) {
      element.play();
    } else {
      element.pause();
    }
  }

  //We need to avoid constant seeking or the video will
  //never contain a complete frame for us to render.
  //So don't do anything if we're seeing or on the first
  //tick after a seek (the latter of which allows the frame
  //to actually be rendered.
  if (this._seeking || this._firstTickAfterSeek) {
    this._firstTickAfterSeek = false;
    return;
  }

  this._trySetPlaybackRate(clock);

  var clockTime = clock.currentTime;
  var epoch = defaultValue(this.epoch, Iso8601.MINIMUM_VALUE);
  var videoTime = JulianDate.secondsDifference(clockTime, epoch);

  var duration = element.duration;
  var desiredTime;
  var currentTime = element.currentTime;
  if (element.loop) {
    videoTime = videoTime % duration;
    if (videoTime < 0.0) {
      videoTime = duration - videoTime;
    }
    desiredTime = videoTime;
  } else if (videoTime > duration) {
    desiredTime = duration;
  } else if (videoTime < 0.0) {
    desiredTime = 0.0;
  } else {
    desiredTime = videoTime;
  }

  //If the playing video's time and the scene's clock time
  //ever drift too far apart, we want to set the video to match
  var tolerance = shouldAnimate ? defaultValue(this.tolerance, 1.0) : 0.001;
  if (Math.abs(desiredTime - currentTime) > tolerance) {
    this._seeking = true;
    element.currentTime = desiredTime;
  }
};

function createSeekFunction(that) {
  return function () {
    that._seeking = false;
    that._firstTickAfterSeek = true;
  };
}
export default VideoSynchronizer;
