Balloon = function(config){
  // confrigurable {
  this.speed = 5000;
  this.speed_factor = 2;
  this.x = 0;
  this.y = 0;
  this.init_y = 0;
  this.init_x = 0;
  this.auto_init = true;
  this.scan_delay = 250;
  this.animate_delay = 250;
  this.selector = ''; // required
  // }
  
  this.view_offset = new Offset();
  this.view_size = new Size();
  this.saved_scroll = 0;
  this.saved_width = 0;
  this.animate_timer = null;
  this.saved_diff = new Offset();
  
  $.extend(this, config);
  
  if(this.auto_init)
    this.init();
}

Balloon.prototype = {
  init: function() {
    this.initPosition();
    this.initScanTimer();
  },
  
  initPosition: function() {
    this.getEl().css({
      top: this.init_y,
      left: this.calcX(this.init_x),
      position: 'absolute'
    });
  },
  
  initScanTimer: function() {
    setTimeout(this.onScan.createDelegate(this), this.scan_delay);
  },
  
  startAnimateTimer: function() {
    this.stopAnimateTimer();
    
    this.animate_timer = setTimeout(this.onAnimationStart.createDelegate(this), this.animate_delay);
  },
  
  stopAnimateTimer: function() {
    if(this.animate_timer !== null)
      clearTimeout(this.animate_timer);
      
    this.animate_timer = null;
  },
  
  _animate: function() {
    this.saveView();
    var diff = this.getDiff();
    if(diff.top > 0)
      this.down(diff.top * 1.3);
    else if(diff.top < 0 || diff.left != 0)
      this.up(-diff.top, diff.left);
    else
      this.initScanTimer();
  },
  
  down: function(top_diff) {
    this.getEl().animate({
      top: '+=' + top_diff
    }, {
      duration: this.speed,
      easing: 'quadEaseOut',
      complete: this._animate.createDelegate(this),
      step: this.onAnimationStep.createDelegate(this)
    });
  },
  
  up: function(top_diff, left_diff) {
    this.getEl().animate({
      top: '-=' + top_diff,
      left: '+=' + left_diff
    }, {
      duration: this.speed + this.calcSpeedIncrement(top_diff), 
      complete: this._animate.createDelegate(this),
      step: this.onAnimationStep.createDelegate(this),
      easing: 'bounceEaseOut'/*,
      specialEasing: {
        top: 'bounceEaseOut',
        left: 'elasticEaseOut'
      }*/
    });
  },
  
  getDiff: function() {
    var offset = new Offset(this.getEl().offset());
    var view_offset = new Offset(this.calcX(this.x), this.getViewOffset().top + this.y);
    return view_offset.sub(offset);
  },
  
  getViewOffset: function() {
    this.view_offset.setup(
      self.pageXOffset || (document.documentElement && document.documentElement.scrollLeft) || (document.body && document.body.scrollLeft),
      self.pageYOffset || (document.documentElement && document.documentElement.scrollTop) || (document.body && document.body.scrollTop)
    );
    return this.view_offset;
  },
  
  getViewSize: function() {
    this.view_size.setup(
      document.compatMode=='CSS1Compat' ?document.documentElement.clientWidth : document.body.clientWidth,
      document.compatMode=='CSS1Compat' ?document.documentElement.clientHeight : document.body.clientHeight
    );
    return this.view_size; 
  },
  
  saveView: function() {
    this.saved_scroll = this.getViewOffset().top;
    this.saved_width = this.getViewSize().width;
  },
  
  isViewChanged: function() {
    return this.getViewOffset().top != this.saved_scroll
           || this.getViewSize().width != this.saved_width;
  },
  
  calcX: function(x) {
    if(this.x >= 0)
      return x;
      
    return this.getViewSize().width + x;
  },
  
  calcSpeedIncrement: function(diff) {
    return diff / $(document).height() * this.speed_factor * this.speed;
  },

  getEl: function() {
    return $(this.selector);
  },
  
  onAnimationStep: function() {
    if(this.isViewChanged()) {
      this.getEl().stop();
      this._animate();
    }
  },
  
  onAnimationStart: function() {
    var diff = this.getDiff();
    if(diff.top != this.saved_diff.top || diff.left != this.saved_diff.left)
      this.initScanTimer();
    else
      this._animate();
  },
  
  onScan: function() {
    var diff = this.getDiff();
    if (diff.top != 0 || diff.left != 0) {
      this.saved_diff.setup(diff);
      this.startAnimateTimer();
    } else {
      this.saved_diff.setup(0, 0);
      this.initScanTimer();
    }
  }
}
