//  Prototip 1.1.0_pre1
//  by Nick Stakenburg - http://www.nickstakenburg.com
//  29-10-2007
//
//  More information on this project:
//  http://www.nickstakenburg.com/projects/prototip/
//
//  Licensed under the Creative Commons Attribution 3.0 License
//  http://creativecommons.org/licenses/by/3.0/
//

var Prototip = {
	Version: '1.1.0_pre1',

	REQUIRED_Prototype: '1.6.0',
	REQUIRED_Scriptaculous: '1.7.1',

	start: function() { this.require('Prototype'); },

	require: function(library) {
		if ((typeof window[library] == 'undefined') ||
		(this.convertVersionString(window[library].Version) < this.convertVersionString(this['REQUIRED_' + library])))
		throw('Prototip requires ' + library + ' >= ' + this['REQUIRED_' + library]);
	},

	// based on Scriptaculous' implementation
	convertVersionString: function(versionString) {
		var r = versionString.split('.');
		return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
	},

	// fixed viewport.getDimensions. Also excludes scrollbars in firefox. Valid doctype required.
	viewport : {
		getDimensions: function() {
			var dimensions = { };
			var B = Prototype.Browser;
			$w('width height').each(function(d) {
				var D = d.capitalize();
				if (B.Opera) dimensions[d] = document.body['client' + D];
				else if (B.WebKit) dimensions[d] = self['inner' + D];
				else dimensions[d] = document.documentElement['client' + D];
			});
			return dimensions;
		}
	}
};
Prototip.start();

var Tips = {
	// Configuration
	closeButtons: false,
	zIndex: 1200,

	fixIE: (function(agent){
		var version = new RegExp('MSIE ([\\d.]+)').exec(agent);
		return version ? (parseFloat(version[1]) <= 6) : false;
	})(navigator.userAgent),

	tips : [],
	visible : [],

	add: function(tip) {
		this.tips.push(tip);
	},

	remove: function(element) {
		var tip = this.tips.find(function(t){ return t.element == $(element); });
		if (tip) {
			tip.deactivate();
			if (tip.tooltip) {
				tip.wrapper.remove();
				if (Tips.fixIE) tip.iframeShim.remove();
			}
			this.tips = this.tips.without(tip);
		}
	},

	zIndexRestore : 1200,
	raise: function(tip) {
		var highestZ = this.zIndexHighest();
		if (!highestZ) {
			tip.style.zIndex = this.zIndexRestore;
			return;
		}
		var newZ = (tip.style.zIndex != highestZ) ? highestZ + 1 : highestZ;
		this.tips.pluck('wrapper').invoke('removeClassName', 'highest');

		tip.setStyle({ zIndex : newZ }).addClassName('highest');
	},

	zIndexHighest: function() {
		var highestZ = this.visible.max(function(v) {
			return parseInt(v.style.zIndex);
		});
		return highestZ;
	},
	
	addVisibile: function(tip) {
		this.removeVisible(tip);
		this.visible.push(tip);
	},

	removeVisible: function(tip) {
		this.visible = this.visible.without(tip);
	}
};

