/*
 *	what about contents being larger than the viewport?
 *	should some sort of scrolling be employed?
 */


// check for content being larger than viewport
// http://jquery.com/demo/thickbox/thickbox-code/thickbox.js

// what about a title, body, buttons?


function AnyBox( content )
{
	var that = this,
		container = document.createElement( 'div' ),
		backdrop = document.createElement( 'div' ),
		positioner = null,
		visualizer = null,
		showHandler = function(){ return true; },
		hideHandler = function(){ return true; };
	
	// remove node from DOM if attached
	if( content.parentNode )
		content.parentNode.removeChild( content );

	// import content
	container.appendChild( content );
	
	// add container to document
	container.className = 'AnyBoxContainer';
	document.body.appendChild( container );
	
	// create backdrop
	document.body.appendChild( backdrop );
	
	// hidden untill told otherwise
	container.style.visibility = 'hidden';
	backdrop.style.visibility = 'hidden';

	function position( t, r, b, l, p )
	{
		container.style.top = t !== null ? (t + 'px') : 'auto';
		container.style.right = r !== null ? (r + 'px') : 'auto';
		container.style.bottom = b !== null ? (b + 'px') : 'auto';
		container.style.left = l !== null ? (l + 'px') : 'auto';
		container.style.position = p ? p : 'fixed';
	}
	function horizontalOffset()
	{
		return ( $(window).width()/2 - container.offsetWidth/2 );
	}
	function verticalOffset()
	{
		return ( $(window).height()/2 - container.offsetHeight/2 );
	}
	function escListener( e )
	{
		if( e.which == 27 )
		{
			that.hide();
		}
	}

	this.type = function( b )
	{
		switch( b )
		{
			case AnyBox.FREE:
				backdrop.className = 'AnyBoxBackdropFree';
				backdrop.style.display = 'none';
			break;
			
			case AnyBox.MODAL:
				backdrop.className = 'AnyBoxBackdropModal';
				backdrop.style.display = '';
			break;
			
			case AnyBox.BACKDROP:
			default:
				backdrop.className = 'AnyBoxBackdrop';
				backdrop.style.display = '';
			break;
		}

		return that;
	}
	this.className = function( cn )
	{
		container.className += ' ' + cn;
		backdrop.className += ' ' + cn;
		return that;
	};
	this.visualizer = function( fn )
	{
		if( typeof( fn ) != 'function' )
			throw new Error( 'the visualizer needs to be a function' );
			
		visualizer = fn;
		return that;
	};
	this.showHandler = function( fn )
	{
		if( typeof( fn ) != 'function' )
			throw new Error( 'the showHandler needs to be a function' );
			
		showHandler = fn;
		return that;
	};
	this.hideHandler = function( fn )
	{
		if( typeof( fn ) != 'function' )
			throw new Error( 'the hideHandler needs to be a function' );
			
		hideHandler = fn;
		return that;
	};
	
	this.box = function()
	{
		return that;
	};

	this.show = function()
	{
		var wasVisible = that.visible();

		if( !positioner )
			that.center();
		
		positioner();

		if( wasVisible )
			return that;
		
		if( !showHandler() )
			return that;

		if( typeof( visualizer ) == 'function' )
		{
			visualizer( "show", container, backdrop );
		}
		else
		{
			container.style.visibility = 'visible';
			backdrop.style.visibility = 'visible';
		}

		jQuery(window).bind( 'resize.anyBoyPositioner', positioner );
		jQuery(document).bind( 'keydown.anyBoyEscapeListener', escListener );
		
		return that;
	};
	this.hide = function()
	{
		if( !hideHandler() )
			return that;

		if( typeof( visualizer ) == 'function' )
		{
			visualizer( "hide", container, backdrop );
		}
		else
		{
			container.style.visibility = 'hidden';
			backdrop.style.visibility = 'hidden';
		}
		
		jQuery( window ).unbind( 'resize', positioner );
		jQuery( document ).unbind( 'keydown', escListener );
		
		return that;
	};
	
	this.visible = function()
	{
		return container.style.visibility != 'hidden';
	};
	this.replace = function( c )
	{
		var t = content.parentNode.replaceChild( c, content );
		content = c;
		positioner();
		
		return t;
	};
	this.remove = function()
	{
		return content.parentNode.removeChild( content );
	};
	this.destruct = function()
	{
		container.parentNode.removeChild( container );
		backdrop.parentNode.removeChild( backdrop );
		
		return that.remove;
	};
	
	
	/*
	 *	Positioning
	 */
	
	this.topRight = function( h, v )
	{
		positioner = function()
		{ 
			position( v || 0, h || 0, null, null ); 
		};
		
		return that;
	};
	this.topCenter = function( v )
	{
		positioner = function()
		{ 
			position( v || 0, null, null, horizontalOffset() );
		};
		
		return that;
	};
	this.topLeft = function( h, v )
	{
		positioner = function()
		{
			position( v || 0, null, null, h || 0 );
		};
		
		return that;
	};
	this.centerLeft = function( h )
	{
		positioner = function()
		{ 
			position( verticalOffset(), null, null, h || 0 ); 
		};
		
		return that;
	};
	this.bottomLeft = function( h, v )
	{
		positioner = function()
		{
			position( null, null, v || 0, h || 0 );
		};
		
		return that;
	};
	this.bottomCenter = function( v )
	{
		positioner = function()
		{
			position( null, null, v || 0, horizontalOffset() );
		};
		
		return that;
	};
	this.bottomRight = function( h, v )
	{
		positioner = function()
		{
			position( null, h || 0, v || 0, null );
		};
		
		return that;
	};
	this.centerRight = function( h )
	{
		positioner = function()
		{
			position( verticalOffset(), h || 0, null, null );
		};
		
		return that;
	};
	this.center = function()
	{
		positioner = function()
		{
			position( verticalOffset(), null, null, horizontalOffset() );
		};
		
		return that;
	};
	this.position = function( x, y )
	{
		positioner = function()
		{
			position( y || 0, null, null, x || 0 );
		};
		
		return that;
	};
	this.bind = function( node, showEvent, hideEvent )
	{
		if( !node )
			throw new ReferenceError( 'AnyBox.bind() expected DomNode and got nothing' );
		
		positioner = function()
		{
			// TODO: determine if space below element is sufficient
			
			var p = jQuery(node).offset(),
			 	d = jQuery(node).outerHeight();

			position( p.top + d, null, null, p.left, 'absolute' );
		};
		
		if( showEvent !== null )
		{
			showEvent = showEvent || 'focus';
			jQuery(node).bind( showEvent, that.show );
		}
		
		if( hideEvent !== null )
		{
			hideEvent = hideEvent || 'blur';
			jQuery(node).bind('blur', function()
			{
				// this is a rather ugly workaround to prevent the OptionsList 
				// from being hidden before the click-event could be fired
				window.setTimeout( that.hide, 200 );
			} );

		}
		return that;
	};


	that.type( AnyBox.FREE );
}

