<commit_before>/**
 * Copyright 2012 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() {

var inherits = function(child, parent) {
  var tmp = function() {};
  tmp.prototype = parent.prototype;
  child.$super = parent;
  child.prototype = new tmp();
  child.prototype.constructor = child;
};

var mixin = function(target, source) {
  for (var k in source) {
    target[k] = source[k];
  }
};

var strip = function(str) {
  return str.replace(/^\s+/, '').replace(/\s+$/, '');
};

var IndexSizeError = function(message) {
  IndexSizeError.$super.call(this);
  this.name = "IndexSizeError";
  this.message = message;
}

inherits(IndexSizeError, Error);

/** @constructor */
var Timing = function(timingDict) {
  this.startDelay = timingDict.startDelay || 0.0;
  this.duration = timingDict.duration;
  if (this.duration < 0.0) {
    throw new IndexSizeError('duration must be >= 0');
  }
  this.iterationCount = isDefined(timingDict.iterationCount) ?
      timingDict.iterationCount : 1.0;
  if (this.iterationCount < 0.0) {
    throw new IndexSizeError('iterationCount must be >= 0');
  }
  this.iterationStart = timingDict.iterationStart || 0.0;
  if (this.iterationStart < 0.0) {
    throw new IndexSizeError('iterationStart must be >= 0');
  }
  this.playbackRate = isDefined(timingDict.playbackRate) ?
      timingDict.playbackRate : 1.0;
  //this.playbackRate = timingDict.playbackRate || 1.0;
  this.direction = timingDict.direction || 'normal';
  if (typeof timingDict.timingFunction === 'string') {
    this.timingFunction = TimingFunction.createFromString(timingDict.timingFunction);
  } else {
    this.timingFunction = timingDict.timingFunction;
  }
  this.fillMode = timingDict.fillMode || 'forwards';
};

mixin(Timing.prototype, {
  clone: function() {
    return new Timing({
      startDelay: this.startDelay,
      duration: this.duration,
      iterationCount: this.iterationCount,
      iterationStart: this.iterationStart,
      playbackRate: this.playbackRate,
      direction: this.direction,
      timingFunction: this.timingFunction ? this.timingFunction.clone() : null,
      fillMode: this.fillMode
    });
  },
});

/** @constructor */
var TimingProxy = function(timing, setter) {
  this._timing = timing;
  this._setter = setter;
};

['startDelay', 'duration', 'iterationCount', 'iterationStart', 'playbackRate',
    'direction', 'timingFunction', 'fillMode'].forEach(function(s) {
  TimingProxy.prototype.__defineGetter__(s, function() {
    return this._timing[s];
  });
  TimingProxy.prototype.__defineSetter__(s, function(v) {
    var old = this._timing[s];
    this._timing[s] = v;
    try {
      this._setter(v);
    } catch (e) {
      this._timing[s] = old;
      throw e;
    }
  });
});

mixin(TimingProxy.prototype, {
  extractMutableTiming: function() {
    return new Timing({
      startDelay: this._timing.startDelay,
      duration: this._timing.duration,
      iterationCount: this._timing.iterationCount,
      iterationStart: this._timing.iterationStart,
      playbackRate: this._timing.playbackRate,
      direction: this._timing.direction,
      timingFunction: this._timing.timingFunction ?
                  this._timing.timingFunction.clone() : null,
      fillMode: this._timing.fillMode
    });
  },
  clone: function() {
    return this._timing.clone();
  }
});

var isDefined = function(val) {
  return typeof val !== 'undefined';
};

var isDefinedAndNotNull = function(val) {
  return isDefined(val) && (val !== null);
};

var ST_MANUAL = 0;
var ST_AUTO = 1;
var ST_FORCED = 2;

/** @constructor */
var TimedItem = function(timing, startTime, parentGroup) {
  this.timing = new TimingProxy(interpretTimingParam(timing), function() {
    this.updateIterationDuration();
  }.bind(this));
  this._startTime = startTime;
  this.currentIteration = null;
  this.iterationTime = null;
  this.animationTime = null;
  this._reversing = false;

  if (parentGroup === null || parentGroup instanceof AnimationGroup) {
    this.parentGroup = parentGroup;
  } else if (!isDefined(parentGroup)) {
    this.parentGroup = DEFAULT_GROUP;
  } else {
    throw new TypeError('parentGroup is not an AnimationGroup');
  }

  if (!isDefined(startTime)) {
    this._startTimeMode = ST_AUTO;
    // We take _effectiveParentTime at the moment this TimedItem is
    // created. Note that the call to _addChild() below may cause the parent
    // to update its timing properties, including its iterationTime.
    this._startTime = this._effectiveParentTime;
  } else {
    this._startTimeMode = ST_MANUAL;
    this._startTime = startTime;
  }

  this._timeDrift = 0;
  this._locallyPaused = false;

  if (this.parentGroup) {
    this.parentGroup._addChild(this);
  }
  this.updateIterationDuration();
  this._pauseStartTime = 0;
};

TimedItem.prototype.__defineGetter__('timeDrift', function() {
  if (this.locallyPaused) {
    return this._effectiveParentTime - this.startTime -
        this._pauseStartTime;
  }
  return this._timeDrift;
});
TimedItem.prototype.__defineGetter__('_effectiveParentTime', function() {
  return this.parentGroup !== null && this.parentGroup.iterationTime !== null ?
      this.parentGroup.iterationTime : 0;
});
TimedItem.prototype.__defineGetter__('currentTime', function() {
  return this._effectiveParentTime - this._startTime - this.timeDrift;
});
TimedItem.prototype.__defineSetter__('currentTime', function(seekTime) {
  if (this._locallyPaused) {
    this._pauseStartTime = seekTime;
  } else {
    this._timeDrift = this._effectiveParentTime - this._startTime -
        seekTime;
  }
  this.updateTimeMarkers();
  if (this.parentGroup) {
    this.parentGroup._childrenStateModified();
  }
});
TimedItem.prototype.__defineGetter__('startTime', function() {
  return this._startTime;
});
TimedItem.prototype.__defineSetter__('startTime', function(newStartTime) {
  if (this.parentGroup && this.parentGroup.type === 'seq') {
    throw new Error('NoModificationAllowedError');
  }
  this._startTime = newStartTime;
  this._startTimeMode = ST_MANUAL;
  this.updateTimeMarkers();
  if (this.parentGroup) {
    this.parentGroup._childrenStateModified();
  }
});
TimedItem.prototype.__defineGetter__('locallyPaused', function() {
  return this._locallyPaused;
});
TimedItem.prototype.__defineSetter__('locallyPaused', function(newVal) {
  if (this._locallyPaused === newVal) {
    return;
  }
  if (this._locallyPaused) {
    this._timeDrift = this._effectiveParentTime - this.startTime -
        this._pauseStartTime;
  } else {
    this._pauseStartTime = this.currentTime;
  }
  this._locallyPaused = newVal;
  this.updateTimeMarkers();
});
TimedItem.prototype.__defineGetter__('paused', function() {
  return this.locallyPaused ||
      (isDefinedAndNotNull(this.parentGroup) && this.parentGroup.paused);
});

