function Spinner(id, config)
{
 if ( config )
 {
  this.config = config;
 }
 else
 {
  this.config = new Spinner.Config();
 }
 this.id = id;
 this._intervalIncrease = null;
 this._toid = null;
 this._incrementAmount = 1;
 this._intervalAmount = 100;
 this._intervalMode = 'single';
 this._isUpEnabled = false;
 this._isDownEnabled = false;
 this.generated = false;
}

Spinner._liste = [];

Spinner.Config = function ()
{
  /* valeur mini */
  this.min = -65535;
  /* valeur maxi */
  this.max = 65536;
  /* quantité de déplacement sur keypress / click en mode single */
  this.amountSingle = 1;
  /* quantité de déplacement sur keypress / click en mode page */
  this.amountPage = 10;
  /* quantité de déplacement sur mousewheel */
  this.wheelAmount = 2;
  /* paramètres de l'intervalle quand on reste clické sur le btn */
  this.interval = {
    /* temps en ms avant prochaine intervalle  */
    amount: 100,
    /* premiere intervalle en ms */
    first: 500,
    /* quantité retranché à la prochaine intervalle en ms */
    decrease: 2,
    /* intervalle minimum a respectée */
    min: 20,
    /* si interval.min est atteint, quantité multipliant la prochaine intervalle (en relation avec la précédente intervalle) */
    growth: 1.01
  };
  /* handler quand la valeur est fixée */
  this.handler = null;
};

Spinner.prototype.generate = function()
{
  var I = $(this.id);
  if ( !I || I.type !== 'text' ) { return false; }
  // @todo : inventer un système pour que la désactivation de l'autocomplete
  // soit générique et sans erreurs Xbrowsers
  I.setAttribute("autocomplete", "off");
  I.style.textAlign = 'right';
  // @todo : a quoi ca sert de resizer ?
//  I.style.width = (I.offsetWidth - 15) + 'px';

  var U = document.createElement('div');
  U.id = 'spinnerUp_' + this.id;
  DOM.setCSS(U, 'spinnerUp');
  /* dans la classe */
  var US = U.style;
  US.lineHeight = '0px';
  US.fontSize = '0px';
  US.padding = '0px';
  US.margin = '0px';
  US.position = 'absolute';
  US.background = '#EBE9ED url(/images/widgets/arrows/up_small.gif) no-repeat center center';
  US.borderStyle = 'outset';
  US.borderWidth = '1px';
  US.borderColor = 'rgb(212,208,200)';
  US.width = '11px';
  /* fin classe */

  var D = document.createElement('div');
  D.id = 'spinnerDown_' + this.id;
  DOM.setCSS(D, 'spinnerDown');
  /* dans la classe */
  var DS = D.style;
  DS.lineHeight = '0px';
  DS.fontSize = '0px';
  DS.padding = '0px';
  DS.margin = '0px';
  DS.position = 'absolute';
  DS.background = '#EBE9ED url(/images/widgets/arrows/down_small.gif) no-repeat center center';
  DS.borderStyle = 'outset';
  DS.borderWidth = '1px';
  DS.borderColor = 'rgb(212,208,200)';
  DS.width = '11px';
  /* fin classe */

  EVT.add(U, 'mousedown', Spinner._onMouseDown_btnUp, this);
  EVT.add(D, 'mousedown', Spinner._onMouseDown_btnDown, this);

  EVT.addWheel(I, Spinner._onWheel, this);

  EVT.add(I, "input", this.fixeValue, this);
  EVT.add(I, "blur", this.fixeValue, this);
  EVT.add(I, "change", this.fixeValue, this);

  I.onkeypress = Spinner._onKeyPress;
  I.onkeydown = Spinner._onKeyDown;
  I.onkeyup = Spinner._onKeyUp;

  document.body.appendChild(U);
  document.body.appendChild(D);
  
  this._setSize();
  this.fixeValue();


  Spinner._liste.push(this);

  // FIX ME : devrait pas etre fait ici, il semblerait qu'il manque un listener quelquepart
  Spinner.repositionne();
  this.generated = true;
  return true;
};