var Tip = Class.create({
	initialize: function(element,content,options) {
		this.element = $(element);
		Tips.remove(this.element);

		this.content = content;

		var isHooking = (arguments[2] && arguments[2].hook);
		var isShowOnClick = (arguments[2] && arguments[2].showOn == 'click');

		this.options = Object.extend({
			className: 'default',                 // see css, this will lead to .prototip .default
			closeButton: Tips.closeButtons,       // true, false
			delay: !isShowOnClick ? 0.2 : false,  // seconds before tooltip appears
			duration: 0.3,                        // duration of the effect
			effect: false,                        // false, 'appear' or 'blind'
			hideOn: 'mouseout',
			hook: false,                          // { element: topLeft|topRight|bottomLeft|bottomRight, tip: see element }
			offset: isHooking ? {x:0, y:0} : {x:16, y:16},
			fixed: isHooking ? true : false,      // follow the mouse if false
			showOn: 'mousemove',
			target: this.element,                 // or another element
			title: false,
			viewport: isHooking ? false : true    // keep within viewport if mouse is followed
		}, arguments[2] || {});

		this.target = $(this.options.target);
				
		this.setup();

		if (this.options.effect) {
			Prototip.require('Scriptaculous');
			this.queue = { position: 'end', limit: 1, scope: this.wrapper.identify() }
		}
		
		Tips.add(this);
		this.activate();
	},

	setup: function() {
		// Everything that needs to be build for observing is done here
		this.wrapper = new Element('div', { 'class' : 'prototip' }).setStyle({
			display: 'none', zIndex: Tips.zIndex++ });
			this.wrapper.identify();

			if (Tips.fixIE) {
				this.iframeShim = new Element('iframe', { 'class' : 'iframeShim', src: 'javascript:false;' }).setStyle({
					display: 'none', zIndex: Tips.zIndexRestore - 1 });
			}

			this.tip = new Element('div', { 'class' : 'content' }).update(this.content);
			this.tip.insert(new Element('div').setStyle({ clear: 'both' }));

			if (this.options.closeButton || (this.options.hideOn.element && this.options.hideOn.element == 'closeButton'))
			this.closeButton = new Element('a', { href: 'javascript:;', 'class' : 'close' });
	},

	build: function() {
		if (Tips.fixIE) document.body.appendChild(this.iframeShim).setOpacity(0);

		// effects go smooth with extra wrapper
		var wrapper = 'wrapper';
		if (this.options.effect) {
			this.effectWrapper = this.wrapper.appendChild(new Element('div', { 'class' : 'effectWrapper' }));
			wrapper = 'effectWrapper';
		}

		this.tooltip = this[wrapper].appendChild(new Element('div', { 'class' : 'tooltip ' + this.options.className }));

		if (this.options.title || this.options.closeButton) {
			this.toolbar = this.tooltip.appendChild(new Element('div', { 'class' : 'toolbar' }));
			this.title = this.toolbar.appendChild(new Element('div', { 'class' : 'title' }).update(this.options.title || ' '));
		}

		this.tooltip.insert(this.tip);
		document.body.appendChild(this.wrapper);

		// fixate elements for better positioning and effects
		var fixate = (this.options.effect) ? [this.wrapper, this.effectWrapper]: [this.wrapper];
		if (Tips.fixIE) fixate.push(this.iframeShim);

		// fix width
		var fixedWidth = this.wrapper.getWidth();
		fixate.invoke('setStyle', { width: fixedWidth + 'px' });

		// make toolbar width fixed
		if(this.toolbar) {
			this.wrapper.setStyle({ visibility : 'hidden' }).show();
			this.toolbar.setStyle({ width: this.toolbar.getWidth() + 'px'});
			this.wrapper.hide().setStyle({ visibility : 'visible' });
		}

		// add close button
		if (this.closeButton)
		this.title.insert({ top: this.closeButton }).insert(new Element('div').setStyle({ clear: 'both' }));

		var fixedHeight = this.wrapper.getHeight();
		fixate.invoke('setStyle', { width: fixedWidth + 'px', height: fixedHeight + 'px' });

		this[this.options.effect ? wrapper : 'tooltip'].hide();
	},

	activate: function() {
		this.eventShow = this.showDelayed.bindAsEventListener(this);
		this.eventHide = this.hide.bindAsEventListener(this);

		// if fixed use mouseover instead of mousemove for less event calls
		if (this.options.fixed && this.options.showOn == 'mousemove') this.options.showOn = 'mouseover';

		if(this.options.showOn == this.options.hideOn) {
			this.eventToggle = this.toggle.bindAsEventListener(this);
			this.element.observe(this.options.showOn, this.eventToggle);
		}

		this.hideElement = Object.isUndefined(this.options.hideOn.element) ? 'element' : this.options.hideOn.element;
		var hideOptions = {
		'element': this.eventToggle ? [] : [this.element],
		'target': this.eventToggle ? [] : [this.target],
		'tip': this.eventToggle ? [] : [this.wrapper],
		'closeButton': [],
		'.close' : this.tip.select('.close')
		}
		this.hideTargets = hideOptions[this.hideElement];

		// add show and hide observers
		if (this.element && !this.eventToggle) this.element.observe(this.options.showOn, this.eventShow);
		this.hideAction = (this.options.hideOn.event || this.options.hideOn);
		if (this.hideTargets) this.hideTargets.invoke('observe', this.hideAction, this.eventHide);

		// add position observer if not fixed
		if (!this.options.fixed && this.options.showOn == 'click') {
			this.eventPosition = this.position.bindAsEventListener(this);
			this.element.observe('mousemove', this.eventPosition);
		}

		// add hide observers to close button and non click elements when they are not the close (delay needs this)
		if (this.closeButton) this.closeButton.observe('click', this.eventHide);
		if (this.options.showOn != 'click' && this.hideElement != 'element') {
			this.eventCheckDelay = this.checkDelay.bindAsEventListener(this);
			this.element.observe('mouseout', this.eventCheckDelay);
		}

		// observe wrapper to raise zIndex
		this.wrapper.observe('mouseover', function(){ Tips.raise(this.wrapper); }.bind(this));
	},

	deactivate: function() {
		if(this.options.showOn == this.options.hideOn)
		this.element.stopObserving(this.options.showOn, this.eventToggle);
		else {
			this.element.stopObserving(this.options.showOn, this.eventShow);
			this.hideTargets.invoke('stopObserving', this.hideAction, this.eventHide);
		}

		if (this.eventPosition) this.element.stopObserving('mousemove', this.eventPosition);
		if (this.closeButton) this.closeButton.stopObserving();
		if (this.eventCheckDelay) this.element.stopObserving('mouseout', this.eventCheckDelay);
		this.wrapper.stopObserving();
	},

	showDelayed: function(event){
		if (!this.tooltip) this.build();
		this.position(event); // follow mouse
		if (this.wrapper.visible()) return;

		this.checkDelay();
		this.timer = this.show.bind(this).delay(this.options.delay);
	},

	checkDelay: function(){
		if (this.timer) {
			clearTimeout(this.timer);
			this.timer = null;
		}
	},
		
	show: function(){
		if (this.wrapper.visible() && this.options.effect != 'appear') return;

		if (Tips.fixIE) this.iframeShim.show();
		Tips.addVisibile(this.wrapper);
		this.wrapper.show();

		if (!this.options.effect) this.tooltip.show();
		else {
			if (this.activeEffect) Effect.Queues.get(this.queue.scope).remove(this.activeEffect);
			this.activeEffect = Effect[Effect.PAIRS[this.options.effect][0]](this.effectWrapper,
			{ duration: this.options.duration, queue: this.queue});
		}
	},

	hide: function(e){
		this.checkDelay();
		if(!this.wrapper.visible()) return;

		if (!this.options.effect) {
			if (Tips.fixIE) this.iframeShim.hide();
			this.tooltip.hide();
			this.wrapper.hide();
			Tips.removeVisible(this.wrapper);
		}
		else {
			if (this.activeEffect) Effect.Queues.get(this.queue.scope).remove(this.activeEffect);
			this.activeEffect = Effect[Effect.PAIRS[this.options.effect][1]](this.effectWrapper,
			{ duration: this.options.duration, queue: this.queue, afterFinish: function(){
				if (Tips.fixIE) this.iframeShim.hide();
				this.wrapper.hide();
				Tips.removeVisible(this.wrapper);
			}.bind(this)});
		}
	},

	toggle: function(event){
		if (this.wrapper && this.wrapper.visible()) this.hide(event);
		else this.showDelayed(event);
	},

	position: function(event){
		if (!this.wrapper.hasClassName('highest')) Tips.raise(this.wrapper);

		var offset = {left: this.options.offset.x, top: this.options.offset.y};
		var targetPosition = Position.cumulativeOffset(this.target);
		var tipd = this.wrapper.getDimensions();
		var pos = { left: (this.options.fixed) ? targetPosition[0] : Event.pointerX(event),
		top: (this.options.fixed) ? targetPosition[1] : Event.pointerY(event) };

		// add offsets
		pos.left += offset.left;
		pos.top += offset.top;

		if (this.options.hook) {
			var dims = {target: this.target.getDimensions(), tip: tipd}
			var hooks = {target: Position.cumulativeOffset(this.target), tip: Position.cumulativeOffset(this.target)}

			for(var z in hooks) {
				switch(this.options.hook[z]){
					case 'topRight':
					hooks[z][0] += dims[z].width;
					break;
					case 'bottomLeft':
					hooks[z][1] += dims[z].height;
					break;
					case 'bottomRight':
					hooks[z][0] += dims[z].width;
					hooks[z][1] += dims[z].height;
					break;
				}
			}

			// move based on hooks
			pos.left += -1*(hooks.tip[0] - hooks.target[0]);
			pos.top += -1*(hooks.tip[1] - hooks.target[1]);
		}

		// move tooltip when there is a different target
		if (!this.options.fixed && this.element !== this.target) {
			var elementPosition = Position.cumulativeOffset(this.element);
			pos.left += -1*(elementPosition[0] - targetPosition[0]);
			pos.top += -1*(elementPosition[1] - targetPosition[1]);
		}

		if (!this.options.fixed && this.options.viewport) {
			var scroll = document.viewport.getScrollOffsets();
			var viewport = Prototip.viewport.getDimensions();
			var pair = {left: 'width', top: 'height'};

			for(var z in pair) {
				if ((pos[z] + tipd[pair[z]] - scroll[z]) > viewport[pair[z]])
				pos[z] = pos[z] - tipd[pair[z]] - 2*offset[z];
			}
		}

		var finalPosition = { left: pos.left + 'px', top: pos.top + 'px' };
		this.wrapper.setStyle(finalPosition);
		if (Tips.fixIE) this.iframeShim.setStyle(finalPosition);
	}
});