/*
 * Copyright (c) 2006, Opera Software ASA
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Opera Software ASA nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY OPERA SOFTWARE ASA AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL OPERA SOFTWARE ASA AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Rev: 5143 $
 * $Author: runeh $
 * $Date: 2008-05-29 08:18:30 +0530 (Thu, 29 May 2008) $
 */

/**
 * @classDescription Manages animation for properties of a given object
 * @constructor
 * @param {Object} obj The object on which animations will be performed
 * @param {int} duration The duration of the animation in ms
 * @param {int} interval The interval between frames in ms
 * @param {Function} callback function to be called when the animation is finished
 * @param {Function} callback function to be called for each tick of the animation
 */
function Animator(obj, duration, interval, callback, tickcb)
{
  this.target = obj;

  /**
   * @type int
   **/
  this.duration = duration==null ? 600 : duration;

  /**
   * @type int
   **/
  this.interval = interval==null ?  50 : Animator.defaultInterval;

  /**
   * @type function
   **/
  this.callback = callback;

  /**
   * @type function
   **/
  this.tickCallback = tickcb;

  /**
   * @private
   */
  this._timerid = null;
  this.properties = [];

  /**
   * @type function
   **/
  this.accelerator = null;

  /**
   * @type boolean
   **/
  this.isRunning = false;

  /**
   * If the animator has been reversed. This toggles when the .reverse()
   * method is called. No changes to it is made when the animation finishes
   * so it can be used in animationFinished callback to check if the animation
   * that just ended was reverse or not.
   * @type boolean
   **/
  this.isReversed = false;

  /**
   * @type int
   **/
  this.pauseTime = null;

  /**
   * @type int
   **/
  this.startTime = null;

  /**
   * @private
   **/
  this._id = "";

  for (var n=0; n<16; n++)
  {
    this._id+="abcdefghijklmnoprstuvw1234567890".charAt(Math.round(Math.random()*32));
  }

  /**
   * Run the animation
   * @method
   */
  this.run = function()
  {
    this.startTime = new Date().getTime();
    if (Animator.animationEnabled)
    {
      this.isRunning = true;
      Animator.run(this)
    }
    else
    {
      this._doInstantTransform();
    }
    return this._timerid;
  }

  this.stop = function()
  {
    this.isRunning = false;
    Animator.run() // recalcs animation que, thus dirty animators will be expunged
    return this;
  }

  this.pause = function()
  {
    this.isRunning = false;
    this.pauseTime = new Date().getTime();
    return this;
  }

  this.resume = function()
  {
    this.startTime = new Date().getTime() - (this.pauseTime - this.startTime)
    this.isRunning = true;
    this.pauseTime = null;
    Animator.run(this);
    return this;
  }

  this.reverse = function()
  {
    if (this.isRunning)
    {
      for (var n=0, e; e=this.properties[n]; n++)
      {
        var f = e.from;
        e.from = e.to;
        e.to = f;
        e.range = e.to - e.from;
      }

      // adjust start time, so there is as much left as we've spent
      var now = new Date().getTime();
      this.startTime = now - (this.duration - (now - this.startTime));
      this.isReversed = this.isReversed ? false : true;

    }
    else
    {
      for (var n=0, e; e=this.properties[n]; n++)
      {
        var f = e.from;
        e.from = e.to;
        e.to = f;
        e.current = e.from;
        e.range = e.to - e.from;
      }
    }
    return this;
  }


  /**
   * We don't create the timer etc, if anims are disabled.
   * Could do this by setting anim dur to 0, but this is slightly
   * nicer if we reenable anims.
   * @private
   */
  this._doInstantTransform = function()
  {
    for (var n=0, e; e=this.properties[n]; n++)
    {
      this.target[e.name] = "" + e.to + e.unit;
    }
    if (this.callback)
    {
      this.callback(this)
    }
  }

  /**
   * Handle a single tick of animation. This function gets
   * called in the timer loop, or in the timer loop of the
   * animation manager.
   * @private
   */
  this._tick = function()
  {
    if (!this.isRunning)
    {
      return false;
    }

    if (this.tickCallback)
    {
      this.tickCallback();
    }

    var v = ( new Date().getTime() - this.startTime ) / this.duration;
    v = Math.min(1, v);
    for (var n=0, e; e=this.properties[n]; n++)
    {
      e.stepnum++;
//      opera.postError(e.name);
      this._tickForProperty(e, v);
    }

    if (v==1)
    {  // animation is finished!
      this.isRunning = false;

      for (var n=0, e; e=this.properties[n]; n++)
      {
              e.stepnum = 0;
              //e.current = e.from;  fixme:neccesary?
      }

      if (this.callback)
      {
        this.callback(this)
      }
    }
    return (v==1) ? false : true; // true means we want to tick again

  }

  /**
   * Run a single tick for the property in dict propdata
   * @private
   */
  this._tickForProperty = function(propdata, normal)
  {
    var newval = propdata.accelerator(normal, propdata.from, propdata.current, propdata.to, propdata.range, propdata.stepnum, this);
    if (newval!=null) { // on null, don't change anything!
      if (propdata.unit == "none" )
      {
        // nichts
      }
      else if (propdata.isFloat)
      {
        newval = newval.toPrecision(3);
      }
      else
      {
        newval = Math.round(newval);
      }

      propdata.current = newval;
      if (propdata.unit == "" || propdata.unit == "none")
      {
        if(!propdata.isIECSSOpacity)
        {
          // for SVG compatibility
          if (!!this.target.nodeType)
          {
            this.target.setAttribute(propdata.name,newval);
          }
          else
          {
             this.target[propdata.name] = newval;
           }
        }
        else
        {
          var _ieOpacity = Math.round(100*newval);
          // Null the filter if opacity is 1 to prevent ugly antialiasing in IE
          this.target.filter = (_ieOpacity < 100)?'progid:DXImageTransform.Microsoft.Alpha(opacity='+_ieOpacity+')':null;
        }
      }
      else
      {
          // for SVG compatibility
          if (!!this.target.nodeType)
          {
            this.target.setAttribute(propdata.name,newval+propdata.unit);
          }
          else
          {
             this.target[propdata.name] = newval+propdata.unit;
           }
      }
    }
  }

  /**
   * Set the accellerator function used for all attributes that does not get an accellerator set explicitly
   * @param {Function} func the default accellerator function
   */
  this.setDefaultAccelerator = function(func)
  {
    this.accelerator = func;
  }

  /**
   * Add a property of the object to be animated.
   * @param {String} propname name of the property
   * @param {Object} from the start value
   * @param {Object} to the end value
   * @param {String} unit the unit of the property.
   * @param {Object} accelerator a function responsible for calculating steps of the animation
   */
  this.addProperty = function(propname, from, to, unit, accelerator)
  {
    var animprop = {};
    animprop.name = propname;
    animprop.unit = (unit==null || unit == "float" || unit == "int" ) ? "" : unit;
    animprop.to = to;
    animprop.stepnum = 0;
    animprop.isFloat = unit && Animator._floatUnits[unit] ? true : false;
    if ((propname.toLowerCase() == "opacity") && window.ActiveXObject && (typeof this.target.cssText == "string"))
    {
      animprop.isIECSSOpacity = true;
    }

    if (from == null)
    {
      animprop.from = this._getPropValue(this.target, propname, unit);
    }
    else
    {
      animprop.from = from;
    }

    animprop.current = animprop.from;
    animprop.range = animprop.to - animprop.from;
    animprop.accelerator = accelerator || this.accelerator || Animator.defaultAccelerator || Animator.accelerator_linear;
    this.properties.push(animprop);
    return this;
  }

  /**
   * Gets the value of a property as an int.
   * @private
   */
  this._getPropValue = function(obj, name, unit)
  {
    if (!obj[name])
    {
      return 0;
    }; // unknown val. assume 0

    var rawval = obj[name];
    if (unit)
    {
      var index = rawval.indexOf(unit);
      if (index > 0)
      {
        rawval = rawval.slice(0, index);
      }
    }
    return parseInt(rawval);
  }
}

