# Copyright 2012 United States Government, as represented by the Secretary of Defense, Under
# Secretary of Defense (Personnel & Readiness).
# 
# 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.

## `animation.vwf` adds standard animation functions to a node.
## 
## Animations play over a given `animationDuration` and at a given `animationRate` with respect to
## simulation time. `animationTime` tracks time as the animation plays. Setting `animationTime`
## moves the animation. The animation plays once to the end, or with `animationLoop` repeats
## indefinitely.
## 
## Events are sent to indicate `animationStarted`, `animationStopped` and `animationLooped`. Changes
## to the animation time are announced with `animationTimeChanged.`
##
## A node may provide an `animationUpdate` method to calculate its own animation state, or it may
## use a specialized behavior that implements a standard animation.
## 
## @name animation.vwf
## @namespace

# The duration must be a non-negative number, times are always in the range [ `0`, `duration` ], and
# the rate may be any number (although there is little protection against rates very near zero).
# 
#   0 <= animationDuration
#   0 <= animationTime <= animationDuration
#   -Infinity < animationRate < Infinity
# 
# The animation play state and time index are maintained in `animationStart$` and
# `animationPause$`. `animationPause$` is `null` while the animation is playing. The
# distance from `animationStart$` to the current time (when playing) or `animationPause$`
# (when paused) gives the time index, after accounting for the play rate.
# 
#   animationTime = ( ( animationPause$ || time ) - ( animationStart$ || time ) ) *
#     animationRate (positive rates)
#   animationTime = ( ( animationPause$ || time ) - ( animationStart$ || time ) ) *
#     animationRate + animationDuration (negative rates)
# 
#   animationPlaying = this.animationStart$ != null && this.animationPause$ == null
# 
# `animationStart$` and `animationPause$` are `null` before the animation is first played.
# This state is interpreted as `animationPlaying` == `false` and `animationTime` == `0`.
# 
#   animationStart$ == null => animationPause$ == null
#   animationStart$ == null => animationStop$ == null
# 
#   animationStart$ != null => animationStart$ <= time
#   animationPause$ != null => animationStart$ <= animationPause$ <= time
#   animationStop$ != null => animationStart$ + animationDuration$ == animationStop$
# 
#   animationDuration$ == animationDuration / animationRate (positive rates)
#   animationDuration$ == -animationDuration / animationRate (negative rates)
# 
# Properties named with a`$` suffix refer to times or durations on the simulation timeline. All
# other time-related properties refer to the animation timeline.

---

