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

  function groupChildDuration(node) {
    return node._timing.delay + node.activeDuration + node._timing.endDelay;
  }

  function constructor(children, timingInput, id) {
    this._id = id;
    this._parent = null;
    this.children = children || [];
    this._reparent(this.children);
    timingInput = shared.numericTimingToObject(timingInput);
    this._timingInput = shared.cloneTimingInput(timingInput);
    this._timing = shared.normalizeTimingInput(timingInput, true);
    this.timing = shared.makeTiming(timingInput, true, this);
    this.timing._effect = this;

    if (this._timing.duration === 'auto') {
      this._timing.duration = this.activeDuration;
    }
  }

  window.SequenceEffect = function() {
    constructor.apply(this, arguments);
  };

  window.GroupEffect = function() {
    constructor.apply(this, arguments);
  };

  constructor.prototype = {
    _isAncestor: function(effect) {
      var a = this;
      while (a !== null) {
        if (a == effect)
          return true;
        a = a._parent;
      }
      return false;
    },
    _rebuild: function() {
      // Re-calculate durations for ancestors with specified duration 'auto'.
      var node = this;
      while (node) {
        if (node.timing.duration === 'auto') {
          node._timing.duration = node.activeDuration;
        }
        node = node._parent;
      }
      if (this._animation) {
        this._animation._rebuildUnderlyingAnimation();
      }
    },
    _reparent: function(newChildren) {
      scope.removeMulti(newChildren);
      for (var i = 0; i < newChildren.length; i++) {
        newChildren[i]._parent = this;
      }
    },
    _putChild: function(args, isAppend) {
      var message = isAppend ? 'Cannot append an ancestor or self' : 'Cannot prepend an ancestor or self';
      for (var i = 0; i < args.length; i++) {
        if (this._isAncestor(args[i])) {
          throw {
            type: DOMException.HIERARCHY_REQUEST_ERR,
            name: 'HierarchyRequestError',
            message: message
          };
        }
      }
      var oldParents = [];
      for (var i = 0; i < args.length; i++) {
        isAppend ? this.children.push(args[i]) : this.children.unshift(args[i]);
      }
      this._reparent(args);
      this._rebuild();
    },
    append: function()  {
      this._putChild(arguments, true);
    },
    prepend: function()  {
      this._putChild(arguments, false);
    },
    get parent() {
      return this._parent;
    },
    get firstChild() {
      return this.children.length ? this.children[0] : null;
    },
    get lastChild() {
      return this.children.length ? this.children[this.children.length - 1] : null;
    },
    clone: function() {
      var clonedTiming = shared.cloneTimingInput(this._timingInput);
      var clonedChildren = [];
      for (var i = 0; i < this.children.length; i++) {
        clonedChildren.push(this.children[i].clone());
      }
      return (this instanceof GroupEffect) ?
          new GroupEffect(clonedChildren, clonedTiming) :
          new SequenceEffect(clonedChildren, clonedTiming);
    },
    remove: function() {
      scope.removeMulti([this]);
    }
  };

  window.SequenceEffect.prototype = Object.create(constructor.prototype);
  Object.defineProperty(
      window.SequenceEffect.prototype,
      'activeDuration',
      {
        get: function() {
          var total = 0;
          this.children.forEach(function(child) {
            total += groupChildDuration(child);
          });
          return Math.max(total, 0);
        }
      });

  window.GroupEffect.prototype = Object.create(constructor.prototype);
  Object.defineProperty(
      window.GroupEffect.prototype,
      'activeDuration',
      {
        get: function() {
          var max = 0;
          this.children.forEach(function(child) {
            max = Math.max(max, groupChildDuration(child));
          });
          return max;
        }
      });

  scope.newUnderlyingAnimationForGroup = function(group) {
    var underlyingAnimation;
    var timing = null;
    var ticker = function(tf) {
      var animation = underlyingAnimation._wrapper;
      if (!animation) {
        return;
      }
      if (animation.playState == 'pending') {
        return;
      }
      if (!animation.effect) {
        return;
      }
      if (tf == null) {
        animation._removeChildAnimations();
        return;
      }

      // If the group has a negative playback rate and is not fill backwards/both, then it should go
      // out of effect when it reaches the start of its active interval (tf == 0). If it is fill
      // backwards/both then it should stay in effect. calculateIterationProgress will return 0 in the
      // backwards-filling case, and null otherwise.
      if (tf == 0 && animation.playbackRate < 0) {
        if (!timing) {
          timing = shared.normalizeTimingInput(animation.effect.timing);
        }
        tf = shared.calculateIterationProgress(shared.calculateActiveDuration(timing), -1, timing);
        if (isNaN(tf) || tf == null) {
          animation._forEachChild(function(child) {
            child.currentTime = -1;
          });
          animation._removeChildAnimations();
          return;
        }
      }
    };

    var underlyingEffect = new KeyframeEffect(null, [], group._timing, group._id);
    underlyingEffect.onsample = ticker;
    underlyingAnimation = scope.timeline._play(underlyingEffect);
    return underlyingAnimation;
  };

  scope.bindAnimationForGroup = function(animation) {
    animation._animation._wrapper = animation;
    animation._isGroup = true;
    scope.awaitStartTime(animation);
    animation._constructChildAnimations();
    animation._setExternalAnimation(animation);
  };

  scope.groupChildDuration = groupChildDuration;

})(webAnimationsShared, webAnimationsNext, webAnimationsTesting);