Spinner.getInstance = function(id)
{
  var R = null;
  for ( var i=0, imax=Spinner._liste.length; i<imax; i++ )
  {
    R = Spinner._liste[i];
    if ( R.id == id )
    {
      break;
    }
  }
  return R;
};
/*
  -------------------------------------------------------------------------------
    KEY EVENT-HANDLING
  -------------------------------------------------------------------------------
*/
/**
 * @scope l'élément HTML <input>
 */
Spinner._onKeyPress = function(evt)
{
  var S = Spinner.getInstance(this.id);
  if ( !S ) { return true; }
  evt = EVT.fix(evt);
  var vCode = EVT.getCharCode(evt);
  
  var T = EVT.touches;
  if ( vCode == T.enter && !evt.altKey )
  {
    S.fixeValue();
    S.selectAll();
  }
  else
  {
    if ( evt.ctrlKey )
    {
      if ( vCode == T.home )
      {
        S.setValue(S.config.min);
      }
      else if ( vCode == T.end )
      {
        S.setValue(S.config.max);
      }
    }
  }
  return true;
};

/**
 * @scope l'élément HTML <input>
 */
Spinner._onKeyDown = function(evt)
{
  var S = Spinner.getInstance(this.id);
  if ( S && S._intervalIncrease === null )
  {
    evt = EVT.fix(evt);
    var vCode = EVT.getCharCode(evt);
    var T = EVT.touches;
    switch ( vCode )
    {
      case T.up:
      case T.down:
        S._intervalIncrease = vCode == T.up;
        S._intervalMode = "single";

        S.resetIncrements(S.config.amountSingle);
        S.fixeValue();

        S.increment();
        S._startTimer();
      break;

      case T.pageup:
      case T.pagedown:
        S._intervalIncrease = vCode == T.pageup;
        S._intervalMode = "page";

        S.resetIncrements(S.config.amountPage);
        S.fixeValue();

        S.increment();
        S._startTimer();
      break;
    }
  }
  return true;
};
/**
 * @scope l'élément HTML <input>
 */
Spinner._onKeyUp = function(evt)
{
  var S = Spinner.getInstance(this.id);
  if ( S && S._intervalIncrease !== null )
  {
    evt = EVT.fix(evt);
    var vCode = EVT.getCharCode(evt);
    var T = EVT.touches;
    switch ( vCode )
    {
      case T.up:
      case T.down:
      case T.pageup:
      case T.pagedown:
        S._stopTimer();
        S._intervalIncrease = null;
        S._intervalMode = null;
      break;
    }
  }
  return true;
};

/*
  -------------------------------------------------------------------------------
    MOUSE EVENT-HANDLING
  -------------------------------------------------------------------------------
*/

/**
 * @scope l'instance de Spinner
 */
Spinner._onMouseDown_btnUp = function(evt)
{
  return Spinner.__onMouseDown.call(this, evt, 'Up');
};
/**
 * @scope l'instance de Spinner
 */
Spinner._onMouseDown_btnDown = function(evt)
{
  return Spinner.__onMouseDown.call(this, evt, 'Down');
};
/**
 * @scope l'instance de Spinner
 */
Spinner.__onMouseDown = function(evt, btnType)
{
  if ( evt.button != EVT.buttons.left ) { return false; }
  if ( !this['_is' + btnType + 'Enabled'] ) { return false; }

  var B = $('spinner' + btnType + '_' + this.id);
  B.style.borderStyle = 'inset';

  var fn = Spinner['_onMouseStop_btn' + btnType];
  EVT.add(B, 'mouseup', fn, this);
  EVT.add(B, 'mouseout', fn, this);

  this._intervalIncrease = btnType == 'Up';
  this.resetIncrements(this.config.amountSingle);
  this.increment();

  this.selectAll();
  this._startTimer();

  return true;
};
/**
 * @scope l'instance de Spinner
 */
Spinner._onMouseStop_btnUp = function(evt)
{
  return Spinner.__onMouseStop.call(this, evt, 'Up');
};
/**
 * @scope l'instance de Spinner
 */
Spinner._onMouseStop_btnDown = function(evt)
{
  return Spinner.__onMouseStop.call(this, evt, 'Down');
};
/**
 * @scope l'instance de Spinner
 */