properties:

  ## The current animation position in seconds.
  ## 
  ## `animationTime` automatically moves with the simulation time while the animation is playing,
  ## tracking the change in time multiplied by `animationRate`. A negative `animationRate` will
  ## cause `animationTime` to move backwards. Setting `animationTime` updates the postion whether or
  ## not the animation is currently playing.

  animationTime:

    set: |
      if(this.animationStartTime == null) {
        this.animationStartTime = 0;
      }
      if(this.animationStopTime == null) {
        this.animationStopTime = this.animationDuration;
      }
      // Save copies to avoid repeated reads.
      var duration = this.animationStopTime - this.animationStartTime,
        rate = this.animationRate;
      // Range limit the incoming value.
      value = Math.min( Math.max( this.animationStartTime, value ), this.animationStopTime );
      // Keep paused if updating start/pause from null/null. Use t=0 instead of `this.time` so that
      // setting `node.animationTime` during initialization is consistent across multiple clients.
      if ( this.animationStart$ == null ) {
        this.animationPause$ = 0;
      }
      // Calculate the start and stop times that makes the new time work.
      this.animationStart$ =
        ( this.animationPause$ != null ? this.animationPause$ : this.time ) -
        ( rate >= 0 ? value - this.animationStartTime : value - duration ) / rate;
      this.animationStop$ =
        this.animationStart$ +
        ( rate >= 0 ? duration : -duration ) / rate;
      // Update the node and fire the changed event.
      if ( value !== this.animationTimeUpdated ) {
        this.animationTimeUpdated = value;
        this.animationUpdate( value, this.animationDuration );
        this.animationTimeChanged( value );
      } //@ sourceURL=animation.animationTime.set.vwf

    get: |
      // Save copies to avoid repeated reads.
      var startTime = this.animationStartTime;
      var stopTime = this.animationStopTime;
      var rate = this.animationRate;
      var animationPause$ = this.animationPause$;
      var animationStart$ = this.animationStart$;
      var time = this.time;
      // Calculate the time from the start and current/pause times.
      var value = (
        ( animationPause$ != null ? animationPause$ : time ) -
        ( animationStart$ != null ? animationStart$ : time )
      ) * rate + ( rate >= 0 ? startTime : stopTime );
      // Range limit the value.
      value = Math.min( Math.max( startTime, value ), stopTime );
      // If changed since last seen, update and fire the changed event.
      if ( value !== this.animationTimeUpdated ) {
        this.animationTimeUpdated = value;
        this.animationUpdate( value, this.animationDuration );
        this.animationTimeChanged( value );
      }
      return value; //@ sourceURL=animation.animationTime.get.vwf

  ## The length of the animation in seconds.
  ## 
  ## `animationTime` will always be within the range [ `0`, `animationDuration` ]. Animations
  ## automatically stop playing once `animationTime` reaches `animationDuration` unless
  ## `animationLoop` is set. Animations with a negative `animationRate` start at
  ## `animationDuration` and stop at `0`.

  animationDuration:  # TODO: allow changes while playing
    set: |
      var duration = value, rate = this.animationRate;
      this.animationDuration = duration;
      this.animationDuration$ = ( rate >= 0 ? duration : -duration ) / rate;
    value: 1

  ## The animation playback rate.
  ## 
  ## Set `animationRate` to a value greater than `1` to increase the rate. Set a value between `0`
  ## and `1` to decrease it. Negative rates will cause the animation to play backwards.

  animationRate:  # TODO: allow changes while playing
    set: |
      var duration = this.animationDuration, rate = value;
      this.animationRate = rate;
      this.animationDuration$ = ( rate >= 0 ? duration : -duration ) / rate;
    value: 1

  ## Automatically replay the animation from the start after reaching the end.

  animationLoop: false

  ## The animation's play/pause control.

  animationPlaying:

    set: |
      if(this.animationStartTime == null) {
        this.animationStartTime = 0;
      }
      if(this.animationStopTime == null) {
        this.animationStopTime = this.animationDuration;
      }
      if ( this.animationStart$ != null && this.animationPause$ == null ) {
        if ( ! value ) {
          // Mark as paused at the current time.
          this.animationPause$ = this.time;
          // Send the `animationStopped` event if stopping at the end.
          if ( this.time == this.animationStop$ ) {
            this.animationStopped();
          }
        }
      } else {
        if ( value ) {
          // Save copies to avoid repeated reads.
          var duration = this.animationStopTime - this.animationStartTime,
            rate = this.animationRate;
          // Start from the beginning if resuming from the end.
          if ( this.animationPause$ == this.animationStop$ ) {
            this.animationPause$ = this.animationStart$;
          }
          // Recalculate the start and stop times to keep paused time unchanged, then resume.
          this.animationStart$ =
            ( this.animationStart$ != null ? this.animationStart$ : this.time ) -
            ( this.animationPause$ != null ? this.animationPause$ : this.time ) +
            this.time;
          this.animationStop$ =
            this.animationStart$ +
            ( rate >= 0 ? duration : -duration ) / rate;
          this.animationPause$ = null;
          // Send the `animationStarted` event if starting from the beginning.
          if ( this.time == this.animationStart$ ) {
            this.animationStarted();
          }
          // Start the animation worker.
          this.logger.debug( "scheduling animationTick" );
          this.animationTick();
        }
      } //@ sourceURL=animation.animationPlaying.set.vwf

    get: |
      return this.animationStart$ != null && this.animationPause$ == null;

  ## The most recent value of `animationTime` recognized by this behavior.
  ## 
  ## Each `animationTime` change causes `animationUpdate` to be called. `animationTimeUpdated`
  ## records the value of `animationTime` from the last time this occurred.
  ## 
  ## `animationTimeUpdated` is for internal use. Do not set this property.

  animationTimeUpdated: null

  ## The simulation time corresponding to the start of the animation.
  ## 
  ## `animationStart$` is `null` until the animation is first played. `animationPlaying` is
  ## `false` and `animationTime` is `0` while `animationStart$` is `null`.
  ## 
  ## `animationStart$` is for internal use. Do not set this property.

  animationStart$: null

  ## The simulation time corresponding to the animation's pause position.
  ## 
  ## `animationPause$` is `null` while the animation is playing and before the animation is
  ## first played.
  ## 
  ## `animationPause$` is for internal use. Do not set this property.

  animationPause$: null

  ## The simulation time corresponding to the end of the animation. The animation worker function
  ## loops or stops the animation when `time` reaches this value.
  ## 
  ## `animationStop$` is for internal use. Do not set this property.

  animationStop$: null

  ## The animation's duration in simulation time, after considering `animationRate`.
  ## 
  ## `animationDuration$` is for internal use. Do not set this property.

  animationDuration$: null

  ## The time that the animation should start at. Used with animationStopTime to play
  ## a subsection of the animation. Defaults to 0 when the animation starts to play
  ## if it is not assigned another value. 'animationStartTime' will always be within 
  ## the range [ `0`, `animationDuration` ]

  animationStartTime: 
    set: |
      this.animationStartTime = value ? Math.min( Math.max( 0, value ), this.animationDuration ) : value;
    value: null

  ## The time that the animation should stop at. Used with animationStartTime to play
  ## a subsection of the animation. Defaults to 'animationDuration' when the animation
  ## starts to play if it is not assigned another value. 'animationStopTime' will always
  ## be within the range [ `0`, `animationDuration` ]

  animationStopTime:  
    set: |
      this.animationStopTime = value ? Math.min( Math.max( 0, value ), this.animationDuration ) : value;
    value: null

  ## The frame that the animation should start at. Setting this value automatically updates the "animationStartTime"
  ## to the "animationStartFrame" / "fps"

  animationStartFrame:
    set: |
      this.animationStartTime = value / this.animationFPS;
    get: |
      return Math.floor(this.animationStartTime * this.animationFPS);

  ## The frame that the animation should stop at. Setting this value automatically updates the "animationStopTime"
  ## to the "animationStopFrame" / "fps"

  animationStopFrame:
    set: |
      this.animationStopTime = value / this.animationFPS;
    get: |
      return Math.floor(this.animationStopTime * this.animationFPS);

  ## The frames per second of the animation loaded from the source model.

  animationFPS: 30

  ## The number of frames of the animation loaded from the source model. 
  animationFrames:
    set: |
      this.animationDuration = value / this.animationFPS;
    get: |
      return Math.ceil(this.animationFPS * this.animationDuration); 

  ## The current frame the animation is on. Equivalent to animationTime.
  animationFrame:
    set: |
      if(this.animationFPS) {
        this.animationTime = value / this.animationFPS;
      }
    get: |
      if(this.animationFPS) {
        return Math.floor(this.animationTime * this.animationFPS);
      }

  ## The animation update rate in ticks per second of simulation time.

  animationTPS: 60