mixin(TimedItem.prototype, {
  reparent: function(parentGroup) {
    if (this.parentGroup) {
      this.parentGroup.remove(this.parentGroup.indexOf(this), 1);
    }
    this.parentGroup = parentGroup;
    this._timeDrift = 0;
    if (this._startTimeMode == ST_FORCED &&
        (!this.parentGroup || this.parentGroup.type != 'seq')) {
      this._startTime = this._stashedStartTime;
      this._startTimeMode = this._stashedStartTimeMode;
    }
    if (this._startTimeMode == ST_AUTO) {
      this._startTime = this._effectiveParentTime;
    }
    this.updateTimeMarkers();
  },
  // TODO: take timing.iterationStart into account. Spec needs to as well.
  updateIterationDuration: function() {
    if (isDefined(this.timing.duration)) {
      this.duration = this.timing.duration;
    } else {
      this.duration = this.intrinsicDuration();
    }
    // Section 6.10: Calculating the intrinsic animation duration
    var repeatedDuration = this.duration * this.timing.iterationCount;
    this.animationDuration = repeatedDuration /
        Math.abs(this.timing.playbackRate);
    this.updateTimeMarkers();
    if (this.parentGroup) {
      this.parentGroup._childrenStateModified();
    }
  },
  updateTimeMarkers: function(parentTime) {
    if (this.locallyPaused) {
      this.endTime = Infinity;
    } else {
      this.endTime = this._startTime + this.animationDuration +
          this.timing.startDelay + this.timeDrift;
    }
    if (this.parentGroup !== null && this.parentGroup.iterationTime !== null) {
      this.itemTime = this.parentGroup.iterationTime -
          this._startTime - this.timeDrift;
    } else if (isDefined(parentTime)) {
      this.itemTime = parentTime;
    } else {
      this.itemTime = null;
    }
    if (this.itemTime !== null) {
      if (this.itemTime <= this.timing.startDelay) {
        if (((this.timing.fillMode == 'backwards') && !this._reversing)
          || this.timing.fillMode == 'both'
          || ((this.timing.fillMode == 'forwards') && this._reversing)) {
          this.animationTime = 0;
        } else {
          this.animationTime = null;
        }
      } else if (this.itemTime <
          this.timing.startDelay + this.animationDuration) {
        this.animationTime = this.itemTime - this.timing.startDelay;
      } else {
        if (((this.timing.fillMode == 'forwards') && !this._reversing)
          || this.timing.fillMode == 'both'
          || ((this.timing.fillMode == 'backwards') && this._reversing)) {
          this.animationTime = this.animationDuration;
        } else {
          this.animationTime = null;
        }
      }
      if (this.animationTime === null) {
        this.iterationTime = null;
        this.currentIteration = null;
        this._timeFraction = null;
      } else if (this.duration == 0) {
        this.iterationTime = 0;
        var isAtEndOfIterations = (this.timing.iterationCount != 0) &&
            ((this.itemTime < this.timing.startDelay) == this._reversing);
        this.currentIteration = isAtEndOfIterations ?
           this._floorWithOpenClosedRange(this.timing.iterationStart +
               this.timing.iterationCount, 1.0) :
           this._floorWithClosedOpenRange(this.timing.iterationStart, 1.0);
        // Equivalent to unscaledIterationTime below.
        var unscaledFraction = isAtEndOfIterations ?
            this._modulusWithOpenClosedRange(this.timing.iterationStart +
                this.timing.iterationCount, 1.0) :
            this._modulusWithClosedOpenRange(this.timing.iterationStart, 1.0);
        this._timeFraction = this._isCurrentDirectionForwards(
            this.timing.direction, this.currentIteration) ?
                unscaledFraction :
                1.0 - unscaledFraction;
        if (this.timing.timingFunction) {
          this._timeFraction = this.timing.timingFunction.scaleTime(
              this._timeFraction);
        }
      } else {
        var startOffset = this.timing.iterationStart * this.duration;
        var effectiveSpeed = this._reversing ?
            -this.timing.playbackRate : this.timing.playbackRate;
        if (effectiveSpeed < 0) {
          var adjustedAnimationTime = (this.animationTime -
              this.animationDuration) * effectiveSpeed + startOffset;
        } else {
          var adjustedAnimationTime = this.animationTime * effectiveSpeed +
              startOffset;
        }
        var repeatedDuration = this.duration * this.timing.iterationCount;
        var isAtEndOfIterations = (this.timing.iterationCount != 0) &&
            (adjustedAnimationTime - startOffset == repeatedDuration);
        this.currentIteration = isAtEndOfIterations ?
            this._floorWithOpenClosedRange(
                adjustedAnimationTime, this.duration) :
            this._floorWithClosedOpenRange(
                adjustedAnimationTime, this.duration);
        var unscaledIterationTime = isAtEndOfIterations ?
            this._modulusWithOpenClosedRange(
                adjustedAnimationTime, this.duration) :
            this._modulusWithClosedOpenRange(
                adjustedAnimationTime, this.duration);
        var scaledIterationTime = unscaledIterationTime;
        this.iterationTime = this._isCurrentDirectionForwards(
            this.timing.direction, this.currentIteration) ?
                scaledIterationTime :
                this.duration - scaledIterationTime;
        this._timeFraction = this.iterationTime / this.duration;
        if (this.timing.timingFunction) {
          this._timeFraction = this.timing.timingFunction.scaleTime(
              this._timeFraction);
          this.iterationTime = this._timeFraction * this.duration;
        }
      }
    } else {
      this.animationTime = null;
      this.iterationTime = null;
      this.currentIteration = null;
      this._timeFraction = null;
    }
    return this._timeFraction !== null;
  },
  pause: function() {
    this.locallyPaused = true;
  },
  seek: function(itemTime) {
    // TODO
  },
  changePlaybackRate: function(playbackRate) {
    var previousRate = this.timing.playbackRate;
    this.timing.playbackRate = playbackRate;
    if (previousRate == 0 || playbackRate == 0) {
      return;
    }
    // TODO: invert the fillMode?
    var seekAdjustment = (this.itemTime - this.timing.startDelay) *
        (1 - previousRate / playbackRate);
    this.currentTime = this.itemTime - seekAdjustment;
  },
  reverse: function() {
    if (this.currentTime === null) {
      var seekTime = 0;
    } else if (this.currentTime < this.timing.startDelay) {
      var seekTime = this.timing.startDelay + this.animationDuration;
    } else if (this.currentTime > this.timing.startDelay +
        this.animationDuration) {
      var seekTime = this.timing.startDelay;
    } else {
      var seekTime = this.timing.startDelay + this.animationDuration -
          this.currentTime;
    }

    this.currentTime = seekTime;
    this._reversing = !(this._reversing);
  },
  cancel: function() {
    if (this.parentGroup) {
      this.parentGroup.remove(this.parentGroup.indexOf(this), 1);
    }
  },
  play: function() {
    // TODO: This should unpause as well
    if (this.currentTime > this.animationDuration + this.timing.startDelay &&
        this.timing.playbackRate >= 0) {
      this.currentTime = this.timing.startDelay;
    }
    this.locallyPaused = false;
  },
  _floorWithClosedOpenRange: function(x, range) {
    return Math.floor(x / range);
  },
  _floorWithOpenClosedRange: function(x, range) {
    return Math.ceil(x / range) - 1;
  },
  _modulusWithClosedOpenRange: function(x, range) {
    return x % range;
  },
  _modulusWithOpenClosedRange: function(x, range) {
    var ret = this._modulusWithClosedOpenRange(x, range);
    return ret == 0 ? range : ret;
  },
  _isCurrentDirectionForwards: function(direction, currentIteration) {
    if (direction == 'normal') {
      return true;
    }
    if (direction == 'reverse') {
      return false;
    }
    var d = currentIteration;
    if (direction == 'alternate-reverse') {
      d += 1;
    }
    // TODO: 6.13.3 step 3. wtf?
    return d % 2 == 0;
  },
  _parentToGlobalTime: function(parentTime) {
    if (!this.parentGroup)
      return parentTime;
    return parentTime + DEFAULT_GROUP.currentTime -
        this.parentGroup.iterationTime;
  },
});

var interpretAnimationFunction = function(animationFunction) {
  if (animationFunction instanceof AnimationFunction) {
    return animationFunction;
  } else if (typeof(animationFunction) === 'object') {
    // Test if the object is actually a CustomAnimationFunction
    // (how does WebIDL actually differentiate different callback interfaces?)
    if (animationFunction.hasOwnProperty('sample') &&
        typeof(animationFunction.sample) === 'function') {
      return animationFunction;
    } else {
      return AnimationFunction.createFromProperties(animationFunction);
    }
  } else {
    try {
      throw new Error('TypeError');
    } catch (e) { console.log(e.stack); throw e; }
  }
};

var interpretTimingParam = function(timing) {
  if (!isDefinedAndNotNull(timing)) {
    return new Timing({});
  }
  if (timing instanceof Timing || timing instanceof TimingProxy) {
    return timing;
  }
  if (typeof(timing) === 'number') {
    return new Timing({duration: timing});
  }
  if (typeof(timing) === 'object') {
    return new Timing(timing);
  }
  throw new TypeError('timing parameters must be undefined, Timing objects, ' +
      'numbers, or timing dictionaries; not \'' + timing + '\'');
};

/** @constructor */
var Animation = function(target, animationFunction, timing, parentGroup, startTime) {
  this.animationFunction = interpretAnimationFunction(animationFunction);
  this.timing = interpretTimingParam(timing);

  Animation.$super.call(this, timing, startTime, parentGroup);

  this.targetElement = target;
  this.name = this.animationFunction instanceof KeyframesAnimationFunction ?
      this.animationFunction.property : '<anon>';
  this._sortOrder = 0;
};

inherits(Animation, TimedItem);
mixin(Animation.prototype, {
  intrinsicDuration: function() {
    // section 6.6
    return Infinity;
  },
  _sample: function() {
    this.animationFunction.sample(this._timeFraction,
        this.currentIteration, this.targetElement,
        this.underlyingValue);
  },
  _getActiveAnimations: function() {
    if (!this.updateTimeMarkers()) {
      return [];
    }

    this._sortOrder = this._parentToGlobalTime(this.startTime);
    return [this];
  },
  toString: function() {
    var funcDescr = this.animationFunction instanceof AnimationFunction ?
        this.animationFunction.toString() : 'Custom scripted function';
    return 'Animation ' + this.startTime + '-' + this.endTime + ' (' +
        this.timeDrift + ' @' + this.currentTime + ') ' + funcDescr;
  }
});

// To use this, need to have children and length member variables.
var AnimationListMixin = {
  initListMixin: function(onListChange) {
    this._clear();
    this.onListChange = onListChange;
  },
  clear: function() {
    this._clear();
    this.onListChange();
  },
  _clear: function() {
    this.children = [];
    var oldLength = this.length;
    this.length = 0;
    this._deleteIdxAccessors(0, oldLength);
    // TODO: call cancel on children? Update timing?
  },
  _createIdxAccessors: function(start, end) {
    for (var i = start; i < end; i++) {
      this.__defineSetter__(i, function(x) { this.children[i] = x; });
      this.__defineGetter__(i, function() { return this.children[i]; });
    }
  },
  _deleteIdxAccessors: function(start, end) {
    for (var i = start; i < end; i++) {
      delete this[i];
    }
  },
  add: function() {
    var newItems = [];
    for (var i = 0; i < arguments.length; i++) {
      newItems.push(arguments[i]);
    }
    this.splice(this.length, 0, newItems);
    return newItems;
  },
  _addChild: function(child) {
    this.children.push(child);
    this._createIdxAccessors(this.length, this.length + 1);
    this.length = this.children.length;
    this.onListChange();
  },
  item: function(index) {
    return this.children[index];
  },
  indexOf: function(item) {
    for (var i = 0; i < this.children.length; i++) {
      if (this.children[i] === item) {
        return i;
      }
    }
    return -1;
  },
  splice: function() {
    // Read params
    var start = arguments[0];
    var deleteCount = arguments[1];
    var newItems = [];
    if (Array.isArray(arguments[2])) {
      newItems = arguments[2];
    } else {
      for (var i = 2; i < arguments.length; i++) {
        newItems.push(arguments[i]);
      }
    }

    var removedItems = [];
    var len = this.length;

    // Interpret params
    var actualStart = start < 0 ?
        Math.max(len + start, 0) : Math.min(start, len);
    var actualDeleteCount =
        Math.min(Math.max(deleteCount, 0), len - actualStart);

    // Reparent items
    for (var i = 0; i < newItems.length; i++) {
      newItems[i].reparent(this);
    }

    // Delete stage
    if (actualDeleteCount) {
      removedItems = this.children.splice(actualStart, actualDeleteCount);
      for (var i = 0; i < removedItems.length; i++) {
        removedItems[i].parentGroup = null;
      }
      this._deleteIdxAccessors(actualStart, actualStart + actualDeleteCount);
    }

    // Add stage
    if (newItems.length) {
      for (var i = 0; i < newItems.length; i++) {
        this.children.splice(actualStart+i, 0, newItems[i]);
      }
      this._createIdxAccessors(actualStart, actualStart + newItems.length);
    }

    this.length = this.children.length;
    this.onListChange();

    return removedItems;
  },
  remove: function(index, count) {
    if (!isDefined(count)) {
      count = 1;
    }
    return this.splice(index, count);
  }
}

