(function ($) {

	var Menu = function (element, options) {
		this.element = $(element);
		this.stack = [];

		this.options = $.extend({}, {
			delay: 400,
			classname: 'hover',
			subselector: 'ul',

			// Callbacks
			init: $.noop,
			beforeOpen: $.noop,
			open: $.noop,
			beforeClose: $.noop,
			close: $.noop,
			start: $.noop,
			end: $.noop
		}, options);

		this.init();
	};

	Menu.prototype = {

		init: function () {
			var that = this;

			this.elements = $('li', this.element).has(this.options.subselector);
			this.elements
				.data('menutimeoutid', null)
				.hover(function (e) {
					that.open($(this));
				}, function (e) {
					that.cancel($(this));
				});

			this.options.init.apply(this, [this.element]);
		},

		open: function (el) {
			var that = this,
				treelist = el.parents('li').andSelf(),
				list;

			window.clearTimeout(el.data('menutimeoutid'));

			if (this.stack.length == 0) {
				this.options.start.apply(this);
			}

			this.options.beforeOpen.apply(this, [el]);

			if ($.inArray(el[0], this.stack) < 0) {
				this.stack.push(el[0]);
			}

			list = $.grep(this.stack, function (x) {
				return $.inArray(x, treelist) < 0;
			});

			$.each(list, function () {
				that.close($(this));
			});

			el.addClass(this.options.classname);
			this.options.open.apply(this, [el]);
		},

		cancel: function (el) {
			var that = this;

			el.data('menutimeoutid', window.setTimeout(function () {
				 that.close(el);
			}, this.options.delay));

			this.options.beforeClose.apply(this, [el]);
		},

		close: function (el) {
			window.clearTimeout(el.data('menutimeoutid'));

			this.stack = $.grep(this.stack, function (x) {
				return x != el[0];
			});

			el.removeClass(this.options.classname);
			this.options.close.apply(this, [el]);

			if (this.stack.length == 0) {
				this.options.end.apply(this);
			}
		}
	};

	$.fn.menu = function (options) {
		return this.each(function() {
			if (!$(this).data('menu')) {
				var menu = new Menu(this, options);
				$(this).data('menu', menu);
			}
		});
	};

})(jQuery);