/**
 * List of units that may be used and flags to tell if they work in
 * floats or ints.
 * @private
 */
Animator._floatUnits =
{
  "cm":     true,
  "em":     true,
  "%":      false,
  "px":     false,
  "ex":     true,
  "float":  true,
  "int":    false,
  "in":     true,
  "pt":     true,
  "none":   true
}

/**
 * Flag to toggle animation on or off for all Animator instances
 * @type boolean
 */
Animator.animationEnabled = true;

// fixme: not sure if this should private
Animator.animators = {};

/**
 * @private
 */
Animator._timerId = null;

/**
 * Callback function that will be called for each tick globally, not for each
 * Animator object
 * @type Function
 */
Animator.tickCallback = null;

/**
 * Starts animating anim and recalcs the interval.
 * anim arg is optional. If omitted it will only recalc
 * interval.
 * If there are no anims, does not add the interval.
 * @param {Object} anim
 */
Animator.run = function(anim) {
    if (this._timerId)
    {
      window.clearInterval(this._timerId);
      this._timerId = null;
    }

    if (this.tickCallback) {
      this.tickCallback();
    }

    if (anim && !(anim._id in this.animators))
    {
      this.animators[anim._id] = anim;
    }

    var newInt = 999999;
    for (id in this.animators)
    {
      newInt = Math.min(newInt, this.animators[id].interval);
    }

    if (newInt < 999999)
    {
      this._timerId = window.setInterval(function() {Animator.tick() }, newInt);
    }

}