AnyBox.BACKDROP = 0;
AnyBox.MODAL = 1;
AnyBox.FREE	= 2;
AnyBox.fadeVisualizer = function( mode, container, backdrop )
{
	if( mode == 'show' )
	{
		jQuery( container ).hide().css( 'visibility', 'visible' ).fadeIn( 300 );
		jQuery( backdrop ).hide().css( 'visibility', 'visible' ).fadeIn( 300 );
	}
	else if( mode == 'hide' )
	{
		jQuery( container ).fadeOut( 100, function(){ jQuery(container).css( 'visibility', 'hidden' ); } );
		jQuery( backdrop ).fadeOut( 100, function(){ jQuery(backdrop).css( 'visibility', 'hidden' ); } );
	}
};


function ImageBox( source, url )
{
	// TODO: ImageBox - enlarge thumbnails
}

function DialogBox( heading, content, footing )
{
	var that = this,
		container = document.createElement( 'div' ),
		footer = document.createElement( 'div' ),
		close = document.createElement( 'div' ),
		header = document.createElement( 'h4' );
	
	header.appendChild( document.createTextNode( heading ) );
	header.className = 'header';
	close.className = 'close';
	container.appendChild( header );
	container.appendChild( content );
	container.appendChild( close );
	
	if( footing )
	{
		footer.className = ' footer';
		footer.appendChild( footing );
		container.appendChild( footer );
	}
	
	var box = (new AnyBox( container )).className( 'dialog' );
	jQuery( close ).bind( 'click', box.hide );

	this.box = function()
	{
		return box;
	};
}