/** @constructor */
var AnimationGroup = function(type, children, timing, startTime, parentGroup) {
  // used by TimedItem via intrinsicDuration(), so needs to be set before
  // initializing super.
  this.type = type || 'par';
  this.initListMixin(this._childrenStateModified);
  AnimationGroup.$super.call(this, timing, startTime, parentGroup);
  if (children && Array.isArray(children)) {
    for (var i = 0; i < children.length; i++) {
      this.add(children[i]);
    }
  }
  // TODO: Work out where to expose name in the API
  // this.name = properties.name || '<anon>';
};

inherits(AnimationGroup, TimedItem);
mixin(AnimationGroup.prototype, AnimationListMixin);
mixin(AnimationGroup.prototype, {
  _childrenStateModified: function() {
    this.updateIterationDuration();
    this._updateChildStartTimes();
    this.updateTimeMarkers();
    if (this.parentGroup) {
      this.parentGroup._childrenStateModified();
    } else {
      maybeRestartAnimation();
    }
  },
  _updateChildStartTimes: function() {
    if (this.type == 'seq') {
      var cumulativeStartTime = 0;
      this.children.forEach(function(child) {
        if (child._startTimeMode != ST_FORCED) {
          child._stashedStartTime = child._startTime;
          child._stashedStartTimeMode = child._startTimeMode;
          child._startTimeMode = ST_FORCED;
        }
        child._startTime = cumulativeStartTime;
        child.updateTimeMarkers();
        cumulativeStartTime += Math.max(0, child.timing.startDelay +
            child.animationDuration);
      }.bind(this));
    }
  },
  getActiveAnimations: function() {
    var result = [];
    if (this._timeFraction === null) {
      return result;
    }
    for (var i = 0; i < this.children.length; i++) {
      if (this.children[i]._timeFraction !== null) {
        if (this.children[i].getActiveAnimations) {
          result = result.concat(this.children[i].getActiveAnimations());
        } else {
          result.push(this.children[i]);
        }
      }
    }
    return result;
  },
  getAnimationsForElement: function(elem) {
    var result = [];
    for (var i = 0; i < this.children.length; i++) {
      if (this.children[i].getAnimationsForElement) {
        result = result.concat(this.children[i].getAnimationsForElement(elem));
      } else if (this.children[i].targetElement == elem) {
        result.push(this.children[i]);
      }
    }
    return result;
  },
  intrinsicDuration: function() {
    if (this.type == 'par') {
      var dur = Math.max.apply(undefined, this.children.map(function(a) {
        return a.endTime;
      }));
      return Math.max(0, dur);
    } else if (this.type == 'seq') {
      var result = 0;
      this.children.forEach(function(a) {
        result += a.animationDuration + a.timing.startDelay;
      });
      return result;
    } else {
      throw 'Unsupported type ' + this.type;
    }
  },
  _getActiveAnimations: function() {
    this.updateTimeMarkers();
    var animations = [];
    this.children.forEach(function(child) {
      animations = animations.concat(child._getActiveAnimations());
    }.bind(this));
    return animations;
  },
  toString: function() {
    return this.type + ' ' + this.startTime + '-' + this.endTime + ' (' +
        this.timeDrift + ' @' + this.currentTime + ') ' + ' [' +
        this.children.map(function(a) { return a.toString(); }) + ']'
  }
});

/** @constructor */
var ParGroup = function(children, timing, parentGroup, startTime) {
  ParGroup.$super.call(
      this, 'par', children, timing, startTime, parentGroup);
};

inherits(ParGroup, AnimationGroup);

/** @constructor */
var SeqGroup = function(children, timing, parentGroup, startTime) {
  SeqGroup.$super.call(
      this, 'seq', children, timing, startTime, parentGroup);
};

inherits(SeqGroup, AnimationGroup);

/** @constructor */
var AnimationFunction = function(operation, accumulateOperation) {
  this.operation = operation === undefined ? 'replace' : operation;
  this.accumulateOperation =
      accumulateOperation == undefined ? 'replace' : operation;
};

mixin(AnimationFunction.prototype, {
  sample: function(timeFraction, currentIteration, target) {
    throw 'Unimplemented sample function';
  },
  getValue: function(target) {
    return;
  },
  clone: function() {
    throw 'Unimplemented clone method'
  }
});

AnimationFunction.createFromProperties = function(properties) {
  // Step 1 - determine set of animation properties
  var animProps = [];
  for (var candidate in properties) {
    if (candidate in propertyTypes) {
      animProps.push(candidate);
    }
  }

  // Step 2 - Create AnimationFunction objects
  if (animProps.length === 0) {
    return null;
  } else if (animProps.length === 1) {
    return AnimationFunction._createKeyframeFunction(
        animProps[0], properties[animProps[0]], properties.operation);
  } else {
    var result = new GroupedAnimationFunction();
    for (var i = 0; i < animProps.length; i++) {
      result.add(AnimationFunction._createKeyframeFunction(
          animProps[i], properties[animProps[i]], properties.operation));
    }
    return result;
  }
}

// Step 3 - Create a KeyframesAnimationFunction object
AnimationFunction._createKeyframeFunction = function(property, value, operation) {
  var func = new KeyframesAnimationFunction(property);

  if (typeof value === 'string') {
    func.frames.add(new Keyframe(value, 0));
    func.frames.add(new Keyframe(value, 1));
    func.operation = 'merge';
  } else if (Array.isArray(value)) {
    for (var i = 0; i < value.length; i++) {
      if (typeof value[i] !== 'string') {
        var val = isDefinedAndNotNull(value[i].value) ? value[i].value : "";
        var offset = isDefinedAndNotNull(value[i].offset) ? value[i].offset : 1;
        func.frames.add(new Keyframe(val, offset));
      } else {
        var offset = i / (value.length - 1);
        func.frames.add(new Keyframe(value[i], offset));
      }
    }
  } else {
    try {
      throw new Error('TypeError');
    } catch (e) { console.log(e.stack); throw e; }
  }

  if (isDefinedAndNotNull(operation)) {
    func.operation = operation;
  }

  return func;
}

/** @constructor */
var GroupedAnimationFunction = function() {
  GroupedAnimationFunction.$super.call(this);
  this.children = [];
};

inherits(GroupedAnimationFunction, AnimationFunction);
mixin(GroupedAnimationFunction.prototype, {
  item: function(i) {
    return this.children[i];
  },
  add: function(func) {
    this.children.push(func);
  },
  remove: function(i) {
    this.children.splice(i, 1);
  },
  sample: function(timeFraction, currentIteration, target) {
    for (var i = 0; i < this.children.length; i++) {
      this.children[i].sample(timeFraction, currentIteration, target);
    }
  },
  clone: function() {
    var result = new GroupedAnimationFunction();
    for (var i = 0; i < this.children.length; i++) {
      result.add(this.children[i].clone());
    }
  }
});

GroupedAnimationFunction.prototype.__defineGetter__('length', function() {
  return this.children.length;
});

/** @constructor */
var PathAnimationFunction = function(path, operation, accumulateOperation) {
  PathAnimationFunction.$super.call(this, operation, accumulateOperation);
  // TODO: path argument is not in the spec -- seems useful since
  // SVGPathSegList doesn't have a constructor.
  this._path = path;
};

inherits(PathAnimationFunction, AnimationFunction);
mixin(PathAnimationFunction.prototype, {
  sample: function(timeFraction, currentIteration, target) {
    var length = this._path.getTotalLength();
    var point = this._path.getPointAtLength(timeFraction * length);
    var x = point.x - target.offsetWidth / 2;
    var y = point.y - target.offsetHeight / 2;
    // TODO: calc(point.x - 50%) doesn't work?
    var value = [{t: 'translate', d: [x, y]}];
    if (this.rotate) {
      // Super hacks
      var lastPoint = this._path.getPointAtLength(timeFraction * length - 0.01);
      var dx = point.x - lastPoint.x;
      var dy = point.y - lastPoint.y;
      var rotation = Math.atan2(dy, dx);
      value.push({t:'rotate', d: rotation / 2 / Math.PI * 360});
    }
    DEFAULT_GROUP.compositor.setAnimatedValue(target, '-webkit-transform',
        new AnimatedResult(value, this.operation, timeFraction));
    DEFAULT_GROUP.compositor.setAnimatedValue(target, 'transform',
        new AnimatedResult(value, this.operation, timeFraction));
  },
  clone: function() {
    return new PathAnimationFunction(this._path.getAttribute('d'));
  }
});

PathAnimationFunction.prototype.__defineSetter__('segments', function(segments) {
  // TODO: moving the path segments is not entirely correct, but we can't
  // assign the list to the path.
  var targetSegments = this._path.pathSegList;
  targetSegments.clear();
  for (var i = 0; i < segments.numberOfItems; i++) {
    this._path.pathSegList.appendItem(segments.getItem(i));
  }
});

PathAnimationFunction.prototype.__defineGetter__('segments', function() {
  return this._path.pathSegList;
});

/** @constructor */
var KeyframesAnimationFunction = function(property, operation, accumulateOperation) {
  KeyframesAnimationFunction.$super.call(this, operation, accumulateOperation);
  this.property = property;
  this.frames = new KeyframeList();
};