methods:

  # Play the animation from the start.

  animationPlay:
    parameters:
      - startTime
      - stopTime
    body: |
      if(!isNaN(stopTime)) {
        this.animationStopTime = stopTime;
      }
      if(!isNaN(startTime)) {
        this.animationStartTime = startTime;
      }
      this.animationPlaying = true;

  # Pause the animation at the current position.

  animationPause: |
    this.animationPlaying = false;

  # Resume the animation from the current position.

  animationResume: |
    this.animationPlaying = true;

  # Stop the animation and reset the position to the start.

  animationStop: |
    this.animationPlaying = false;
    this.animationTime = 0;

  ## Update the animation. If `animationTime` has reached the end, stop or loop depending on the
  ## `animationLoop` setting. Schedule the next tick if the animation did not stop.
  ## 
  ## `animationTick` is for internal use. Do not call this method directly.

  animationTick: |
    if ( this.animationPlaying ) {
      // Read the time to recognize the current time and update.
      // TODO: move side effects out of the getter!!! (says Kevin)
      this.animationTime; 
      // Loop or stop after reaching the end.
      if ( this.time === this.animationStop$ ) {
        if ( this.animationLoop ) {
          this.animationLooped();
          this.animationTime = this.animationRate >= 0 ?
            this.animationStartTime : this.animationStopTime;
        } else {
          this.animationPlaying = false;
        }
      }
      // Schedule the next tick if still playing.
      if ( this.animationPlaying ) {
        if ( this.animationStop$ - this.time > 1 / this.animationTPS ) {
          this.in( 1 / this.animationTPS ).animationTick(); // next interval
        } else {

          // TODO: When animationStop$ is 0 (usually when a model does not actually have an 
          //       animation on it), we schedule a method call for a time in the past (at time 0).
          //       That immediately calls animationTick again, but this.time does not equal 
          //       animationStop$ as we would expect.  So, it doesn't stop the animation and we get
          //       caught in an infinite loop.
          //       Ideally we should catch the case where animationStop$ is 0 before this point.
          //       But for now, we catch it here.  
          if ( this.animationStop$ > 0 ) {
            this.at( this.animationStop$ ).animationTick(); // exactly at end
          } else {
            this.animationPlaying = false;
          }
        }
      } else {
        this.logger.debug( "canceling animationTick" );
      }
    } //@ sourceURL=animation.animationTick.vwf

  ## `animation.vwf` calls `animationUpdate` each time `animationTime` changes. Nodes that
  ## implement `animation.vwf` should provide an `animationUpdate` method to calculate the animation
  ## state appropriate for the node.
  ## 
  ## Since this event is sent within the `animationTime` getter function, references to
  ## `this.animationTime` will return `undefined` due to reentrancy protections. Use the provided
  ## `time` parameter instead.
  ## 
  ## @param {Number} time
  ##   The animation's `animationTime`.
  ## @param {Number} duration
  ##   The animation's `animationDuration`.

  animationUpdate:
    parameters:
      - time
      - duration