function OptionsBox( source, handle )
{
	var that = this,
		list = document.createElement( 'ul' ),
		defaultItem = document.createElement( 'li' ),
		selectedItem = null,
		box = (new AnyBox( list )).bind( source ).className( 'options' );
	
	if( source.id )
		box.className( source.id + 'OptionBox' );

	this.showMessage = function( message )
	{
		that.removeItems();
		that.addItem( message, 'message' );
		return that;
	}
	this.addItem = function( item, className )
	{
		var listitem = document.createElement( 'li' );
		
		if( typeof( item ) == 'string' )
		{
			var t = document.createTextNode( item );
			listitem.appendChild( t );
			listitem.isMessage = true;
		}
		else
		{
			listitem.appendChild( item );
			jQuery( listitem ).bind( 'mouseover', function(){ that.select(this); } );
			jQuery( listitem ).bind( 'click', function(){ that.absorbSelected(this); } );
		}
				
		if( className )
			listitem.className += ' ' + className;

		list.appendChild( listitem );
		return that;
	};
	this.removeItems = function()
	{
		while( list.childNodes.length > 0 )
			list.removeChild( list.childNodes[0] );
		
		selectedItem = null;
		
		return that;
	};
	

	this.isSelecting = function()
	{
		return box.visible();
	};
	this.selectUp = function()
	{
		if( !selectedItem )
			return;
			
		if( !selectedItem.previousSibling )
			return that.select( null );

		that.select( selectedItem.previousSibling );
	};
	this.selectDown = function()
	{
		// select first element if nothing was selected before
		if( !selectedItem )
			return that.select( list.firstChild );
		
		if( !selectedItem.nextSibling )
			return;

		that.select( selectedItem.nextSibling );
	};
	this.select = function( item )
	{
		if( item == selectedItem )
			return;
	
		if( item && item.isMessage )
			return;
		
		that.deselect( selectedItem );
		if( item )
			item.className += ' selected';
			
		selectedItem = item;
		
		if( item )
			handle.viewport( item );
	};
	this.deselect = function( item )
	{
		if( !selectedItem )
			return;

		selectedItem.className = selectedItem.className.replace( /\bselected\b/, ' ' );
	};

	this.absorbInput = function()
	{
		handle.input();
	};
	this.absorbSelected = function( item )
	{
		if( item && item.parentNode == list )
			that.select( item );
		
		if( !selectedItem )
			return false;

		handle.selection( selectedItem.firstChild );
				
		box.hide();
		return true;
	};
	
	this.box = function()
	{
		return box;
	};
	
	var keyInterval = null
		keyTimeout = null;
	function keyUpListener( e )
	{
		switch( e.which )
		{
			case 38: // up
			case 40: // down
				// make sure box is visible
				if( keyInterval )
				{
					window.clearInterval( keyInterval );
					keyInterval = null;
				}
				if( keyTimeout )
				{
					window.clearTimeout( keyTimeout );
					keyTimeout = null;
				}
			break;
		}
		
		return false;
	}
	
	function startKeyInterval( e, interval )
	{
		if( keyInterval )
			return;
			
		if( !interval )
		{
			keyTimeout = window.setTimeout( function()
			{  
				keyTimeout = null;
				startKeyInterval( e, true );
			}, 500 );
			return
		}
		
		keyInterval = window.setInterval( function()
		{  
			jQuery( source ).trigger( 'keydown', e );
		}, 100 );
	}
	
	// take control of typing in source	
	function keyDownListener( e )
	{
		switch( e.which )
		{
			case 38: // up
				// make sure box is visible
				box.show();
				that.selectUp();
				e.stopPropagation();
				e.preventDefault();
				startKeyInterval( e );
			break;
			
			case 40: // down
				// make sure box is visible
				box.show();
				that.selectDown();
				e.stopPropagation();
				e.preventDefault();
				startKeyInterval( e );
			break;
			
			case 13: // enter
				if( that.isSelecting() )
				{
					//console.log( 'keyListener absorb!' );
					if( that.absorbSelected() )
					{
						e.stopPropagation();
						e.preventDefault();
					}
					else if( typeof( handle.enterKey ) == 'function' )
					{
						handle.enterKey( e, source );
					}
					else
					{
						e.stopPropagation();
						e.preventDefault();
					}
				}
				else if( typeof( handle.enterKey ) == 'function' )
				{
					handle.enterKey( e, source );
				}
				
				return true;
			break;
			
			case 9:
				// ignore TAB
				return true;
			break;
			
			default:
				//console.log( "key: ", e.which );
				// give the browser some time to add the new character to .value
				window.setTimeout( function()
				{
					that.absorbInput();
				}, 10 );
				return true;
			break;
		}
		
		return false;
	}
	
	// be sure to add to keydown - keypress will *NOT* capture [left/right/up/down]
	jQuery( source ).bind( 'keydown', keyDownListener );
	jQuery( source ).bind( 'keyup', keyUpListener );
	
	// tell the box to fire quering on show
	box.showHandler( function(){ that.absorbInput(); return true; } );
	
	// init
	handle.init( that, source );	
}