inherits(KeyframesAnimationFunction, AnimationFunction);
mixin(KeyframesAnimationFunction.prototype, {
  sample: function(timeFraction, currentIteration, target) {
    var frames = this.frames._sorted();
    if (frames.length == 0) {
      return;
    }
    var afterFrameNum = null;
    var beforeFrameNum = null;
    var i = 0;
    while (i < frames.length) {
      if (frames[i].offset == timeFraction) {
        // TODO: This should probably call fromCssValue and toCssValue for
        // cases where we have to massage the data before setting e.g.
        // 'rotate(45deg)' is valid, but for UAs that don't support CSS
        // Transforms syntax on SVG content we have to convert that to
        // 'rotate(45)' before setting.
        this.ensureRawValue(frames[i]);
        DEFAULT_GROUP.compositor.setAnimatedValue(target, this.property,
            new AnimatedResult(frames[i].rawValue, this.operation,
            timeFraction));
        return;
      }
      if (frames[i].offset > timeFraction) {
        afterFrameNum = i;
        break;
      }
      i++;
    }
    if (afterFrameNum == 0) {
      // In the case where we have a negative time fraction and a keyframe at
      // offset 0, the expected behavior is to extrapolate the interval that
      // starts at 0, rather than to use the base value.
      if (frames[0].offset === 0) {
        afterFrameNum = frames.length > 1 ? 1 : frames.length;
        beforeFrameNum = 0;
      } else {
        beforeFrameNum = -1;
      }
    } else if (afterFrameNum == null) {
      // In the case where we have a time fraction greater than 1 and a
      // keyframe at 1, the expected behavior is to extrapolate the interval
      // that ends at 1, rather than to use the base value.
      if (frames[frames.length-1].offset === 1) {
        afterFrameNum = frames.length - 1;
        beforeFrameNum = frames.length > 1 ? frames.length - 2 : -1;
      } else {
        beforeFrameNum = frames.length - 1;
        afterFrameNum = frames.length;
      }
    } else {
      beforeFrameNum = afterFrameNum - 1;
    }
    if (beforeFrameNum == -1) {
      beforeFrame = {
        rawValue: zero(this.property, frames[afterFrameNum].value),
        offset: 0
      };
    } else {
      beforeFrame = frames[beforeFrameNum];
      this.ensureRawValue(beforeFrame);
    }

    if (afterFrameNum == frames.length) {
      afterFrame = {
        rawValue: zero(this.property, frames[beforeFrameNum].value),
        offset: 1
      };
    } else {
      afterFrame = frames[afterFrameNum];
      this.ensureRawValue(afterFrame);
    }
    // TODO: apply time function
    var localTimeFraction = (timeFraction - beforeFrame.offset) /
        (afterFrame.offset - beforeFrame.offset);
    // TODO: property-based interpolation for things that aren't simple
    var animationValue = interpolate(this.property, beforeFrame.rawValue,
        afterFrame.rawValue, localTimeFraction);
    DEFAULT_GROUP.compositor.setAnimatedValue(target, this.property,
        new AnimatedResult(animationValue, this.operation, timeFraction));
  },
  getValue: function(target) {
    return getValue(target, this.property);
  },
  clone: function() {
    var result = new KeyframesAnimationFunction(
        this.property, this.operation, this.accumulateOperation);
    result.frames = this.frames.clone();
    return result;
  },
  ensureRawValue: function(frame) {
    if (isDefinedAndNotNull(frame.rawValue)) {
      return;
    }
    frame.rawValue = fromCssValue(this.property, frame.value);
  },
  toString: function() {
    return this.property;
  }
});

/** @constructor */
var Keyframe = function(value, offset, timingFunction) {
  this.value = value;
  this.rawValue = null;
  this.offset = offset;
  this.timingFunction = timingFunction;
};

/** @constructor */
var KeyframeList = function() {
  this.frames = [];
  this._isSorted = true;
};

mixin(KeyframeList.prototype, {
  _sorted: function() {
    if (!this._isSorted) {
      this.frames.sort(function(a, b) {
        if (a.offset < b.offset) {
          return -1;
        }
        if (a.offset > b.offset) {
          return 1;
        }
        return 0;
      });
      this._isSorted = true;
    }
    return this.frames;
  },
  item: function(index) {
    if (index >= this.length || index < 0) {
      return null;
    }
    return this.frames[index];
  },
  add: function(frame) {
    this.frames.push(frame);
    this._isSorted = false;
    return frame;
  },
  remove: function(frame) {
    var index = this.frames.indexOf(frame);
    if (index == -1) {
      return undefined;
    }
    this.frames.splice(index, 1);
    return frame;
  },
  clone: function() {
    var result = new KeyframeList();
    for (var i = 0; i < this.frames.length; i++) {
      result.add(new Keyframe(this.frames[i].value, this.frames[i].offset,
          this.frames[i].timingFunction));
    }
    return result;
  }
});

KeyframeList.prototype.__defineGetter__('length', function() {
  return this.frames.length;
});

var presetTimings = {
  'ease': [0.25, 0.1, 0.25, 1.0],
  'linear': [0.0, 0.0, 1.0, 1.0],
  'ease-in': [0.42, 0, 1.0, 1.0],
  'ease-out': [0, 0, 0.58, 1.0],
  'ease-in-out': [0.42, 0, 0.58, 1.0]
};

/** @constructor */
var TimingFunction = function(spec) {
  if (!Array.isArray(spec)) {
    return TimingFunction.createFromString(spec);
  }
  this.params = spec;
  this.map = []
  for (var ii = 0; ii <= 100; ii += 1) {
    var i = ii / 100;
    this.map.push([
      3*i*(1-i)*(1-i)*this.params[0] + 3*i*i*(1-i)*this.params[2] + i*i*i,
      3*i*(1-i)*(1-i)*this.params[1] + 3*i*i*(1-i)*this.params[3] + i*i*i
    ]);
  }
};

mixin(TimingFunction.prototype, {
  scaleTime: function(fraction) {
    var fst = 0;
    while (fst != 100 && fraction > this.map[fst][0]) {
      fst += 1;
    }
    if (fraction == this.map[fst][0] || fst == 0) {
      return this.map[fst][1];
    }
    var yDiff = this.map[fst][1] - this.map[fst - 1][1];
    var xDiff = this.map[fst][0] - this.map[fst - 1][0];
    var p = (fraction - this.map[fst - 1][0]) / xDiff;
    return this.map[fst - 1][1] + p * yDiff;
  },
  clone: function() {
    return new TimingFunction(this.params);
  }
});

TimingFunction.createFromString = function(spec) {
  var preset = presetTimings[spec];
  if (preset) {
    return new TimingFunction(presetTimings[spec]);
  }
  var stepMatch = /steps\(\s*(\d+)\s*,\s*(start|end|middle)\s*\)/.exec(spec);
  if (stepMatch) {
    return new StepTimingFunction(Number(stepMatch[1]), stepMatch[2]);
  }
  var bezierMatch = /cubic-bezier\(([^,]*),([^,]*),([^,]*),([^)]*)\)/.exec(spec);
  if (bezierMatch) {
    return new TimingFunction([
        Number(bezierMatch[1]),
        Number(bezierMatch[2]),
        Number(bezierMatch[3]),
        Number(bezierMatch[4])]);
  }
  throw 'not a timing function: ' + spec;
};

/** @constructor */
var StepTimingFunction = function(numSteps, position) {
  this.numSteps = numSteps;
  this.position = position || 'end';
}

inherits(StepTimingFunction, TimingFunction);
mixin(StepTimingFunction.prototype, {
  scaleTime: function(fraction) {
    if (fraction >= 1)
      return 1;
    var stepSize = 1 / this.numSteps;
    if (this.position == 'start') {
      fraction += stepSize;
    } else if (this.position == 'middle') {
      fraction += stepSize / 2;
    }
    return fraction - fraction % stepSize;
  },
  clone: function() {
    return new StepTimingFunction(this.numSteps, this.position);
  }
});

var interp = function(from, to, f, type) {
  if (Array.isArray(from) || Array.isArray(to)) {
    return interpArray(from, to, f, type);
  }
  var zero = type == 'scale' ? 1.0 : 0.0;
  to   = isDefinedAndNotNull(to) ? to : zero;
  from = isDefinedAndNotNull(from) ? from : zero;

  return to * f + from * (1 - f);
};

var interpArray = function(from, to, f, type) {
  console.assert(Array.isArray(from) || from === null,
      'From is not an array or null');
  console.assert(Array.isArray(to) || to === null,
      'To is not an array or null');
  console.assert(from === null || to === null || from.length === to.length,
      'Arrays differ in length');
  var length = from ? from.length : to.length;

  var result = [];
  for (var i = 0; i < length; i++) {
    result[i] = interp(from ? from[i] : null, to ? to[i] : null, f, type);
  }
  return result;
};

var numberType = {
  zero: function() { return 0; },
  add: function(base, delta) { return base + delta; },
  interpolate: interp,
  toCssValue: function(value) { return value + ''; },
  fromCssValue: function(value) { return value !== '' ? Number(value): null; }
};

var integerType = {
  interpolate: function(from, to, f) { return Math.floor(interp(from, to, f)); },
};
integerType.__proto__ = numberType;

var outerCalcRE = /-webkit-calc\s*\(\s*([^)]*)\)/;
var valueRE = /\s*([0-9.]*)([a-zA-Z%]*)/;
var operatorRE = /\s*([+-])/;