Spinner.__onMouseStop = function(evt, btnType)
{
  var btn = $('spinner' + btnType + '_' + this.id);
  btn.style.borderStyle = 'outset';

  var fn = Spinner['_onMouseStop_btn' + btnType];
  EVT.remove(btn, 'mouseup', fn);
  EVT.remove(btn, 'mouseout', fn);

  this._stopTimer();
  this._intervalIncrease = null;

  this.fixeValue();
  this.selectAll();
  this.focus();

  return true;
};
/**
 * @scope l'instance de Spinner
 */
Spinner._onWheel = function(evt)
{
  this.setValue( this.getValue() + ( this.config.wheelAmount * EVT.getWheelDelta(evt) ) );
  this.selectAll();
  EVT.stop(evt);
  return false;
};

/*
  -------------------------------------------------------------------------------
    UTILITY
  -------------------------------------------------------------------------------
*/

Spinner.prototype._setSize = function()
{
  var I = $(this.id);
  var TW = parseInt(DOM.getStyle(I, 'borderTopWidth'), 10);
  var BW = parseInt(DOM.getStyle(I, 'borderBottomWidth'), 10);
  var PT = parseInt(DOM.getStyle(I, 'paddingTop'), 10);
  var PB = parseInt(DOM.getStyle(I, 'paddingBottom'), 10);
  var H = parseInt( ( I.offsetHeight - TW - BW - PT - PB) / 2, 10 );

  $('spinnerUp_' + this.id).style.height = H + 'px';
  $('spinnerDown_' + this.id).style.height = H + 'px';
};

Spinner.prototype._positionne = function()
{
  var I = $(this.id);
//  I.style.width = ( I.offsetWidth - 13 ) + 'px';
  var U = $('spinnerUp_' + this.id);
  var D = $('spinnerDown_' + this.id);

//  var LW = parseInt(DOM.getStyle(I, 'borderLeftWidth'), 10);
//  var RW = parseInt(DOM.getStyle(I, 'borderRightWidth'), 10);
  var TW = parseInt(DOM.getStyle(I, 'borderTopWidth'), 10);
  var BW = parseInt(DOM.getStyle(I, 'borderBottomWidth'), 10);
  var PT = parseInt(DOM.getStyle(I, 'paddingTop'), 10);
  var PB = parseInt(DOM.getStyle(I, 'paddingBottom'), 10);

  var X = DOM.getX(I);
  var Y = DOM.getY(I);
  var W = I.offsetWidth;
  var H = I.offsetHeight;
  var height = parseInt( ( H - TW - BW - PT - PB) / 2, 10);

  U.style.top = ( Y - TW ) + 'px';
  D.style.top = ( Y - TW + height ) + 'px';

  // avant n'était fait que pour IE et visiblement FX le veut aussi
  W = W + parseInt(DOM.getStyle(I, 'paddingRight'), 10);
  W = W + parseInt(DOM.getStyle(I, 'paddingLeft'), 10);

  U.style.left = ( X + W ) + 'px';
  D.style.left = ( X + W ) + 'px';
};

Spinner.prototype.setEnabled = function(btnType, etat)
{
  this['_is' + btnType + 'Enabled'] = etat;
//  DOM.setOpacity($('spinner' + btnType + '_' + this.id), etat ? 1 : 0.5);
  $('spinner' + btnType + '_' + this.id).style.visibility = etat ? '' : 'hidden';
};

Spinner.prototype.fixeValue = function()
{
  var
   I = $(this.id),
   V = I.value,
   C = this.config;

  // fixe les 0 au début qui casse le parseInt() -> parseInt('00-100', 10) ==> 0 au lieu de -100 :/
  if ( V.length > 1 )
  {
    while ( V.charAt(0) == '0' )
    {
      V = V.substr(1, V.length);
    }
  }

  // fixe la gestion des entiers négatifs
  if ( ( V == '-' || ( /^-[0-9]+$/.test(V) ) ) && C.min < 0 )
  {
    this.setEnabled('Down', true);
    if ( C.min > 0 )
    {
      this.setEnabled('Up', true);
    }
  }
  else
  {
    V = parseInt(V, 10) || 0;
    V = V.limit(C.min, C.max);
    this.setEnabled('Down', V > C.min);
    this.setEnabled('Up', V < C.max);
  }

  I.value = V;
  
  if ( this.generated && C.handler ) { C.handler.call(I, V); }
};