function OptionsBoxProvider( target, mandatory )
{
	var that = this;
	
	this.initialized = 'einfach tippen…'; 
	this.loading = 'lade…';
	this.notfound = 'nichts gefunden…';
	this.notselected = 'Es wurde keine Auswahl getroffen';
	
	this.ob = null;
	this.source = null;
	this.target = target;
	this.state = -1;
	this.items = {};
	
	this.maxOptions = 5;
	
	this.init = function( ob, source )
	{
		that.ob = ob;
		that.source = source;
		
		that.source.setAttribute("autocomplete","off");
		that.ob.showMessage( that.initialized );
		that.source.previousValue = jQuery.trim( that.source.value );
		
		// preload data
		that.load();
		
		// register submitHandler
		if( target.form && mandatory )
		{
			jQuery( target.form ).bind( 'submit', function(e){
				if( !target.value )
				{
					e.stopPropagation();
					e.preventDefault();
					alert( that.notselected );
				}
			} );
		}
	
		this.state = OptionsBoxProvider.INITIALIZED;
	};
	this.input = function()
	{
		// spaces at the beginning and the end are unnecessary
		var sourceValue = jQuery.trim( that.source.value );
		// don't fire if nothing has happened
		if( that.source.previousValue && that.source.previousValue == sourceValue )
			return;
		
		that.source.previousValue = sourceValue;

		if( !sourceValue )
		{
			that.clear();
			that.ob.showMessage( that.initialized );
			return;
		}
		
		// show if not visible
		if( !that.ob.box().visible() )
			that.ob.box().show();
		
		// deselect previous
		that.deselection();
		
		// clean current options
		that.ob.removeItems();
		
		
		// try cache first
		var cache = that.cache();
		if( typeof(cache) != 'undefined' )
		{
			that.filter( cache );
			return;
		}
		
		// load some data
		that.ob.showMessage( that.loading );
		that.load( that.filter );
	},

	this.load = function( cb )
	{
		throw new Error( "OptionsBoxProvider.load() is not defined" );
	};
	
	this.cache = function()
	{
		// filter items by current source.value
		return that.items[ that.source.value ];
	};
	
	this.filter = function( items )
	{
			this.status = OptionsBoxProvider.FILTERING;

			//var spacify = /[^a-z0-9]+/g; // what about "äöüß"?
			var spacify = /[,\."'\/\\&_-]+/g;

			// operate on lowerCase alphanumeric+space characters only
			var inputValue = jQuery.trim(that.source.value).toLowerCase(),
				sourceValue = inputValue.replace( spacify, ' ' ),
				sourceTokens = sourceValue.split(' '),
				hiliteTokens = [that.source.value].concat( sourceTokens );

			// count elements shown
			var elements = {
				count: 0,
				max: that.maxOptions,
				token: []

			};

			jQuery.each( items, function(i,record)
			{
				// limit the number of displayed options
				if( elements.count >= elements.max )
					return false; // break;

				// POSITIVE: simple matching, abort if sourceValue not contained
				var rt = record.t.toLowerCase();
				if( rt.indexOf( inputValue ) != -1 )
				{
					that.addElement( record, inputValue );
					elements.count++;
					return true; // continue;
				}
				
				if( rt.indexOf( sourceValue ) != -1 )
				{
					that.addElement( record, sourceValue );
					elements.count++;
					return true; // continue;
				}

				// NEGATIVE: search by token, abort if any token is not contained
				for( var ti=0, token; token = sourceTokens[ti]; ti++ )
					if( record.q.indexOf( token ) == -1 )
						return true; // continue;


				elements.token.push( record );
				//that.addElement( record, hiliteTokens );
				elements.count++;

			} );

			if( that.status == OptionsBoxProvider.FILTERING )
			{
				jQuery.each( elements.token, function(i,record)
				{
					// limit the number of displayed options
					if( elements.count >= elements.max )
						return false; // break;

					that.addElement( record, hiliteTokens );	
				} );
			}

			//return;
			if( that.status == OptionsBoxProvider.FILTERING )
			{
				// tell user nothing was found
				that.ob.showMessage( that.notfound );
			}
			else
			{
				// select the first option
				that.ob.selectDown();
			}

			that.status = OptionsBoxProvider.INITIALIZED;
	};
	this.addElement = function( record, hilite )
	{
		// clean current options
		if( that.status != OptionsBoxProvider.ADDING )
		{
			that.ob.removeItems();
			that.status = OptionsBoxProvider.ADDING;
		}
			
		var className = 'combined',
			c = document.createElement( 'div' ),
			t = document.createElement( 'h4' ),
			s = null,
			img = null;
			
		t.appendChild( document.createTextNode( record.t ) );		
		jQuery( t ).hilite( hilite );
		c.appendChild( t );
		
		if( record.s )
		{
			s = document.createElement( 'p' );
			s.appendChild( document.createTextNode( record.s ) );
			c.appendChild( s );
		}
		
		if( record.img )
		{
			img = document.createElement( 'img' );
			img.src = record.img;
			img.alt = '';
			c.insertBefore( img, t );
			
			className = 'combined icon';
		}

		c.record = record;
		that.ob.addItem( c, className );
	};

	this.deselection = function()
	{
		that.deselect();
	};
	this.selection = function( selected )
	{
		that.select( selected.record );
	};
	
	this.select = function( record )
	{
		// set correct title
		that.source.value = record.t; 
		// set record ID
		that.target.value = record.i;
	};
	
	this.deselect = function()
	{
		that.target.value = '';	
	};
	
	this.clear = function()
	{
		//that.source.value = '';
		//that.target.value = '';
	};

	this.viewport = function( item )
	{
		var $item = jQuery(item),
			itemOffset = $item.position().top,
			itemHeight = $item.outerHeight(),
			scrollPos = $item.parent().scrollTop(),
			viewportHeight = $item.parent().parent().innerHeight();

		// is selected item above viewport?
		if( itemOffset < 0 )
		{
			$item.parent().scrollTop( itemOffset + scrollPos );
		}
		
		// is selected item below viewport?
		else if( itemOffset + itemHeight > viewportHeight )
		{
			$item.parent().scrollTop( scrollPos + itemOffset - viewportHeight + itemHeight );
		}
	}
}

OptionsBoxProvider.SETUP = 0;
OptionsBoxProvider.INITIALIZED = 1;
OptionsBoxProvider.LOADING = 2;
OptionsBoxProvider.FILTERING = 3;
OptionsBoxProvider.ADDING = 4;

function SelectOptionsBoxProvider( target, mandatory, dataSource )
{
	var that = new OptionsBoxProvider( target, mandatory ),
		old = { init: that.init, load: that.load, filter: that.filter },
		//spacify = /[^a-z0-9]+/g; // what about "äöüß"?
		spacify = /[,\."'\/\\&_-]+/g;
	
	that.init = function( ob, source )
	{
		old.init( ob, source );
		
		var ds = dataSource[0];
		if( ds && ds.value )
		{
			jQuery.each( that.cache(), function( i, record )
			{
				if( record.i != ds.value )
					return true; // continue;

				that.select( record );
				return false;
			} );
		}
		
	};
	
	that.load = function( cb )
	{
		//console.log( 'entered load' );
		// this needs to be loaded only once
		if( that.items.selection )
		{
			// execute callback
			if( cb )
				cb( that.items.selection );
				
			return;
		}
			
		that.status = OptionsBoxProvider.LOADING;
		that.items.selection = [];
		
		jQuery('option', dataSource).each( function(i,o)
		{
			if( !o.value || o.value == '0' )
				return true; // continue;
			
			var text = jQuery.trim( jQuery(o).text() )
			that.items.selection.push( {
				i : o.value,
				t : text,
				s : typeof(o.parentNode.label) == 'string' ? jQuery.trim( o.parentNode.label ) : '',
				q : text.toLowerCase().replace( spacify, ' ' )
			} );
			
		});
		
		// execute callback
		if( cb )
			cb( that.items.selection );
	};

	that.cache = function(){ return that.items.selection };
	
	return that;
}
