// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//     You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//     See the License for the specific language governing permissions and
// limitations under the License.
(function (shared, scope, testing) {
  scope.animationsWithPromises = [];

  scope.Animation = function (effect, timeline) {
    this.id = '';

    if (effect && effect._id) {
      this.id = effect._id;
    }

    this.effect = effect;

    if (effect) {
      effect._animation = this;
    }

    if (!timeline) {
      throw new Error('Animation with null timeline is not supported');
    }

    this._timeline = timeline;
    this._sequenceNumber = shared.sequenceNumber++;
    this._holdTime = 0;
    this._paused = false;
    this._isGroup = false;
    this._animation = null;
    this._childAnimations = [];
    this._callback = null;
    this._oldPlayState = 'idle';

    this._rebuildUnderlyingAnimation(); // Animations are constructed in the idle state.


    this._animation.cancel();

    this._updatePromises();
  };

  scope.Animation.prototype = {
    _updatePromises: function () {
      var oldPlayState = this._oldPlayState;
      var newPlayState = this.playState;

      if (this._readyPromise && newPlayState !== oldPlayState) {
        if (newPlayState == 'idle') {
          this._rejectReadyPromise();

          this._readyPromise = undefined;
        } else if (oldPlayState == 'pending') {
          this._resolveReadyPromise();
        } else if (newPlayState == 'pending') {
          this._readyPromise = undefined;
        }
      }

      if (this._finishedPromise && newPlayState !== oldPlayState) {
        if (newPlayState == 'idle') {
          this._rejectFinishedPromise();

          this._finishedPromise = undefined;
        } else if (newPlayState == 'finished') {
          this._resolveFinishedPromise();
        } else if (oldPlayState == 'finished') {
          this._finishedPromise = undefined;
        }
      }

      this._oldPlayState = this.playState;
      return this._readyPromise || this._finishedPromise;
    },
    _rebuildUnderlyingAnimation: function () {
      this._updatePromises();

      var oldPlaybackRate;
      var oldPaused;
      var oldStartTime;
      var oldCurrentTime;
      var hadUnderlying = this._animation ? true : false;

      if (hadUnderlying) {
        oldPlaybackRate = this.playbackRate;
        oldPaused = this._paused;
        oldStartTime = this.startTime;
        oldCurrentTime = this.currentTime;

        this._animation.cancel();

        this._animation._wrapper = null;
        this._animation = null;
      }

      if (!this.effect || this.effect instanceof window.KeyframeEffect) {
        this._animation = scope.newUnderlyingAnimationForKeyframeEffect(this.effect);
        scope.bindAnimationForKeyframeEffect(this);
      }

      if (this.effect instanceof window.SequenceEffect || this.effect instanceof window.GroupEffect) {
        this._animation = scope.newUnderlyingAnimationForGroup(this.effect);
        scope.bindAnimationForGroup(this);
      }

      if (this.effect && this.effect._onsample) {
        scope.bindAnimationForCustomEffect(this);
      }

      if (hadUnderlying) {
        if (oldPlaybackRate != 1) {
          this.playbackRate = oldPlaybackRate;
        }

        if (oldStartTime !== null) {
          this.startTime = oldStartTime;
        } else if (oldCurrentTime !== null) {
          this.currentTime = oldCurrentTime;
        } else if (this._holdTime !== null) {
          this.currentTime = this._holdTime;
        }

        if (oldPaused) {
          this.pause();
        }
      }

      this._updatePromises();
    },
    _updateChildren: function () {
      if (!this.effect || this.playState == 'idle') return;
      var offset = this.effect._timing.delay;

      this._childAnimations.forEach(function (childAnimation) {
        this._arrangeChildren(childAnimation, offset);

        if (this.effect instanceof window.SequenceEffect) offset += scope.groupChildDuration(childAnimation.effect);
      }.bind(this));
    },
    _setExternalAnimation: function (animation) {
      if (!this.effect || !this._isGroup) return;

      for (var i = 0; i < this.effect.children.length; i++) {
        this.effect.children[i]._animation = animation;

        this._childAnimations[i]._setExternalAnimation(animation);
      }
    },
    _constructChildAnimations: function () {
      if (!this.effect || !this._isGroup) return;
      var offset = this.effect._timing.delay;

      this._removeChildAnimations();

      this.effect.children.forEach(function (child) {
        var childAnimation = scope.timeline._play(child);

        this._childAnimations.push(childAnimation);

        childAnimation.playbackRate = this.playbackRate;
        if (this._paused) childAnimation.pause();
        child._animation = this.effect._animation;

        this._arrangeChildren(childAnimation, offset);

        if (this.effect instanceof window.SequenceEffect) offset += scope.groupChildDuration(child);
      }.bind(this));
    },
    _arrangeChildren: function (childAnimation, offset) {
      if (this.startTime === null) {
        childAnimation.currentTime = this.currentTime - offset / this.playbackRate;
      } else if (childAnimation.startTime !== this.startTime + offset / this.playbackRate) {
        childAnimation.startTime = this.startTime + offset / this.playbackRate;
      }
    },

    get timeline() {
      return this._timeline;
    },

    get playState() {
      return this._animation ? this._animation.playState : 'idle';
    },

    get finished() {
      if (!window.Promise) {
        console.warn('Animation Promises require JavaScript Promise constructor');
        return null;
      }

      if (!this._finishedPromise) {
        if (scope.animationsWithPromises.indexOf(this) == -1) {
          scope.animationsWithPromises.push(this);
        }

        this._finishedPromise = new Promise(function (resolve, reject) {
          this._resolveFinishedPromise = function () {
            resolve(this);
          };

          this._rejectFinishedPromise = function () {
            reject({
              type: DOMException.ABORT_ERR,
              name: 'AbortError'
            });
          };
        }.bind(this));

        if (this.playState == 'finished') {
          this._resolveFinishedPromise();
        }
      }

      return this._finishedPromise;
    },

    get ready() {
      if (!window.Promise) {
        console.warn('Animation Promises require JavaScript Promise constructor');
        return null;
      }

      if (!this._readyPromise) {
        if (scope.animationsWithPromises.indexOf(this) == -1) {
          scope.animationsWithPromises.push(this);
        }

        this._readyPromise = new Promise(function (resolve, reject) {
          this._resolveReadyPromise = function () {
            resolve(this);
          };

          this._rejectReadyPromise = function () {
            reject({
              type: DOMException.ABORT_ERR,
              name: 'AbortError'
            });
          };
        }.bind(this));

        if (this.playState !== 'pending') {
          this._resolveReadyPromise();
        }
      }

      return this._readyPromise;
    },

    get onfinish() {
      return this._animation.onfinish;
    },

    set onfinish(v) {
      if (typeof v == 'function') {
        this._animation.onfinish = function (e) {
          e.target = this;
          v.call(this, e);
        }.bind(this);
      } else {
        this._animation.onfinish = v;
      }
    },

    get oncancel() {
      return this._animation.oncancel;
    },

    set oncancel(v) {
      if (typeof v == 'function') {
        this._animation.oncancel = function (e) {
          e.target = this;
          v.call(this, e);
        }.bind(this);
      } else {
        this._animation.oncancel = v;
      }
    },

    get currentTime() {
      this._updatePromises();

      var currentTime = this._animation.currentTime;

      this._updatePromises();

      return currentTime;
    },

    set currentTime(v) {
      this._updatePromises();

      this._animation.currentTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE;

      this._register();

      this._forEachChild(function (child, offset) {
        child.currentTime = v - offset;
      });

      this._updatePromises();
    },

    get startTime() {
      return this._animation.startTime;
    },

    set startTime(v) {
      this._updatePromises();

      this._animation.startTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE;

      this._register();

      this._forEachChild(function (child, offset) {
        child.startTime = v + offset;
      });

      this._updatePromises();
    },

    get playbackRate() {
      return this._animation.playbackRate;
    },

    set playbackRate(value) {
      this._updatePromises();

      var oldCurrentTime = this.currentTime;
      this._animation.playbackRate = value;

      this._forEachChild(function (childAnimation) {
        childAnimation.playbackRate = value;
      });

      if (oldCurrentTime !== null) {
        this.currentTime = oldCurrentTime;
      }

      this._updatePromises();
    },

    play: function () {
      this._updatePromises();

      this._paused = false;

      this._animation.play();

      if (this._timeline._animations.indexOf(this) == -1) {
        this._timeline._animations.push(this);
      }

      this._register();

      scope.awaitStartTime(this);

      this._forEachChild(function (child) {
        var time = child.currentTime;
        child.play();
        child.currentTime = time;
      });

      this._updatePromises();
    },
    pause: function () {
      this._updatePromises();

      if (this.currentTime) {
        this._holdTime = this.currentTime;
      }

      this._animation.pause();

      this._register();

      this._forEachChild(function (child) {
        child.pause();
      });

      this._paused = true;

      this._updatePromises();
    },
    finish: function () {
      this._updatePromises();

      this._animation.finish();

      this._register();

      this._updatePromises();
    },
    cancel: function () {
      this._updatePromises();

      this._animation.cancel();

      this._register();

      this._removeChildAnimations();

      this._updatePromises();
    },
    reverse: function () {
      this._updatePromises();

      var oldCurrentTime = this.currentTime;

      this._animation.reverse();

      this._forEachChild(function (childAnimation) {
        childAnimation.reverse();
      });

      if (oldCurrentTime !== null) {
        this.currentTime = oldCurrentTime;
      }

      this._updatePromises();
    },
    addEventListener: function (type, handler) {
      var wrapped = handler;

      if (typeof handler == 'function') {
        wrapped = function (e) {
          e.target = this;
          handler.call(this, e);
        }.bind(this);

        handler._wrapper = wrapped;
      }

      this._animation.addEventListener(type, wrapped);
    },
    removeEventListener: function (type, handler) {
      this._animation.removeEventListener(type, handler && handler._wrapper || handler);
    },
    _removeChildAnimations: function () {
      while (this._childAnimations.length) this._childAnimations.pop().cancel();
    },
    _forEachChild: function (f) {
      var offset = 0;
      if (this.effect.children && this._childAnimations.length < this.effect.children.length) this._constructChildAnimations();

      this._childAnimations.forEach(function (child) {
        f.call(this, child, offset);
        if (this.effect instanceof window.SequenceEffect) offset += child.effect.activeDuration;
      }.bind(this));

      if (this.playState == 'pending') return;
      var timing = this.effect._timing;
      var t = this.currentTime;
      if (t !== null) t = shared.calculateIterationProgress(shared.calculateActiveDuration(timing), t, timing);
      if (t == null || isNaN(t)) this._removeChildAnimations();
    }
  };
  window.Animation = scope.Animation;

  if (WEB_ANIMATIONS_TESTING) {
    testing.webAnimationsNextAnimation = scope.Animation;
  }
})(webAnimationsShared, webAnimationsNext, webAnimationsTesting);