Spinner.prototype.selectAll = function()
{
  try { $(this.id).select(); } catch(x) {}
};

Spinner.prototype.focus = function()
{
  try { $(this.id).focus(); } catch(x) {}
};

Spinner.prototype.resetIncrements = function(amount)
{
  this._stopTimer();
  this._incrementAmount = amount || 1;
  this._intervalAmount = this.config.interval.amount;
};

Spinner.prototype.increment = function()
{
  var increment = ( this._intervalIncrease ? 1 : -1 ) * this._incrementAmount;
  var value = this.getValue() + increment;
  this.setValue(value);
};

Spinner.prototype.getValue = function()
{
  return parseInt($(this.id).value, 10);
};

Spinner.prototype.setValue = function(V)
{
  $(this.id).value = parseInt(V.limit(this.config.min, this.config.max), 10);
  this.fixeValue();
};

/*
  -------------------------------------------------------------------------------
    TIMER
  -------------------------------------------------------------------------------
*/

Spinner.prototype._startTimer = function()
{
  this._stopTimer();
  // porky
  var o = this;
  this._toid = setTimeout(function() { o._onTimer(); }, this.config.interval.first);
};

Spinner.prototype._onTimer = function()
{
  this._stopTimer();
  var C = this.config;
  var CI = C.interval;
  this._intervalAmount = Math.max(CI.min, this._intervalAmount - CI.decrease);

  if ( this._intervalMode == "single" )
  {
    if ( this._intervalAmount <= CI.min )
    {
      this._incrementAmount = CI.growth * this._incrementAmount;
    }
  }
  this.increment();
  var V = this.getValue();
  if ( this._intervalIncrease && ( V >= C.max || V <= C.min ) )
  {
    return false;
  }

  var o = this;
  this._toid = setTimeout(function() { o._onTimer(); }, this._intervalAmount);
  
  return true;
};

Spinner.prototype._stopTimer = function()
{
  if ( this._toid )
  {
    clearTimeout(this._toid);
  }
};

/* gestion du resize */
Spinner.repositionne = function()
{
  for ( var i = Spinner._liste.length; i--; )
  {
    Spinner._liste[i]._positionne();
  }
  return true;
};

Spinner.initialise = function(id, vMin, vMax)
{
  var S = new Spinner(id);
  S.config.min = vMin;
  S.config.max = vMax;
  S.generate();
  return S;
};

Spinner.prototype.dispose = function()
{
  var I = $(this.id);
  var U =  $('spinnerUp_' + this.id);
  var D =  $('spinnerDown_' + this.id);

  EVT.remove(U, 'mousedown', Spinner._onMouseDown_btnUp);
  EVT.remove(D, 'mousedown', Spinner._onMouseDown_btnDown);

  EVT.removeWheel(I, Spinner._onWheel);

  EVT.remove(I, "input", this.fixeValue);
  EVT.remove(I, "blur", this.fixeValue);
  EVT.remove(I, "change", this.fixeValue);

  I.onkeypress = null;
  I.onkeydown = null;
  I.onkeyup = null;

  document.body.removeChild(U);
  document.body.removeChild(D);
  return Spinner._genericUnloader();
};

Spinner._dispose = function(evt)
{
  for ( var i=0, imax=Spinner._liste.length; i<imax; i++ )
  {
    Spinner._liste[i].dispose();
  }
  return true;
};

if ( window.$$app )
{
  window.$$app.addListener('layout_change', Spinner.repositionne);
}
else
{
  EVT.add(window, "resize", Spinner.repositionne);
  EVT.unloader(
    function()
    {
      EVT.remove(window, "resize", Spinner.repositionne);
    }
  );
  EVT.loader(Spinner.repositionne);
}
EVT.unloader(Spinner._dispose);

Spinner._genericUnloader = function() { return true; };