events:

  # Sent when the animation starts playing from the beginning.

  animationStarted:

  # Sent when the animation reaches the end and `animationLoop` is not set.

  animationStopped:

  # Sent when the animation reaches the end and `animationLoop` is set.

  animationLooped:

  ## Sent each time `animationTime` changes.
  ## 
  ## Since this event is sent within the `animationTime` getter function, references to
  ## `this.animationTime` will return `undefined` due to reentrancy protections. Use the provided
  ## `time` parameter instead.
  ## 
  ## @param {Number} time
  ##   The animation's `animationTime`.

  animationTimeChanged:
    parameters:
      - time
scripts:
- |
  this.initialize = function() {

    // Locate child nodes that extend or implement "http://vwf.example.com/animation/position.vwf"
    // to identify themselves as animation key positions.

    var positions = this.find( "./element(*,'http://vwf.example.com/animation/position.vwf')" );

    // Fill in missing `animationTime` properties, distributing evenly between the left and right
    // positions that define `animationTime`.

    // 1: [ - ] => [ 0 ]
    // 1: [ 0, - ] => [ 0, 1 ]
    // 1: [ -, 1 ] => [ 0, 1 ]
    // 1: [ 0, -, - ] => [ 0, 1/2, 1 ]
    // 1: [ -, -, 1 ] => [ 0, 1/2, 1 ]
    // 1: [ 0, - , -, 1 ] => [ 0, 1/3 , 2/3, 1 ]

    var leftTime, leftIndex;
    var rightTime, rightIndex = -Infinity;

    if ( positions.length > 0 ) {
      
      positions.sort(function(a, b) {
        return a.sequence - b.sequence;
      });

      if ( positions[0].animationTime === null ) {
        positions[0].animationTime = 0;
      }

      if ( positions[positions.length-1].animationTime === null ) {
        positions[positions.length-1].animationTime = this.animationDuration;
      }

      positions.forEach( function( position, index ) {

        if ( position.animationTime !== null ) {

          leftTime = position.animationTime;
          leftIndex = index;

        } else {

          if ( index > rightIndex ) {
            for ( rightIndex = index + 1; rightIndex < positions.length; rightIndex++ ) {
              if ( ( rightTime = /* assignment! */ positions[rightIndex].animationTime ) !== null ) {
                break;
              }
            }
          }

          position.animationTime = leftTime + ( rightTime - leftTime ) *
            ( index - leftIndex )  / ( rightIndex - leftIndex );

        }

      }, this );

    }

  } //@ sourceURL=http://vwf.example.com/animation.vwf/scripts~initialize
