/**
 * @author      Roland Franssen <franssen.roland@gmail.com>
 * @website     http://roland.devarea.nl/toolset/
 * @copyright   2008 http://roland.devarea.nl/toolset/
 * @license     MIT
 * @version     1.0
 **/

var Toolset = {
	nameSpace:				'toolset',		// string
	mouseMargin:			2,				// integer (pixels)
	defaultOptions: {
		prefix:				'toolset',		// string
		theme:				'default',		// null | string
		position:			'bottom',		// string (top|right|bottom|left)
		offset: {							// object
			x:				0,				// - integer (pixels)
			y:				0				// - integer (pixels)
		},
		dimensions: {						// object
			width:			null,			// - null | integer (pixels)
			widthMax:		null,			// - null | integer (pixels)
			height:			null,			// - null | integer (pixels)
			heightMax:		null			// - null | integer (pixels)
		},
		open: {								// object
			event:			'click',		// - string
			delay:			null,			// - null | integer (seconds)
			toggle:			false			// - boolean
		},
		close: {							// object
			event:			null,			// - null | string
			delay:			null,			// - null | integer (seconds)
			clickOutside:	false,			// - boolean
			mouseOutside:	false			// - boolean
		},
		viewport: {							// object
			margin:			5,				// - integer (pixels)
			horizontal:		true,			// - boolean
			vertical:		true			// - boolean
		},
		mouse: {							// object
			follow:			false,			// - boolean
			attach:			false			// - boolean
		},
		stopDefaultEvent:	true,			// boolean
		effect:				false,			// boolean
		effectOptions: {					// object
			duration:		.3,				// - integer (seconds)
			animation:		'appear'		// - string (appear|slide|blind)
		},
		callback: {							// object
			beforeOpen:		null,			// - null | function
			beforeClose:	null,			// - null | function
			afterOpen:		null,			// - null | function
			afterClose:		null			// - null | function
		}
	},
	Build: function(prefix){
		if(!Object.isString(prefix) || Object.isElement($(prefix))) return;
		$(document.body).insert(
			new Element('div', {id: prefix, style: 'display:none;top:-99999px;left:-99999px'}).insert(
				new Element('div', {id: prefix + '-title', style: 'display:none'})
			).insert(
				new Element('div', {id: prefix + '-content'})
			)
		);
	},
	getBuild: function(prefix){
		return $(prefix, prefix + '-title', prefix + '-content');
	},
	Init: Class.create()
};
Element.addMethods({
	toolset: function(el, content, opt){
		new Toolset.Init(el, content, opt || {});
		return el;
	}
});
Object.extend(Array.prototype, {
	toolset: function(content, opt){
		new Toolset.Init(this, content, opt || {});
		return this;
	}
});
Object.extend(Toolset.Init.prototype, {
	initialize: function(handlers, content, opt){
		this._handlers = this._getHandlers(handlers);
		if(this._handlers.size() < 1) return false;
		this._opt = Object.extend({}, Toolset.defaultOptions);
		this.setOpt(opt);
		if(this._opt.effect) this.setOpt('effect', (typeof Effect != 'undefined'));
		if(this._opt.open.toggle) this.setOpt({
			open:  {event: (this._opt.open.event || 'click'), toggle: true, delay: (this._opt.open.delay || null)},
			mouse: {follow: false, attach: (this._opt.mouse.attach || false)}
		});
		this._content  = content;
		Toolset.Build(this._opt.prefix);
		this._observe();
	},
	setOpt: function(k, v){
		var _set = function(_k, _v){
			if(!Object.isUndefined(this._opt[_k]))
				this._opt[_k] = _v;
		}.bind(this);
		if(Object.isString(k) && !!v) _set(k, v);
		if(typeof k == 'object') for(var x in k) _set(x, k[x]);
		return this;
	},
	setTheme: function(theme){
		var elements = Toolset.getBuild(this._opt.prefix);
		elements.invoke('writeAttribute', 'class', 'theme-' + (theme || this._opt.theme));
		['', 'title', 'content'].each(function(part, i){
			elements[i].addClassName(Toolset.nameSpace + (i > 0 ? '-' + part : ''));
		});
		return this;
	},
	setTitle: function(title){
		var target = $(this._opt.prefix + '-title');
		if(Object.isString(title)) target.update(title).show();
		else target.hide();
		return this;
	},
	setContent: function(content, clear){
		var target = $(this._opt.prefix + '-content');
		if(clear) target.update('');
		if(Object.isArray(content)) content.flatten().each(function(part){
			target.insert(part);
		});
		else target.insert(content);
		return this;
	},
	setDimensions: function(w, h){
		this._setStyle(arguments, ['width', 'height']);
		return this;
	},
	setPosition: function(x, y){
		this._setStyle(arguments, ['left', 'top']);
		return this;
	},
	open: function(){
		this._doOpenClose(true);
		return this;
	},
	close: function(){
		this._doOpenClose(false);
		return this;
	},
	toggle: function(){
		this._doOpenClose(!$(this._opt.prefix).visible());
		return this;
	},
	_getEvent: function(mode){
		if(this._opt.mouse.follow) return [(mode == 'open' ? 'mouseover' : 'mouseout')];
		var str = this._opt[mode];
		if(!str || !Object.isString(str = str.event)) return [];
		if(!str.include('|')) return [str];
		else return str.split('|').uniq();
	},
	_setEvent: function(events, func, handle){
		if(!Object.isArray(events) || events.size() == 0) return;
		func   = func.bindAsEventListener(this);
		handle = (handle || this._handlers);
		events.each(function(event){
			if(Object.isArray(handle)) handle.invoke('observe', event, func);
			else $(handle).observe(event, func);
		});
	},
	_openEvent: function(ev){
		if(!this._content) return;
		var target = $(this._opt.prefix), element = ev.element(), id = element.identify(),
		    exec = function(obj){ return Object.isFunction(obj) ? obj.call(null, ev) : obj; }.bind(this),
			content = exec(this._content), visible = target.visible(), self = (id == target.readAttribute(Toolset.nameSpace));
		this._clearDelay();
		this._clearEffect();
		if(!self){
			this._reset();
			this.setTitle(exec(content.title) || null).setContent(content.content ? exec(content.content) : content, true);
		}
		(function(){
			if(!self){
				target.show();
				this._calcDimensions();
			}
			if(!this._opt.mouse.follow)
				this._calcPosition(
					(this._opt.mouse.attach
						? {top: ev.pointerY(), left: ev.pointerX()}
						: element.cumulativeOffset()),
					element
				);
			if(!visible) target.hide();
			this._eventExec(ev, true);
		}.bind(this)).defer();
	},
	_closeEvent: function(ev){
		this._eventExec(ev, false);
	},
	_closeOutside: function(ev){
		var target = $(this._opt.prefix);
		if(!target.visible() || (this._opt.effect && Effect.Queues.get(this._opt.prefix).size() > 0)) return;
		var x  = ev.pointerX(),
			y  = ev.pointerY(),
			id = ev.element().readAttribute('id'),
			top  = parseInt(target.getStyle('top')),
			left = parseInt(target.getStyle('left')),
			dim  = target.getDimensions(),
			inHandle = false,
			inTarget = false
			offset = {
				X: parseInt(this._opt.offset.x) || 0,
				Y: parseInt(this._opt.offset.y) || 0,
				e: parseInt(Toolset.mouseMargin) || 0
			},
			coords = {
				min: {X: (left - offset.e),  Y: (top - offset.e)},
				max: {X: (left + dim.width + offset.e), Y: (top + dim.height + offset.e)}
			};
		coords[(offset.Y > 0 ? 'min' : 'max')]['Y'] -= offset.Y;
		coords[(offset.X > 0 ? 'min' : 'max')]['X'] -= offset.X;
		if(Object.isString(id) && id == target.readAttribute(Toolset.nameSpace)) inHandle = true;
		if(x >= coords.min.X && y >= coords.min.Y && x <= coords.max.X && y <= coords.max.Y) inTarget = true;
		if(!inTarget && !inHandle){
			if(!this._delay){
				Object.extend(this, {_id: null});
				target.writeAttribute(Toolset.nameSpace, null);
				this._fireCallback('before-close', ev);
				this.close();
			}
		}else this._clearDelay();
	},
	_eventExec: function(ev, mode){
		var target = $(this._opt.prefix), go = true, id = ev.element().identify();
		if(!!this._id){
			if(id != this._id){
				Object.extend(this, {_id: id});
				go = false;
			}else if(this._opt.open.toggle) mode = !target.visible();
		}
		var alias = mode ? 'open' : 'close';
		this._fireCallback('before-' + alias, ev);
		if(go){
			Object.extend(this, {_id: (mode ? id : null)});
			target.writeAttribute(Toolset.nameSpace, this._id);
			(function(){
				this[alias]();
			}.bind(this)).defer();
		}
	},
	_observe: function(){
		['open', 'close'].each(function(mode){
			this._setEvent(this._getEvent(mode), function(ev){
				if(this._opt.stopDefaultEvent) ev.stop();
				this['_' + mode + 'Event'](ev);
			});
		}.bind(this));
		if(this._opt.mouse.follow)
			this._setEvent(['mousemove'], function(ev){
				this._calcPosition({
					top:  ev.pointerY(),
					left: ev.pointerX()
				});
			});
		else if(this._opt.close.mouseOutside) this._setEvent(['mouseover', 'mousemove'], this._closeOutside, document);
		else if(this._opt.close.clickOutside) this._setEvent(['click'], this._closeOutside, document);
	},
	_setStyle: function(arg, alias){
		arg = $A(arg);
		var target = $(this._opt.prefix);
		if(alias.indexOf(arg[0]) > 0)
			target.setStyle(arg[0] + ':' + (Object.isNumber(arg[1]) ? arg[1] + 'px' : 'auto'));
		else alias.each(function(style, i){
			target.setStyle(style + ':' + (Object.isNumber(arg[i]) ? arg[i] + 'px' : 'auto'));
		});
	},
	_doOpenClose: function(mode){
		var effect  = (this._opt.effect ? this._opt.effectOptions : false),
		    target  = $(this._opt.prefix),
		    visible = target.visible();
		this._clearEffect();
		if((mode && visible) || (!mode && !visible)) return;
		this._clearDelay();
		var func = (mode ? 'show' : 'hide'),
		    anim = (effect ? effect.animation : null), doEffect = false;
		if(Object.isString(anim)){
			doEffect = true;
			switch(anim){
				case 'slide':
				case 'blind':
					func = anim + (mode ? 'Down' : 'Up');
					break;
				case 'appear':
					func = mode ? 'appear' : 'fade';
					break;
				default:
					doEffect = false;
					break;
			}
		}
		var alias = (mode ? 'open' : 'close'),
		    dTime = (parseInt(this._opt[alias].delay).abs() || 0),
		    dFunc = function(){
				var _handler  = Element[func],
				    _callback = function(){
						this._fireCallback('after-' + alias, target);
						Object.extend(this, {_delay: null});
					}.bind(this);
				if(!doEffect){
					_handler(target);
					_callback();
				}else _handler(target, {
					duration: (effect.duration || .3),
					afterFinish: _callback,
					queue: {
						position: 'front',
						scope: this._opt.prefix
					}
				});
			}.bind(this);
		if(Object.isNumber(dTime) && dTime > 0) Object.extend(this, {_delay: dFunc.delay(dTime)});
		else dFunc();
	},
	_calcDimensions: function(){
		var target  = $(this._opt.prefix),
		    dim     = this._opt.dimensions,
		    setDim  = {};
		['width', 'height'].each(function(mode){
			var dimMax = dim[mode + 'Max'], dimValue = null, dimReal = target[('get-' + mode).camelize()]();
			if(Object.isNumber(dim[mode])) dimValue = dim[mode];
			else if(Object.isNumber(dimMax) && dimReal > dimMax) dimValue = dimMax;
			setDim[mode] = dimValue;
		}.bind(this));
		this.setDimensions(setDim.width, setDim.height);
		this._dim = target.getDimensions();
	},
	_calcPosition: function(pos, element){
		if(!pos.top || !pos.left) return this;
		if(!(this._opt.mouse.follow || this._opt.mouse.attach) && Object.isElement(element)) var elm = element;
		pos.top  += (this._opt.offset.y || 0);
		pos.left += (this._opt.offset.x || 0);
		var target = $(this._opt.prefix);
		switch(this._opt.position){
			case 'top':
				pos.top -= target.getHeight();
				break;
			case 'right':
				if(elm && !this._opt.mouse.attach) pos.left += elm.getWidth();
				break;
			case 'left':
				pos.left -= target.getWidth();
				break;
			case 'bottom':
			default:
				if(elm && !this._opt.mouse.attach) pos.top += elm.getHeight();
				break;
		}
		this._pos = pos;
		this.setPosition(pos.left, pos.top)._rePosition(elm);
	},
	_rePosition: function(elm){
		if(!this._pos || !this._dim) return;
		var target = $(this._opt.prefix);
		var view, opt = this._opt.viewport, viewObj = document.viewport;
		if(opt.horizontal && opt.vertical) view = viewObj.getDimensions();
		else if(opt.horizontal) view = {width: viewObj.getWidth()};
		else if(opt.vertical) view = {height: viewObj.getHeight()};
		else return;
		var margin = (opt.margin.abs() || 0),
		    scroll = viewObj.getScrollOffsets(),
			elmDim = (Object.isElement(elm) ? elm.getDimensions() : {width: 0, height: 0}),
		    within = {
				width:  {min: true, max: true},
				height: {min: true, max: true}
			},
			offset = {
				top:  (parseInt(this._opt.offset.y) * 2) || 0,
				left: (parseInt(this._opt.offset.x) * 2) || 0
			};
		['width', 'height'].each(function(mode){
			if(!view[mode]) return;
			var alias = (mode == 'width' ? 'left' : 'top'), basePos = (this._pos[alias] - scroll[alias]);
			within[mode]['min'] = basePos >= margin;
			within[mode]['max'] = (basePos + this._dim[mode]) <= (view[mode] - margin);
			['min', 'max'].each(function(type){
				if(within[mode][type]) return;
				var value = this._pos[alias];
				switch(this._opt.position){
					case 'top':
						if(mode == 'height' && type == 'min') value += (this._dim[mode] - elmDim[mode]);
						if(mode == 'width' && type == 'max')  value -= (this._dim[mode] - elmDim[mode]);
						break;
					case 'right':
						if(mode == 'height' && type == 'max') value -= (this._dim[mode] - elmDim[mode]);
						if(mode == 'width' && type == 'max')  value -= (this._dim[mode] + elmDim[mode]);
						break;
					case 'left':
						if(mode == 'height' && type == 'max') value -= (this._dim[mode] - elmDim[mode]);
						if(mode == 'width' && type == 'min')  value += (this._dim[mode] + elmDim[mode]);
						break;
					case 'bottom':
						if(mode == 'height' && type == 'max') value -= (this._dim[mode] + elmDim[mode]);
						if(mode == 'width' && type == 'max')  value -= (this._dim[mode] - elmDim[mode]);
					default:
						break;
				}
				if(value != this._pos[alias]) this.setPosition(alias, (value - offset[alias]));
			}.bind(this));
		}.bind(this));
	},
	_fireCallback: function(){
		var arg = $A(arguments), event = arg.shift(), callback = this._opt.callback[event.camelize()];
		if(Object.isFunction(callback))
			callback.apply(null, arg);
	},
	_reset: function(pos){
		var elements = Toolset.getBuild(this._opt.prefix),
		    reset    = {position: null, bottom: null, top: null, left: null, right: null, width: null, height: null};
		this.setTheme();
		[elements[1], elements[2]].invoke('writeAttribute', 'style', reset);
	},
	_clearDelay: function(){
		if(this._delay){
			window.clearTimeout(this._delay);
			Object.extend(this, {_delay: null});
		}
	},
	_clearEffect: function(){
		if(this._opt.effect){
			Effect.Queues.get(this._opt.prefix).each(function(effect){
				effect.cancel();
			});
			this._reset();
			this._calcDimensions();
		}
	},
	_getHandlers: function(handlers){
		if(Object.isFunction(handlers)) handlers = handlers.call(null);
		var result = [];
		if(Object.isString(handlers) || Object.isElement(handlers)) result.push(handlers);
		else if(Object.isArray(handlers)) result = handlers.flatten().uniq().each(Element.extend);
		return result.reject(function(elm){
			return !Object.isElement(elm);
		});
	}
});
