// 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) {
  shared.sequenceNumber = 0;

  var AnimationEvent = function (target, currentTime, timelineTime) {
    this.target = target;
    this.currentTime = currentTime;
    this.timelineTime = timelineTime;
    this.type = 'finish';
    this.bubbles = false;
    this.cancelable = false;
    this.currentTarget = target;
    this.defaultPrevented = false;
    this.eventPhase = Event.AT_TARGET;
    this.timeStamp = Date.now();
  };

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

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

    this._sequenceNumber = shared.sequenceNumber++;
    this._currentTime = 0;
    this._startTime = null;
    this._paused = false;
    this._playbackRate = 1;
    this._inTimeline = true;
    this._finishedFlag = true;
    this.onfinish = null;
    this._finishHandlers = [];
    this._effect = effect;
    this._inEffect = this._effect._update(0);
    this._idle = true;
    this._currentTimePending = false;
  };

  scope.Animation.prototype = {
    _ensureAlive: function () {
      // If an animation is playing backwards and is not fill backwards/both
      // then it should go out of effect when it reaches the start of its
      // active interval (currentTime == 0).
      if (this.playbackRate < 0 && this.currentTime === 0) {
        this._inEffect = this._effect._update(-1);
      } else {
        this._inEffect = this._effect._update(this.currentTime);
      }

      if (!this._inTimeline && (this._inEffect || !this._finishedFlag)) {
        this._inTimeline = true;

        scope.timeline._animations.push(this);
      }
    },
    _tickCurrentTime: function (newTime, ignoreLimit) {
      if (newTime != this._currentTime) {
        this._currentTime = newTime;
        if (this._isFinished && !ignoreLimit) this._currentTime = this._playbackRate > 0 ? this._totalDuration : 0;

        this._ensureAlive();
      }
    },

    get currentTime() {
      if (this._idle || this._currentTimePending) return null;
      return this._currentTime;
    },

    set currentTime(newTime) {
      newTime = +newTime;
      if (isNaN(newTime)) return;
      scope.restart();

      if (!this._paused && this._startTime != null) {
        this._startTime = this._timeline.currentTime - newTime / this._playbackRate;
      }

      this._currentTimePending = false;
      if (this._currentTime == newTime) return;

      if (this._idle) {
        this._idle = false;
        this._paused = true;
      }

      this._tickCurrentTime(newTime, true);

      scope.applyDirtiedAnimation(this);
    },

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

    set startTime(newTime) {
      newTime = +newTime;
      if (isNaN(newTime)) return;
      if (this._paused || this._idle) return;
      this._startTime = newTime;

      this._tickCurrentTime((this._timeline.currentTime - this._startTime) * this.playbackRate);

      scope.applyDirtiedAnimation(this);
    },

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

    set playbackRate(value) {
      if (value == this._playbackRate) {
        return;
      }

      var oldCurrentTime = this.currentTime;
      this._playbackRate = value;
      this._startTime = null;

      if (this.playState != 'paused' && this.playState != 'idle') {
        this._finishedFlag = false;
        this._idle = false;

        this._ensureAlive();

        scope.applyDirtiedAnimation(this);
      }

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

    get _isFinished() {
      return !this._idle && (this._playbackRate > 0 && this._currentTime >= this._totalDuration || this._playbackRate < 0 && this._currentTime <= 0);
    },

    get _totalDuration() {
      return this._effect._totalDuration;
    },

    get playState() {
      if (this._idle) return 'idle';
      if (this._startTime == null && !this._paused && this.playbackRate != 0 || this._currentTimePending) return 'pending';
      if (this._paused) return 'paused';
      if (this._isFinished) return 'finished';
      return 'running';
    },

    _rewind: function () {
      if (this._playbackRate >= 0) {
        this._currentTime = 0;
      } else if (this._totalDuration < Infinity) {
        this._currentTime = this._totalDuration;
      } else {
        throw new DOMException('Unable to rewind negative playback rate animation with infinite duration', 'InvalidStateError');
      }
    },
    play: function () {
      this._paused = false;

      if (this._isFinished || this._idle) {
        this._rewind();

        this._startTime = null;
      }

      this._finishedFlag = false;
      this._idle = false;

      this._ensureAlive();

      scope.applyDirtiedAnimation(this);
    },
    pause: function () {
      if (!this._isFinished && !this._paused && !this._idle) {
        this._currentTimePending = true;
      } else if (this._idle) {
        this._rewind();

        this._idle = false;
      }

      this._startTime = null;
      this._paused = true;
    },
    finish: function () {
      if (this._idle) return;
      this.currentTime = this._playbackRate > 0 ? this._totalDuration : 0;
      this._startTime = this._totalDuration - this.currentTime;
      this._currentTimePending = false;
      scope.applyDirtiedAnimation(this);
    },
    cancel: function () {
      if (!this._inEffect) return;
      this._inEffect = false;
      this._idle = true;
      this._paused = false;
      this._finishedFlag = true;
      this._currentTime = 0;
      this._startTime = null;

      this._effect._update(null); // effects are invalid after cancellation as the animation state
      // needs to un-apply.


      scope.applyDirtiedAnimation(this);
    },
    reverse: function () {
      this.playbackRate *= -1;
      this.play();
    },
    addEventListener: function (type, handler) {
      if (typeof handler == 'function' && type == 'finish') this._finishHandlers.push(handler);
    },
    removeEventListener: function (type, handler) {
      if (type != 'finish') return;

      var index = this._finishHandlers.indexOf(handler);

      if (index >= 0) this._finishHandlers.splice(index, 1);
    },
    _fireEvents: function (baseTime) {
      if (this._isFinished) {
        if (!this._finishedFlag) {
          var event = new AnimationEvent(this, this._currentTime, baseTime);

          var handlers = this._finishHandlers.concat(this.onfinish ? [this.onfinish] : []);

          setTimeout(function () {
            handlers.forEach(function (handler) {
              handler.call(event.target, event);
            });
          }, 0);
          this._finishedFlag = true;
        }
      } else {
        this._finishedFlag = false;
      }
    },
    _tick: function (timelineTime, isAnimationFrame) {
      if (!this._idle && !this._paused) {
        if (this._startTime == null) {
          if (isAnimationFrame) {
            this.startTime = timelineTime - this._currentTime / this.playbackRate;
          }
        } else if (!this._isFinished) {
          this._tickCurrentTime((timelineTime - this._startTime) * this.playbackRate);
        }
      }

      if (isAnimationFrame) {
        this._currentTimePending = false;

        this._fireEvents(timelineTime);
      }
    },

    get _needsTick() {
      return this.playState in {
        'pending': 1,
        'running': 1
      } || !this._finishedFlag;
    },

    _targetAnimations: function () {
      var target = this._effect._target;

      if (!target._activeAnimations) {
        target._activeAnimations = [];
      }

      return target._activeAnimations;
    },
    _markTarget: function () {
      var animations = this._targetAnimations();

      if (animations.indexOf(this) === -1) {
        animations.push(this);
      }
    },
    _unmarkTarget: function () {
      var animations = this._targetAnimations();

      var index = animations.indexOf(this);

      if (index !== -1) {
        animations.splice(index, 1);
      }
    }
  };

  if (WEB_ANIMATIONS_TESTING) {
    testing.webAnimations1Animation = scope.Animation;
  }
})(webAnimationsShared, webAnimations1, webAnimationsTesting);