var percentLengthType = {
  zero: function() { return {}; },
  add: function(base, delta) { 
    var out = {};
    for (value in base) {
        out[value] = base[value] + (delta[value] || 0);
    }
    for (value in delta) {
        if (value in base) {
            continue;
        }
        out[value] = delta[value];
    }
    return out;
  },
  interpolate: function(from, to, f) {
    var out = {};
    for (var value in from) {
        out[value] = interp(from[value], to[value], f);
    }
    for (var value in to) {
        if (value in out) {
            continue;
        }
        out[value] = interp(0, to[value], f);
    }
    return out;
  },
  toCssValue: function(value) { 
    var s = '';
    var single_value = true;
    for (var item in value) {
      if (s === '') {
        s = value[item] + item;
      } else if (single_value) {
        s = '-webkit-calc(' + s + ' + ' + value[item] + item + ')';
        single_value = false;
      } else {
        s = s.substring(0, s.length - 1) + ' + ' + value[item] + item + ')';
      }
    }
    return s;
  },
  fromCssValue: function(value) {
    var out = {}
    var innards = outerCalcRE.exec(value);
    if (!innards) {
      var singleValue = valueRE.exec(value);
      if (singleValue.length == 3) {
        out[singleValue[2]] = Number(singleValue[1]);
        return out;
      }
      return {};<commit_msg>fix indentation and test.<commit_after>/**
 * Copyright 2012 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() {

var inherits = function(child, parent) {
  var tmp = function() {};
  tmp.prototype = parent.prototype;
  child.$super = parent;
  child.prototype = new tmp();
  child.prototype.constructor = child;
};

var mixin = function(target, source) {
  for (var k in source) {
    target[k] = source[k];
  }
};

var strip = function(str) {
  return str.replace(/^\s+/, '').replace(/\s+$/, '');
};

var IndexSizeError = function(message) {
  IndexSizeError.$super.call(this);
  this.name = "IndexSizeError";
  this.message = message;
}

inherits(IndexSizeError, Error);

/** @constructor */
var Timing = function(timingDict) {
  this.startDelay = timingDict.startDelay || 0.0;
  this.duration = timingDict.duration;
  if (this.duration < 0.0) {
    throw new IndexSizeError('duration must be >= 0');
  }
  this.iterationCount = isDefined(timingDict.iterationCount) ?
      timingDict.iterationCount : 1.0;
  if (this.iterationCount < 0.0) {
    throw new IndexSizeError('iterationCount must be >= 0');
  }
  this.iterationStart = timingDict.iterationStart || 0.0;
  if (this.iterationStart < 0.0) {
    throw new IndexSizeError('iterationStart must be >= 0');
  }
  this.playbackRate = isDefined(timingDict.playbackRate) ?
      timingDict.playbackRate : 1.0;
  //this.playbackRate = timingDict.playbackRate || 1.0;
  this.direction = timingDict.direction || 'normal';
  if (typeof timingDict.timingFunction === 'string') {
    this.timingFunction = TimingFunction.createFromString(timingDict.timingFunction);
  } else {
    this.timingFunction = timingDict.timingFunction;
  }
  this.fillMode = timingDict.fillMode || 'forwards';
};

mixin(Timing.prototype, {
  clone: function() {
    return new Timing({
      startDelay: this.startDelay,
      duration: this.duration,
      iterationCount: this.iterationCount,
      iterationStart: this.iterationStart,
      playbackRate: this.playbackRate,
      direction: this.direction,
      timingFunction: this.timingFunction ? this.timingFunction.clone() : null,
      fillMode: this.fillMode
    });
  },
});

/** @constructor */
var TimingProxy = function(timing, setter) {
  this._timing = timing;
  this._setter = setter;
};

['startDelay', 'duration', 'iterationCount', 'iterationStart', 'playbackRate',
    'direction', 'timingFunction', 'fillMode'].forEach(function(s) {
  TimingProxy.prototype.__defineGetter__(s, function() {
    return this._timing[s];
  });
  TimingProxy.prototype.__defineSetter__(s, function(v) {
    var old = this._timing[s];
    this._timing[s] = v;
    try {
      this._setter(v);
    } catch (e) {
      this._timing[s] = old;
      throw e;
    }
  });
});

mixin(TimingProxy.prototype, {
  extractMutableTiming: function() {
    return new Timing({
      startDelay: this._timing.startDelay,
      duration: this._timing.duration,
      iterationCount: this._timing.iterationCount,
      iterationStart: this._timing.iterationStart,
      playbackRate: this._timing.playbackRate,
      direction: this._timing.direction,
      timingFunction: this._timing.timingFunction ?
                  this._timing.timingFunction.clone() : null,
      fillMode: this._timing.fillMode
    });
  },
  clone: function() {
    return this._timing.clone();
  }
});

var isDefined = function(val) {
  return typeof val !== 'undefined';
};

var isDefinedAndNotNull = function(val) {
  return isDefined(val) && (val !== null);
};

var ST_MANUAL = 0;
var ST_AUTO = 1;
var ST_FORCED = 2;

/** @constructor */
var TimedItem = function(timing, startTime, parentGroup) {
  this.timing = new TimingProxy(interpretTimingParam(timing), function() {
    this.updateIterationDuration();
  }.bind(this));
  this._startTime = startTime;
  this.currentIteration = null;
  this.iterationTime = null;
  this.animationTime = null;
  this._reversing = false;

  if (parentGroup === null || parentGroup instanceof AnimationGroup) {
    this.parentGroup = parentGroup;
  } else if (!isDefined(parentGroup)) {
    this.parentGroup = DEFAULT_GROUP;
  } else {
    throw new TypeError('parentGroup is not an AnimationGroup');
  }

  if (!isDefined(startTime)) {
    this._startTimeMode = ST_AUTO;
    // We take _effectiveParentTime at the moment this TimedItem is
    // created. Note that the call to _addChild() below may cause the parent
    // to update its timing properties, including its iterationTime.
    this._startTime = this._effectiveParentTime;
  } else {
    this._startTimeMode = ST_MANUAL;
    this._startTime = startTime;
  }

  this._timeDrift = 0;
  this._locallyPaused = false;

  if (this.parentGroup) {
    this.parentGroup._addChild(this);
  }
  this.updateIterationDuration();
  this._pauseStartTime = 0;
};

TimedItem.prototype.__defineGetter__('timeDrift', function() {
  if (this.locallyPaused) {
    return this._effectiveParentTime - this.startTime -
        this._pauseStartTime;
  }
  return this._timeDrift;
});
TimedItem.prototype.__defineGetter__('_effectiveParentTime', function() {
  return this.parentGroup !== null && this.parentGroup.iterationTime !== null ?
      this.parentGroup.iterationTime : 0;
});
TimedItem.prototype.__defineGetter__('currentTime', function() {
  return this._effectiveParentTime - this._startTime - this.timeDrift;
});
TimedItem.prototype.__defineSetter__('currentTime', function(seekTime) {
  if (this._locallyPaused) {
    this._pauseStartTime = seekTime;
  } else {
    this._timeDrift = this._effectiveParentTime - this._startTime -
        seekTime;
  }
  this.updateTimeMarkers();
  if (this.parentGroup) {
    this.parentGroup._childrenStateModified();
  }
});
TimedItem.prototype.__defineGetter__('startTime', function() {
  return this._startTime;
});
TimedItem.prototype.__defineSetter__('startTime', function(newStartTime) {
  if (this.parentGroup && this.parentGroup.type === 'seq') {
    throw new Error('NoModificationAllowedError');
  }
  this._startTime = newStartTime;
  this._startTimeMode = ST_MANUAL;
  this.updateTimeMarkers();
  if (this.parentGroup) {
    this.parentGroup._childrenStateModified();
  }
});
TimedItem.prototype.__defineGetter__('locallyPaused', function() {
  return this._locallyPaused;
});
TimedItem.prototype.__defineSetter__('locallyPaused', function(newVal) {
  if (this._locallyPaused === newVal) {
    return;
  }
  if (this._locallyPaused) {
    this._timeDrift = this._effectiveParentTime - this.startTime -
        this._pauseStartTime;
  } else {
    this._pauseStartTime = this.currentTime;
  }
  this._locallyPaused = newVal;
  this.updateTimeMarkers();
});
TimedItem.prototype.__defineGetter__('paused', function() {
  return this.locallyPaused ||
      (isDefinedAndNotNull(this.parentGroup) && this.parentGroup.paused);
});

mixin(TimedItem.prototype, {
  reparent: function(parentGroup) {
    if (this.parentGroup) {
      this.parentGroup.remove(this.parentGroup.indexOf(this), 1);
    }
    this.parentGroup = parentGroup;
    this._timeDrift = 0;
    if (this._startTimeMode == ST_FORCED &&
        (!this.parentGroup || this.parentGroup.type != 'seq')) {
      this._startTime = this._stashedStartTime;
      this._startTimeMode = this._stashedStartTimeMode;
    }
    if (this._startTimeMode == ST_AUTO) {
      this._startTime = this._effectiveParentTime;
    }
    this.updateTimeMarkers();
  },
  // TODO: take timing.iterationStart into account. Spec needs to as well.
  updateIterationDuration: function() {
    if (isDefined(this.timing.duration)) {
      this.duration = this.timing.duration;
    } else {
      this.duration = this.intrinsicDuration();
    }
    // Section 6.10: Calculating the intrinsic animation duration
    var repeatedDuration = this.duration * this.timing.iterationCount;
    this.animationDuration = repeatedDuration /
        Math.abs(this.timing.playbackRate);
    this.updateTimeMarkers();
    if (this.parentGroup) {
      this.parentGroup._childrenStateModified();
    }
  },
  updateTimeMarkers: function(parentTime) {
    if (this.locallyPaused) {
      this.endTime = Infinity;
    } else {
      this.endTime = this._startTime + this.animationDuration +
          this.timing.startDelay + this.timeDrift;
    }
    if (this.parentGroup !== null && this.parentGroup.iterationTime !== null) {
      this.itemTime = this.parentGroup.iterationTime -
          this._startTime - this.timeDrift;
    } else if (isDefined(parentTime)) {
      this.itemTime = parentTime;
    } else {
      this.itemTime = null;
    }
    if (this.itemTime !== null) {
      if (this.itemTime <= this.timing.startDelay) {
        if (((this.timing.fillMode == 'backwards') && !this._reversing)
          || this.timing.fillMode == 'both'
          || ((this.timing.fillMode == 'forwards') && this._reversing)) {
          this.animationTime = 0;
        } else {
          this.animationTime = null;
        }
      } else if (this.itemTime <
          this.timing.startDelay + this.animationDuration) {
        this.animationTime = this.itemTime - this.timing.startDelay;
      } else {
        if (((this.timing.fillMode == 'forwards') && !this._reversing)
          || this.timing.fillMode == 'both'
          || ((this.timing.fillMode == 'backwards') && this._reversing)) {
          this.animationTime = this.animationDuration;
        } else {
          this.animationTime = null;
        }
      }
      if (this.animationTime === null) {
        this.iterationTime = null;
        this.currentIteration = null;
        this._timeFraction = null;
      } else if (this.duration == 0) {
        this.iterationTime = 0;
        var isAtEndOfIterations = (this.timing.iterationCount != 0) &&
            ((this.itemTime < this.timing.startDelay) == this._reversing);
        this.currentIteration = isAtEndOfIterations ?
           this._floorWithOpenClosedRange(this.timing.iterationStart +
               this.timing.iterationCount, 1.0) :
           this._floorWithClosedOpenRange(this.timing.iterationStart, 1.0);
        // Equivalent to unscaledIterationTime below.
        var unscaledFraction = isAtEndOfIterations ?
            this._modulusWithOpenClosedRange(this.timing.iterationStart +
                this.timing.iterationCount, 1.0) :
            this._modulusWithClosedOpenRange(this.timing.iterationStart, 1.0);
        this._timeFraction = this._isCurrentDirectionForwards(
            this.timing.direction, this.currentIteration) ?
                unscaledFraction :
                1.0 - unscaledFraction;
        if (this.timing.timingFunction) {
          this._timeFraction = this.timing.timingFunction.scaleTime(
              this._timeFraction);
        }
      } else {
        var startOffset = this.timing.iterationStart * this.duration;
        var effectiveSpeed = this._reversing ?
            -this.timing.playbackRate : this.timing.playbackRate;
        if (effectiveSpeed < 0) {
          var adjustedAnimationTime = (this.animationTime -
              this.animationDuration) * effectiveSpeed + startOffset;
        } else {
          var adjustedAnimationTime = this.animationTime * effectiveSpeed +
              startOffset;
        }
        var repeatedDuration = this.duration * this.timing.iterationCount;
        var isAtEndOfIterations = (this.timing.iterationCount != 0) &&
            (adjustedAnimationTime - startOffset == repeatedDuration);
        this.currentIteration = isAtEndOfIterations ?
            this._floorWithOpenClosedRange(
                adjustedAnimationTime, this.duration) :
            this._floorWithClosedOpenRange(
                adjustedAnimationTime, this.duration);
        var unscaledIterationTime = isAtEndOfIterations ?
            this._modulusWithOpenClosedRange(
                adjustedAnimationTime, this.duration) :
            this._modulusWithClosedOpenRange(
                adjustedAnimationTime, this.duration);
        var scaledIterationTime = unscaledIterationTime;
        this.iterationTime = this._isCurrentDirectionForwards(
            this.timing.direction, this.currentIteration) ?
                scaledIterationTime :
                this.duration - scaledIterationTime;
        this._timeFraction = this.iterationTime / this.duration;
        if (this.timing.timingFunction) {
          this._timeFraction = this.timing.timingFunction.scaleTime(
              this._timeFraction);
          this.iterationTime = this._timeFraction * this.duration;
        }
      }
    } else {
      this.animationTime = null;
      this.iterationTime = null;
      this.currentIteration = null;
      this._timeFraction = null;
    }
    return this._timeFraction !== null;
  },
  pause: function() {
    this.locallyPaused = true;
  },
  seek: function(itemTime) {
    // TODO
  },
  changePlaybackRate: function(playbackRate) {
    var previousRate = this.timing.playbackRate;
    this.timing.playbackRate = playbackRate;
    if (previousRate == 0 || playbackRate == 0) {
      return;
    }
    // TODO: invert the fillMode?
    var seekAdjustment = (this.itemTime - this.timing.startDelay) *
        (1 - previousRate / playbackRate);
    this.currentTime = this.itemTime - seekAdjustment;
  },
  reverse: function() {
    if (this.currentTime === null) {
      var seekTime = 0;
    } else if (this.currentTime < this.timing.startDelay) {
      var seekTime = this.timing.startDelay + this.animationDuration;
    } else if (this.currentTime > this.timing.startDelay +
        this.animationDuration) {
      var seekTime = this.timing.startDelay;
    } else {
      var seekTime = this.timing.startDelay + this.animationDuration -
          this.currentTime;
    }

    this.currentTime = seekTime;
    this._reversing = !(this._reversing);
  },
  cancel: function() {
    if (this.parentGroup) {
      this.parentGroup.remove(this.parentGroup.indexOf(this), 1);
    }
  },
  play: function() {
    // TODO: This should unpause as well
    if (this.currentTime > this.animationDuration + this.timing.startDelay &&
        this.timing.playbackRate >= 0) {
      this.currentTime = this.timing.startDelay;
    }
    this.locallyPaused = false;
  },
  _floorWithClosedOpenRange: function(x, range) {
    return Math.floor(x / range);
  },
  _floorWithOpenClosedRange: function(x, range) {
    return Math.ceil(x / range) - 1;
  },
  _modulusWithClosedOpenRange: function(x, range) {
    return x % range;
  },
  _modulusWithOpenClosedRange: function(x, range) {
    var ret = this._modulusWithClosedOpenRange(x, range);
    return ret == 0 ? range : ret;
  },
  _isCurrentDirectionForwards: function(direction, currentIteration) {
    if (direction == 'normal') {
      return true;
    }
    if (direction == 'reverse') {
      return false;
    }
    var d = currentIteration;
    if (direction == 'alternate-reverse') {
      d += 1;
    }
    // TODO: 6.13.3 step 3. wtf?
    return d % 2 == 0;
  },
  _parentToGlobalTime: function(parentTime) {
    if (!this.parentGroup)
      return parentTime;
    return parentTime + DEFAULT_GROUP.currentTime -
        this.parentGroup.iterationTime;
  },
});

var interpretAnimationFunction = function(animationFunction) {
  if (animationFunction instanceof AnimationFunction) {
    return animationFunction;
  } else if (typeof(animationFunction) === 'object') {
    // Test if the object is actually a CustomAnimationFunction
    // (how does WebIDL actually differentiate different callback interfaces?)
    if (animationFunction.hasOwnProperty('sample') &&
        typeof(animationFunction.sample) === 'function') {
      return animationFunction;
    } else {
      return AnimationFunction.createFromProperties(animationFunction);
    }
  } else {
    try {
      throw new Error('TypeError');
    } catch (e) { console.log(e.stack); throw e; }
  }
};

var interpretTimingParam = function(timing) {
  if (!isDefinedAndNotNull(timing)) {
    return new Timing({});
  }
  if (timing instanceof Timing || timing instanceof TimingProxy) {
    return timing;
  }
  if (typeof(timing) === 'number') {
    return new Timing({duration: timing});
  }
  if (typeof(timing) === 'object') {
    return new Timing(timing);
  }
  throw new TypeError('timing parameters must be undefined, Timing objects, ' +
      'numbers, or timing dictionaries; not \'' + timing + '\'');
};

/** @constructor */
var Animation = function(target, animationFunction, timing, parentGroup, startTime) {
  this.animationFunction = interpretAnimationFunction(animationFunction);
  this.timing = interpretTimingParam(timing);

  Animation.$super.call(this, timing, startTime, parentGroup);

  this.targetElement = target;
  this.name = this.animationFunction instanceof KeyframesAnimationFunction ?
      this.animationFunction.property : '<anon>';
  this._sortOrder = 0;
};

inherits(Animation, TimedItem);
mixin(Animation.prototype, {
  intrinsicDuration: function() {
    // section 6.6
    return Infinity;
  },
  _sample: function() {
    this.animationFunction.sample(this._timeFraction,
        this.currentIteration, this.targetElement,
        this.underlyingValue);
  },
  _getActiveAnimations: function() {
    if (!this.updateTimeMarkers()) {
      return [];
    }

    this._sortOrder = this._parentToGlobalTime(this.startTime);
    return [this];
  },
  toString: function() {
    var funcDescr = this.animationFunction instanceof AnimationFunction ?
        this.animationFunction.toString() : 'Custom scripted function';
    return 'Animation ' + this.startTime + '-' + this.endTime + ' (' +
        this.timeDrift + ' @' + this.currentTime + ') ' + funcDescr;
  }
});

// To use this, need to have children and length member variables.
var AnimationListMixin = {
  initListMixin: function(onListChange) {
    this._clear();
    this.onListChange = onListChange;
  },
  clear: function() {
    this._clear();
    this.onListChange();
  },
  _clear: function() {
    this.children = [];
    var oldLength = this.length;
    this.length = 0;
    this._deleteIdxAccessors(0, oldLength);
    // TODO: call cancel on children? Update timing?
  },
  _createIdxAccessors: function(start, end) {
    for (var i = start; i < end; i++) {
      this.__defineSetter__(i, function(x) { this.children[i] = x; });
      this.__defineGetter__(i, function() { return this.children[i]; });
    }
  },
  _deleteIdxAccessors: function(start, end) {
    for (var i = start; i < end; i++) {
      delete this[i];
    }
  },
  add: function() {
    var newItems = [];
    for (var i = 0; i < arguments.length; i++) {
      newItems.push(arguments[i]);
    }
    this.splice(this.length, 0, newItems);
    return newItems;
  },
  _addChild: function(child) {
    this.children.push(child);
    this._createIdxAccessors(this.length, this.length + 1);
    this.length = this.children.length;
    this.onListChange();
  },
  item: function(index) {
    return this.children[index];
  },
  indexOf: function(item) {
    for (var i = 0; i < this.children.length; i++) {
      if (this.children[i] === item) {
        return i;
      }
    }
    return -1;
  },
  splice: function() {
    // Read params
    var start = arguments[0];
    var deleteCount = arguments[1];
    var newItems = [];
    if (Array.isArray(arguments[2])) {
      newItems = arguments[2];
    } else {
      for (var i = 2; i < arguments.length; i++) {
        newItems.push(arguments[i]);
      }
    }

    var removedItems = [];
    var len = this.length;

    // Interpret params
    var actualStart = start < 0 ?
        Math.max(len + start, 0) : Math.min(start, len);
    var actualDeleteCount =
        Math.min(Math.max(deleteCount, 0), len - actualStart);

    // Reparent items
    for (var i = 0; i < newItems.length; i++) {
      newItems[i].reparent(this);
    }

    // Delete stage
    if (actualDeleteCount) {
      removedItems = this.children.splice(actualStart, actualDeleteCount);
      for (var i = 0; i < removedItems.length; i++) {
        removedItems[i].parentGroup = null;
      }
      this._deleteIdxAccessors(actualStart, actualStart + actualDeleteCount);
    }

    // Add stage
    if (newItems.length) {
      for (var i = 0; i < newItems.length; i++) {
        this.children.splice(actualStart+i, 0, newItems[i]);
      }
      this._createIdxAccessors(actualStart, actualStart + newItems.length);
    }

    this.length = this.children.length;
    this.onListChange();

    return removedItems;
  },
  remove: function(index, count) {
    if (!isDefined(count)) {
      count = 1;
    }
    return this.splice(index, count);
  }
}

/** @constructor */
var AnimationGroup = function(type, children, timing, startTime, parentGroup) {
  // used by TimedItem via intrinsicDuration(), so needs to be set before
  // initializing super.
  this.type = type || 'par';
  this.initListMixin(this._childrenStateModified);
  AnimationGroup.$super.call(this, timing, startTime, parentGroup);
  if (children && Array.isArray(children)) {
    for (var i = 0; i < children.length; i++) {
      this.add(children[i]);
    }
  }
  // TODO: Work out where to expose name in the API
  // this.name = properties.name || '<anon>';
};

inherits(AnimationGroup, TimedItem);
mixin(AnimationGroup.prototype, AnimationListMixin);
mixin(AnimationGroup.prototype, {
  _childrenStateModified: function() {
    this.updateIterationDuration();
    this._updateChildStartTimes();
    this.updateTimeMarkers();
    if (this.parentGroup) {
      this.parentGroup._childrenStateModified();
    } else {
      maybeRestartAnimation();
    }
  },
  _updateChildStartTimes: function() {
    if (this.type == 'seq') {
      var cumulativeStartTime = 0;
      this.children.forEach(function(child) {
        if (child._startTimeMode != ST_FORCED) {
          child._stashedStartTime = child._startTime;
          child._stashedStartTimeMode = child._startTimeMode;
          child._startTimeMode = ST_FORCED;
        }
        child._startTime = cumulativeStartTime;
        child.updateTimeMarkers();
        cumulativeStartTime += Math.max(0, child.timing.startDelay +
            child.animationDuration);
      }.bind(this));
    }
  },
  getActiveAnimations: function() {
    var result = [];
    if (this._timeFraction === null) {
      return result;
    }
    for (var i = 0; i < this.children.length; i++) {
      if (this.children[i]._timeFraction !== null) {
        if (this.children[i].getActiveAnimations) {
          result = result.concat(this.children[i].getActiveAnimations());
        } else {
          result.push(this.children[i]);
        }
      }
    }
    return result;
  },
  getAnimationsForElement: function(elem) {
    var result = [];
    for (var i = 0; i < this.children.length; i++) {
      if (this.children[i].getAnimationsForElement) {
        result = result.concat(this.children[i].getAnimationsForElement(elem));
      } else if (this.children[i].targetElement == elem) {
        result.push(this.children[i]);
      }
    }
    return result;
  },
  intrinsicDuration: function() {
    if (this.type == 'par') {
      var dur = Math.max.apply(undefined, this.children.map(function(a) {
        return a.endTime;
      }));
      return Math.max(0, dur);
    } else if (this.type == 'seq') {
      var result = 0;
      this.children.forEach(function(a) {
        result += a.animationDuration + a.timing.startDelay;
      });
      return result;
    } else {
      throw 'Unsupported type ' + this.type;
    }
  },
  _getActiveAnimations: function() {
    this.updateTimeMarkers();
    var animations = [];
    this.children.forEach(function(child) {
      animations = animations.concat(child._getActiveAnimations());
    }.bind(this));
    return animations;
  },
  toString: function() {
    return this.type + ' ' + this.startTime + '-' + this.endTime + ' (' +
        this.timeDrift + ' @' + this.currentTime + ') ' + ' [' +
        this.children.map(function(a) { return a.toString(); }) + ']'
  }
});

/** @constructor */
var ParGroup = function(children, timing, parentGroup, startTime) {
  ParGroup.$super.call(
      this, 'par', children, timing, startTime, parentGroup);
};

inherits(ParGroup, AnimationGroup);

/** @constructor */
var SeqGroup = function(children, timing, parentGroup, startTime) {
  SeqGroup.$super.call(
      this, 'seq', children, timing, startTime, parentGroup);
};

inherits(SeqGroup, AnimationGroup);

/** @constructor */
var AnimationFunction = function(operation, accumulateOperation) {
  this.operation = operation === undefined ? 'replace' : operation;
  this.accumulateOperation =
      accumulateOperation == undefined ? 'replace' : operation;
};

mixin(AnimationFunction.prototype, {
  sample: function(timeFraction, currentIteration, target) {
    throw 'Unimplemented sample function';
  },
  getValue: function(target) {
    return;
  },
  clone: function() {
    throw 'Unimplemented clone method'
  }
});

AnimationFunction.createFromProperties = function(properties) {
  // Step 1 - determine set of animation properties
  var animProps = [];
  for (var candidate in properties) {
    if (candidate in propertyTypes) {
      animProps.push(candidate);
    }
  }

  // Step 2 - Create AnimationFunction objects
  if (animProps.length === 0) {
    return null;
  } else if (animProps.length === 1) {
    return AnimationFunction._createKeyframeFunction(
        animProps[0], properties[animProps[0]], properties.operation);
  } else {
    var result = new GroupedAnimationFunction();
    for (var i = 0; i < animProps.length; i++) {
      result.add(AnimationFunction._createKeyframeFunction(
          animProps[i], properties[animProps[i]], properties.operation));
    }
    return result;
  }
}

// Step 3 - Create a KeyframesAnimationFunction object
AnimationFunction._createKeyframeFunction = function(property, value, operation) {
  var func = new KeyframesAnimationFunction(property);

  if (typeof value === 'string') {
    func.frames.add(new Keyframe(value, 0));
    func.frames.add(new Keyframe(value, 1));
    func.operation = 'merge';
  } else if (Array.isArray(value)) {
    for (var i = 0; i < value.length; i++) {
      if (typeof value[i] !== 'string') {
        var val = isDefinedAndNotNull(value[i].value) ? value[i].value : "";
        var offset = isDefinedAndNotNull(value[i].offset) ? value[i].offset : 1;
        func.frames.add(new Keyframe(val, offset));
      } else {
        var offset = i / (value.length - 1);
        func.frames.add(new Keyframe(value[i], offset));
      }
    }
  } else {
    try {
      throw new Error('TypeError');
    } catch (e) { console.log(e.stack); throw e; }
  }

  if (isDefinedAndNotNull(operation)) {
    func.operation = operation;
  }

  return func;
}

/** @constructor */
var GroupedAnimationFunction = function() {
  GroupedAnimationFunction.$super.call(this);
  this.children = [];
};

inherits(GroupedAnimationFunction, AnimationFunction);
mixin(GroupedAnimationFunction.prototype, {
  item: function(i) {
    return this.children[i];
  },
  add: function(func) {
    this.children.push(func);
  },
  remove: function(i) {
    this.children.splice(i, 1);
  },
  sample: function(timeFraction, currentIteration, target) {
    for (var i = 0; i < this.children.length; i++) {
      this.children[i].sample(timeFraction, currentIteration, target);
    }
  },
  clone: function() {
    var result = new GroupedAnimationFunction();
    for (var i = 0; i < this.children.length; i++) {
      result.add(this.children[i].clone());
    }
  }
});

GroupedAnimationFunction.prototype.__defineGetter__('length', function() {
  return this.children.length;
});

/** @constructor */
var PathAnimationFunction = function(path, operation, accumulateOperation) {
  PathAnimationFunction.$super.call(this, operation, accumulateOperation);
  // TODO: path argument is not in the spec -- seems useful since
  // SVGPathSegList doesn't have a constructor.
  this._path = path;
};

inherits(PathAnimationFunction, AnimationFunction);
mixin(PathAnimationFunction.prototype, {
  sample: function(timeFraction, currentIteration, target) {
    var length = this._path.getTotalLength();
    var point = this._path.getPointAtLength(timeFraction * length);
    var x = point.x - target.offsetWidth / 2;
    var y = point.y - target.offsetHeight / 2;
    // TODO: calc(point.x - 50%) doesn't work?
    var value = [{t: 'translate', d: [x, y]}];
    if (this.rotate) {
      // Super hacks
      var lastPoint = this._path.getPointAtLength(timeFraction * length - 0.01);
      var dx = point.x - lastPoint.x;
      var dy = point.y - lastPoint.y;
      var rotation = Math.atan2(dy, dx);
      value.push({t:'rotate', d: rotation / 2 / Math.PI * 360});
    }
    DEFAULT_GROUP.compositor.setAnimatedValue(target, '-webkit-transform',
        new AnimatedResult(value, this.operation, timeFraction));
    DEFAULT_GROUP.compositor.setAnimatedValue(target, 'transform',
        new AnimatedResult(value, this.operation, timeFraction));
  },
  clone: function() {
    return new PathAnimationFunction(this._path.getAttribute('d'));
  }
});

PathAnimationFunction.prototype.__defineSetter__('segments', function(segments) {
  // TODO: moving the path segments is not entirely correct, but we can't
  // assign the list to the path.
  var targetSegments = this._path.pathSegList;
  targetSegments.clear();
  for (var i = 0; i < segments.numberOfItems; i++) {
    this._path.pathSegList.appendItem(segments.getItem(i));
  }
});

PathAnimationFunction.prototype.__defineGetter__('segments', function() {
  return this._path.pathSegList;
});

/** @constructor */
var KeyframesAnimationFunction = function(property, operation, accumulateOperation) {
  KeyframesAnimationFunction.$super.call(this, operation, accumulateOperation);
  this.property = property;
  this.frames = new KeyframeList();
};

inherits(KeyframesAnimationFunction, AnimationFunction);
mixin(KeyframesAnimationFunction.prototype, {
  sample: function(timeFraction, currentIteration, target) {
    var frames = this.frames._sorted();
    if (frames.length == 0) {
      return;
    }
    var afterFrameNum = null;
    var beforeFrameNum = null;
    var i = 0;
    while (i < frames.length) {
      if (frames[i].offset == timeFraction) {
        // TODO: This should probably call fromCssValue and toCssValue for
        // cases where we have to massage the data before setting e.g.
        // 'rotate(45deg)' is valid, but for UAs that don't support CSS
        // Transforms syntax on SVG content we have to convert that to
        // 'rotate(45)' before setting.
        this.ensureRawValue(frames[i]);
        DEFAULT_GROUP.compositor.setAnimatedValue(target, this.property,
            new AnimatedResult(frames[i].rawValue, this.operation,
            timeFraction));
        return;
      }
      if (frames[i].offset > timeFraction) {
        afterFrameNum = i;
        break;
      }
      i++;
    }
    if (afterFrameNum == 0) {
      // In the case where we have a negative time fraction and a keyframe at
      // offset 0, the expected behavior is to extrapolate the interval that
      // starts at 0, rather than to use the base value.
      if (frames[0].offset === 0) {
        afterFrameNum = frames.length > 1 ? 1 : frames.length;
        beforeFrameNum = 0;
      } else {
        beforeFrameNum = -1;
      }
    } else if (afterFrameNum == null) {
      // In the case where we have a time fraction greater than 1 and a
      // keyframe at 1, the expected behavior is to extrapolate the interval
      // that ends at 1, rather than to use the base value.
      if (frames[frames.length-1].offset === 1) {
        afterFrameNum = frames.length - 1;
        beforeFrameNum = frames.length > 1 ? frames.length - 2 : -1;
      } else {
        beforeFrameNum = frames.length - 1;
        afterFrameNum = frames.length;
      }
    } else {
      beforeFrameNum = afterFrameNum - 1;
    }
    if (beforeFrameNum == -1) {
      beforeFrame = {
        rawValue: zero(this.property, frames[afterFrameNum].value),
        offset: 0
      };
    } else {
      beforeFrame = frames[beforeFrameNum];
      this.ensureRawValue(beforeFrame);
    }

    if (afterFrameNum == frames.length) {
      afterFrame = {
        rawValue: zero(this.property, frames[beforeFrameNum].value),
        offset: 1
      };
    } else {
      afterFrame = frames[afterFrameNum];
      this.ensureRawValue(afterFrame);
    }
    // TODO: apply time function
    var localTimeFraction = (timeFraction - beforeFrame.offset) /
        (afterFrame.offset - beforeFrame.offset);
    // TODO: property-based interpolation for things that aren't simple
    var animationValue = interpolate(this.property, beforeFrame.rawValue,
        afterFrame.rawValue, localTimeFraction);
    DEFAULT_GROUP.compositor.setAnimatedValue(target, this.property,
        new AnimatedResult(animationValue, this.operation, timeFraction));
  },
  getValue: function(target) {
    return getValue(target, this.property);
  },
  clone: function() {
    var result = new KeyframesAnimationFunction(
        this.property, this.operation, this.accumulateOperation);
    result.frames = this.frames.clone();
    return result;
  },
  ensureRawValue: function(frame) {
    if (isDefinedAndNotNull(frame.rawValue)) {
      return;
    }
    frame.rawValue = fromCssValue(this.property, frame.value);
  },
  toString: function() {
    return this.property;
  }
});

/** @constructor */
var Keyframe = function(value, offset, timingFunction) {
  this.value = value;
  this.rawValue = null;
  this.offset = offset;
  this.timingFunction = timingFunction;
};

/** @constructor */
var KeyframeList = function() {
  this.frames = [];
  this._isSorted = true;
};

mixin(KeyframeList.prototype, {
  _sorted: function() {
    if (!this._isSorted) {
      this.frames.sort(function(a, b) {
        if (a.offset < b.offset) {
          return -1;
        }
        if (a.offset > b.offset) {
          return 1;
        }
        return 0;
      });
      this._isSorted = true;
    }
    return this.frames;
  },
  item: function(index) {
    if (index >= this.length || index < 0) {
      return null;
    }
    return this.frames[index];
  },
  add: function(frame) {
    this.frames.push(frame);
    this._isSorted = false;
    return frame;
  },
  remove: function(frame) {
    var index = this.frames.indexOf(frame);
    if (index == -1) {
      return undefined;
    }
    this.frames.splice(index, 1);
    return frame;
  },
  clone: function() {
    var result = new KeyframeList();
    for (var i = 0; i < this.frames.length; i++) {
      result.add(new Keyframe(this.frames[i].value, this.frames[i].offset,
          this.frames[i].timingFunction));
    }
    return result;
  }
});

KeyframeList.prototype.__defineGetter__('length', function() {
  return this.frames.length;
});

var presetTimings = {
  'ease': [0.25, 0.1, 0.25, 1.0],
  'linear': [0.0, 0.0, 1.0, 1.0],
  'ease-in': [0.42, 0, 1.0, 1.0],
  'ease-out': [0, 0, 0.58, 1.0],
  'ease-in-out': [0.42, 0, 0.58, 1.0]
};

/** @constructor */
var TimingFunction = function(spec) {
  if (!Array.isArray(spec)) {
    return TimingFunction.createFromString(spec);
  }
  this.params = spec;
  this.map = []
  for (var ii = 0; ii <= 100; ii += 1) {
    var i = ii / 100;
    this.map.push([
      3*i*(1-i)*(1-i)*this.params[0] + 3*i*i*(1-i)*this.params[2] + i*i*i,
      3*i*(1-i)*(1-i)*this.params[1] + 3*i*i*(1-i)*this.params[3] + i*i*i
    ]);
  }
};

mixin(TimingFunction.prototype, {
  scaleTime: function(fraction) {
    var fst = 0;
    while (fst != 100 && fraction > this.map[fst][0]) {
      fst += 1;
    }
    if (fraction == this.map[fst][0] || fst == 0) {
      return this.map[fst][1];
    }
    var yDiff = this.map[fst][1] - this.map[fst - 1][1];
    var xDiff = this.map[fst][0] - this.map[fst - 1][0];
    var p = (fraction - this.map[fst - 1][0]) / xDiff;
    return this.map[fst - 1][1] + p * yDiff;
  },
  clone: function() {
    return new TimingFunction(this.params);
  }
});

TimingFunction.createFromString = function(spec) {
  var preset = presetTimings[spec];
  if (preset) {
    return new TimingFunction(presetTimings[spec]);
  }
  var stepMatch = /steps\(\s*(\d+)\s*,\s*(start|end|middle)\s*\)/.exec(spec);
  if (stepMatch) {
    return new StepTimingFunction(Number(stepMatch[1]), stepMatch[2]);
  }
  var bezierMatch = /cubic-bezier\(([^,]*),([^,]*),([^,]*),([^)]*)\)/.exec(spec);
  if (bezierMatch) {
    return new TimingFunction([
        Number(bezierMatch[1]),
        Number(bezierMatch[2]),
        Number(bezierMatch[3]),
        Number(bezierMatch[4])]);
  }
  throw 'not a timing function: ' + spec;
};

/** @constructor */
var StepTimingFunction = function(numSteps, position) {
  this.numSteps = numSteps;
  this.position = position || 'end';
}

inherits(StepTimingFunction, TimingFunction);
mixin(StepTimingFunction.prototype, {
  scaleTime: function(fraction) {
    if (fraction >= 1)
      return 1;
    var stepSize = 1 / this.numSteps;
    if (this.position == 'start') {
      fraction += stepSize;
    } else if (this.position == 'middle') {
      fraction += stepSize / 2;
    }
    return fraction - fraction % stepSize;
  },
  clone: function() {
    return new StepTimingFunction(this.numSteps, this.position);
  }
});

var interp = function(from, to, f, type) {
  if (Array.isArray(from) || Array.isArray(to)) {
    return interpArray(from, to, f, type);
  }
  var zero = type == 'scale' ? 1.0 : 0.0;
  to   = isDefinedAndNotNull(to) ? to : zero;
  from = isDefinedAndNotNull(from) ? from : zero;

  return to * f + from * (1 - f);
};

var interpArray = function(from, to, f, type) {
  console.assert(Array.isArray(from) || from === null,
      'From is not an array or null');
  console.assert(Array.isArray(to) || to === null,
      'To is not an array or null');
  console.assert(from === null || to === null || from.length === to.length,
      'Arrays differ in length');
  var length = from ? from.length : to.length;

  var result = [];
  for (var i = 0; i < length; i++) {
    result[i] = interp(from ? from[i] : null, to ? to[i] : null, f, type);
  }
  return result;
};

var numberType = {
  zero: function() { return 0; },
  add: function(base, delta) { return base + delta; },
  interpolate: interp,
  toCssValue: function(value) { return value + ''; },
  fromCssValue: function(value) { return value !== '' ? Number(value): null; }
};

var integerType = {
  interpolate: function(from, to, f) { return Math.floor(interp(from, to, f)); },
};
integerType.__proto__ = numberType;

var outerCalcRE = /-webkit-calc\s*\(\s*([^)]*)\)/;
var valueRE = /\s*([0-9.]*)([a-zA-Z%]*)/;
var operatorRE = /\s*([+-])/;

var percentLengthType = {
  zero: function() { return {}; },
  add: function(base, delta) { 
    var out = {};
    for (value in base) {
      out[value] = base[value] + (delta[value] || 0);
    }
    for (value in delta) {
      if (value in base) {
        continue;
      }
      out[value] = delta[value];
    }
    return out;
  },
  interpolate: function(from, to, f) {
    var out = {};
    for (var value in from) {
      out[value] = interp(from[value], to[value], f);
    }
    for (var value in to) {
      if (value in out) {
        continue;
      }
      out[value] = interp(0, to[value], f);
    }
    return out;
  },
  toCssValue: function(value) { 
    var s = '';
    var single_value = true;
    for (var item in value) {
      if (s === '') {
        s = value[item] + item;
      } else if (single_value) {
        s = '-webkit-calc(' + s + ' + ' + value[item] + item + ')';
        single_value = false;
      } else {
        s = s.substring(0, s.length - 1) + ' + ' + value[item] + item + ')';
      }
    }
    return s;
  },
  fromCssValue: function(value) {
    var out = {}
    var innards = outerCalcRE.exec(value);
    if (!innards) {
      var singleValue = valueRE.exec(value);
      if (singleValue && (singleValue.length == 3)) {
        out[singleValue[2]] = Number(singleValue[1]);
        return out;
      }
      return {};
{'fraction_non_alphanumeric': 0.1358094698043663, 'fraction_numerical': 0.006755340787219092, 'mean_word_length': 2.981838708094046, 'pattern_counts': {'":': 0, '<': 69, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5127982', 'n_tokens_mistral': 25769, 'n_tokens_neox': 25200, 'n_words': 5496}