/**
 * Returns true if there is currently an animation, any animation, running.
 *
 */
Animator.isRunning = function()
{
  return !!this._timerId;
}

/**
 *
 * @private
 */
Animator.tick = function()
{
  var dirty = [];
  var doRecalc = false;
  for (id in this.animators)
  {
    var a = this.animators[id];
    if(!a._tick())
    {
      dirty.push(id)
    }
  }

  // we do this is in two passes since an animators callback
  // might call run() on itself.
  // A dirty list means something may be scheduled for removal
  // but not necessarily.

  if (dirty.length)
  { // something may be finished. Or may have added itself back in the cb..
    for (var n=0, id; id=dirty[n]; n++)
    {
      var a = this.animators[id];
      if (!a.isRunning) { delete this.animators[id]};
      doRecalc = true;
    }
  }

  if (doRecalc)
  {
    this.run()
  }
  // fixme: make a "defer recalc" flag.
}

/**
 * Global default accelerator function. Wil lbe applied to all animators
 * that does not set a specific one
 * @type Function
 */
Animator.defaultAccelerator = null;

/**
 * Global default tick interval
 * @type Function
 */
Animator.defaultInterval = 50;


/**
 * Accelerators return the next position of the element.
 * @argument {float} normal The normalized position in the animation, between 0 and 1.
 * @argument {float} from The start value of the property
 * @argument {float} cur The current value of the property
 * @argument {float} to The start value of the property
 * @argument {float} range The range of numbers the property will move within.
 * @argument {float} anim The Animation object.
 *
 */
Animator.accelerator_linear = function(normal, from, cur, to, range, anim)
{
  return ( range * normal ) + from ;
}

Animator.accelerator_sine = function(normal, from, cur, to, range, anim)
{
  return from+(to-from)*(.5-.5*Math.cos( normal*Math.PI ));
}

Animator.accelerator_quartic = function(normal, from, cur, to, range, anim)
{
  var v = (  Math.pow(  normal, 2 )  );
  return from + v*(to-from);
}

Animator.accelerator_cubic = function(normal, from, cur, to, range, anim)
{
  var v = (  Math.pow(  normal, 3 )  );
  return from + v*(to-from);
}
