(function($) {
    $.tipsy = function(target, settings) {
      this.settings = $.extend({}, $.tipsy.defaults, settings);
      this.target = target;
      this.timeoutId = null;

      this.init();
    };
  
    $.extend($.tipsy, {
      defaults: {
        hover: true,
        extraClass: "",
        maxlength: null,
        message: "",
        fade: false,
        gravity: "n",
        notify: false,
        notifyDuration: 4000,
        delay: 500,
        hideAll: false,
        align: "center"
      },

      setDefaults: function(settings) {
        $.extend($.tipsy.defaults, settings);
      },

      prototype: {
        init: function() {},
        showTip: function(directMessage) {
          var self = this;
          var target = self.target;
          var message = directMessage || target.attr("title");
          var tip = $.data(target, "active.tipsy");

          // never show tip for hidden target
          if(target.is(":hidden") || target.is(".hiding")) {
            return;
          }

          // shorted message if necessary
          if(self.settings.maxlength != null && message.length > self.settings.maxlength) {
            message = message.substr(0, self.settings.maxlength) + "...";
          }

          // create a new tip
          if(!tip) {
              tip = $("<div class='tipsy " + (self.settings.extraClass || "") + "'><div class='tipsy-inner'>" + message + "</div></div>");
              
              // only clear title (to avoid duplicate tooltip) if no directMessage provided
              if(!directMessage) {
                target.attr("title", "");
              }
              
              // cache tip
              $.data(target, "active.tipsy", tip);

          } else if(directMessage) {
            $(".tipsy-inner", tip).text(message);
          }
          
          var pos = $.extend({}, target.offset(), {
            width: target[0].offsetWidth,
            height: target[0].offsetHeight
          });

          tip.remove().css({
            top: 0,
            left: 0,
            visibility: "hidden",
            display: "block" }).appendTo(document.body);
          
          var actualWidth = tip[0].offsetWidth, actualHeight = tip[0].offsetHeight;
          var align = self.settings.align.toLowerCase();
          var scalar = align == "max" ? 2 : align == "min" ? 0 : 1;

          switch (self.settings.gravity.charAt(0)) {
              case "n":
                  tip.css({top: pos.top + pos.height, left: pos.left + scalar * pos.width / 2 - actualWidth / 2}).addClass("tipsy-north");
                  break;
              case "s":
                  tip.css({top: pos.top - actualHeight, left: pos.left + scalar * pos.width / 2 - actualWidth / 2}).addClass("tipsy-south");
                  break;
              case "e":
                  tip.css({top: pos.top + scalar * pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}).addClass("tipsy-east");
                  break;
              case "w":
                  tip.css({top: pos.top + scalar * pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}).addClass("tipsy-west");
                  break;
          }

          if (self.settings.fade) {
              tip.css({ opacity: 0, display: "block", visibility: "visible" }).animate({opacity: 1});
          } else {
              tip.css({ visibility: "visible" });
          }
        },

        hideTip: function(forceFade) {
          var tip = $.data(this.target, "active.tipsy");
          
          // cancel pending tips
          if(this.timeoutId != null) {
            clearTimeout(this.timeoutId);
          }

          // no tip to remove, do nothing
          if(!tip) {
            return;
          }

          if (this.settings.fade || forceFade) {
              tip.stop().fadeOut(function() { tip.remove(); });
          } else {
              tip.remove();
          }
        }
    }});

    $.fn.tipsy = function(settings) {
      return this.each(function() {
        var $this = $(this);
        var tp = $this.data("tipsy");

        if(tp) {
          if(settings.hideAll) {
            tp.hideTip();
          }

          return true;
        }
 
        // disable all other functionality for hideAll mode
        if(settings.hideAll) {
          return true;
        }

        // generate new tipsy instance
        tp = new $.tipsy($this, settings);
        $this.data("tipsy", tp).addClass("tipsy-target");

        if(tp.settings.notify) {
          tp.showTip(tp.settings.message);

          if(tp.settings.notifyDuration != null) {
            tp.timeoutId = setTimeout(function () { tp.hideTip(true); }, tp.settings.notifyDuration);
          }
        }

        if(tp.settings.hover) {
          return $this.hover(function() {
            tp.timeoutId = setTimeout(function () { tp.showTip(tp.settings.message) }, tp.settings.delay);
          }, function() {
            tp.hideTip();
          });
        }
      });
    };
})(jQuery);

