// 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);
