/**
 * @overview <p><strong>BBButtons.js</strong> is a generic system for handling user interaction
 * (most likely triggered by buttons). </p>
 * <p>The Class BBButtons does not implement any behaviour
 * of any BBCode-Button itself, it merely coordinates usage, enables using
 * selections / ranges in an input-element and adds a layer of abstraction to
 * enable asynchronous handling of code-insertion.</p>
 * <p>The Class BBButton is a factory-object to create BBCode-Buttons. The abstraction
 * is necessary, since a Button can be anything in any combination of attributes.</p>
 * <p>To give users an easy entry this package comes with a few default BBCode-handlers
 * for [code], [image], [link], [tag] insertion, for example. The default handlers do not
 * postulate a certain BBCode syntax, but let the users specify their own syntax (in StringTemplate-format)</p>
 * <p>compared to similar solutions, BBButtions.js is able to adopt to the power of AJAX. Asynchronous
 * handlers could query a database, present the user with a resultset and insert respective codes based on
 * decision by the user. For more info read the comments on <em>BBButton</em> below.</p>
 *
 * @author <a href="http://rodneyrehm.de" target="_blank">Rodney Rehm</a>
 * @see <a href="http://rodneyrehm.de/tools" target="_blank">rodneyrehm.de/tools</a>
 *
 * @version 23. May 2007
 *
 * @license Published under the
 * <a href="http://www.opensource.org/licenses/bsd-license.php" target="_blank">Open Source BSD-License</a>
 **/

/**
 * Enhance an input-field by Buttons for easy BBCode insertion.
 * This is merely the generic framework to enable all sorts of BBCodes and
 * interactions, but does not implement them.
 * @class BBButtons
 * @param {String} InputID The ID of the input-element or the element itself (most likely a textarea) the instance should work with
 * @param {String} [listClassName] The CSS-Class to assign to the list of buttons (<ul>)
 * @param {Number} [maxLength] The maximum number of characters the input-element may accept
 **/
function BBButtons( InputID, listClassName, maxLength )
{
	/**
	 * Self-reference for use in closures
	 * @field self
	 * @private
	 **/
	var that = this;

	/**
	 * Reference to the input element
	 * @field input
	 * @private
	 **/
	var input;

	/**
	 * Reference to the list of buttons element
	 * @field buttonlist
	 * @private
	 **/
	var buttonlist;

	/**
	 * Reference to the characterCounter element
	 * @field charCounter
	 * @private
	 **/
	var charCounter = null;

	/**
	 * Start of selection (gecko)
	 * @field cStart
	 * @private
	 **/
	var cStart = null;

	/**
	 * End of selection (gecko)
	 * @field cEnd
	 * @private
	 **/
	var cEnd = null;

	/**
	 * Range of selection (IE)
	 * @field cRange
	 * @private
	 **/
	var cRange = null;

	/**
	 * Contents of selection
	 * @field cSelectedText
	 * @private
	 **/
	var cSelectedText = null;

	/**
	 * Reference to the active BBButton element
	 * @field cButton
	 * @private
	 **/
	var cButton = null;

	/**
	 * Cancel-Callback for use with "asynchronous" handlers
	 * @field cCancel
	 * @private
	 **/
	var cCancel = null;	// remember range even on blur


	/**
	 * Frame the BBButton's handler to gain access to the selection.
	 * @method processTag
	 * @param {Element} button The BBButton to work on
	 **/
	this.processTag = function( button )
	{
		// make sure cache is empty
		this.resetSelectionCache();

		cButton = button;

		// get selected text and stop execution if singleLineSelection has been compromised
		cSelectedText = this.getSelectedText( button.bbSingleLineSelection );
		if( cSelectedText === null )
			return;

		// work the tag
		var text = button.bbGenerate( cSelectedText, that );

		// restore to previous state on error in handler
		if( text === null )
		{
			this.resetSelectionCache();
			return;
		}

		// replace selected text (or insert at caret) if there is no bbCancel function
		// if bbCancel is set asynchronous behaviour is expected
		if( !button.bbCancel )
			this.insertText( text );
		else
			cCancel = button.bbCancel;
	};


	/**
	 * Get the contents of the selection in input-field
	 * @method getSelectedText
	 * @param {boolean} singleLine If true the selection has to be a single line
	 *		(defaults to false, meaning multiline selection)
	 * @returns {String} the selection's contents
	 **/
	this.getSelectedText = function( singleLine )
	{
		// selection needs to be inside input-element
		input.focus();

		/* INTERNET EXPLORER */
		if( typeof document.selection != 'undefined' )
		{
			cRange = document.selection.createRange();
			return this.preventNewline( cRange.text, singleLine );
		}
		/* GECKO ENGINES */
		else if( typeof input.selectionStart != 'undefined' )
		{
			cStart = input.selectionStart;
			cEnd = input.selectionEnd;
			return this.preventNewline( input.value.substring(cStart, cEnd), singleLine );
		}

		return "";
	};


	/**
	 * Insert or replace selection by new content
	 * @method insertText
	 * @param {String} text Text to insert at current caret-position
	 * @param {Number} [textLength] Offset from current caret-position to jump to after insertion
	 *		(reqired for external overwriting of caret-position only!)
	 **/
	this.insertText = function( text, textLength )
	{
		// selection needs to be inside input-element
		input.focus();

		// cannot insert more than allowed by maxLength
		if( maxLength  )
		{
			var sctl = cSelectedText ? cSelectedText.length : 0;

			if( maxLength < text.length + input.value.length - sctl )
			{
				this.resetSelectionCache();
				this.handleChanges();
				alert('Code konnte nicht eingefügt werden, da dadurch das Limit von '+ maxLength +' Zeichen überschritten werden würde');
				return;
			}
		}

		/* overcome selection-settings */
		if( typeof textLength != 'undefined' )
			var stLength = textLength;
		/* use selection length */
		else if( cSelectedText )
			var stLength = cSelectedText.length;
		/* no selection made */
		else
			var stLength = 0;

		var tagLength = cButton.bbTagName.length;

		/* INTERNET EXPLORER */
		if( typeof document.selection != 'undefined' )
		{
			// insert text
			cRange.text = text;

			// reset caret position
			range = document.selection.createRange();
			if( stLength == 0 )
				range.move('character', -1);
			else
				range.moveStart('character', stLength);

			range.select();
		}
		/* GECKO ENGINES */
		else if( typeof input.selectionStart != 'undefined' )
		{
			// insert text
			input.value = input.value.substr(0, cStart) + text  + input.value.substr(cEnd);

			// reset caret position
			var pos;
			if( stLength == 0 )
				pos = cStart + tagLength + 2;	// move inside inserted bbcode
			else
				pos = cStart + text.length;

			input.selectionStart = pos;
			input.selectionEnd = pos;
		}
		/* OTHER ENGINES */
		else
		{
			// insert text
			input.value += text;
		}

		// bbcode insert operation is complete, free caches
		this.resetSelectionCache();
		this.handleChanges();
	};


	/**
	 * Reset cache variables and call asynchronous-cancelHandler if necessary
	 * @method resetSelectionCache
	 **/
	this.resetSelectionCache = function()
	{
		cStart = null;
		cEnd = null;
		cRange = null;
		cSelectedText = null;
		cButton = null;

		if( cCancel )
			cCancel();

		cCancel = null;
	};


	/**
	 * Alert User on linebreaks in string
	 * @method preventNewline
	 * @param {String} string text to check for newlines
	 * @param {boolean} singleLine If true string needs to be free of newlines (defaults to false)
	 * @returns {String} the string or null on error
	 **/
	this.preventNewline = function( string, singleLine )
	{
		if( singleLine )
		{
			var nl = new RegExp('(\n|\r|\r\n).');
			if( nl.test( string ) )
			{
				alert('Der markierte Text darf keine Zeilenumbrüche enthalten');
				return null;
			}
		}

		return string;
	};


	/**
	 * Add a button to the button list. Set BBButtons specific onclickHandler.
	 * @method addButton
	 * @param {BBButton} button BBButton-Instance (or any Object implementing a toElement()-Method) to add to button listing
	 **/
	this.addButton = function( button )
	{
		b = button.toElement();

		// non-propagating onclickHandler calling BBButtons.processTag()
		b.onclick = function(e)
		{
			// internet explorer
			if( !e )
				e = window.event;

			// actually do something
			try
			{
				that.processTag( this );
			}
			catch( ex )
			{
// 				console.log(ex);
				return false;
			}
			
			// stop bubbling
			e.cancelBubble = true;
			if( e.stopPropagation )
				e.stopPropagation();
			
			// abort default action
			return false;
		};
		b.keyup = b.onclick; // make compatible with keyboard

		// wrap the button in a list-item and add item to button listing
		var li = document.createElement('li');
		li.appendChild( b );
		buttonlist.appendChild(li);
	};


	/**
	 * ChangeHandler to register with anything which could possibly change the
	 * contents of input-element. Important to asynchronous callbacks for cancelling purposes.
	 * Update character counter, if one is set.
	 * @method handleChanges
	 **/
	this.handleChanges = function()
	{
		if( cCancel )
		{
			cCancel();
			cCancel = null;
		}

		if( charCounter )
		{
			charCounter.updateCount( input, maxLength );
		}
	};


	/**
	 * Set the maximum allowed number of characters
	 * @method setMaxLength
	 * @param {Number} ml The Maximum length of input element
	 **/
	this.setMaxLength = function( ml )
	{
		maxLength = ml;
	}


	/**
	 * Set the character counter element
	 * @method setCharCounter
	 * @param {Element} elem The element to assign as the character counter,
	 * 		the method updateCount( {Element} input, {Number} maxLength )
	 * 		must be implemented.
	 **/
	this.setCharCounter = function( elem )
	{
		charCounter = elem;
	}


	/**
	 * Initialize BBButtons instance. Set up button list and register
	 * eventHandlers with input-element. Aborts if InputID does not
	 * reference a valid element
	 * @method init
	 **/
	this.init = function()
	{
		input = typeof( InputID ) == 'string' ? document.getElementById( InputID ) : InputID;
		if( !input )
			return;

		// insert buttons before textfield
		buttonlist = document.createElement('ul');
		buttonlist.className = listClassName;
		input.parentNode.insertBefore( buttonlist , input );

		// enable asynchronous behaviour, cancel an open asnyc-callback
		var oldKeyUp = input.onkeyup;
		if( oldKeyUp )
		{
			input.onkeyup = function(){
				that.handleChanges(this);
				oldKeyUp();
			};
		}
		else
		{
			input.onkeyup = that.handleChanges;
		}
		input.onchange = input.onkeyup;

	};

	this.init();
	
	this.getButtonlist = function()
	{
		return buttonlist;
	}
}



/**
 * Factory Object to create a BBButton for interaktion with BBButtons.
 * Asynchronous behaviour can be activated by setting a cancelHandler.
 * @class BBButton
 * @param {String} [title] The title of the button
 * @param {String} [tagName] The BBCode-TagName of the button
 * @param {Function} [generateHandler] The callback-function to construct the code
 * @param {boolean} [singleLineSelection] Selections cannot be multiline if set to true (defaults to false)
 **/
function BBButton( title, tagName, generateHandler, singleLineSelection )
{
	/**
	 * The BBButton's element
	 * @field button
	 * @private
	 **/
	var button = null;

	/**
	 * The BBButton's attributes
	 * @field attributes
	 * @private
	 **/
	var attributes = {};

	/**
	 * The BBButton's code-formats
	 * @field formats
	 * @private
	 **/
	var formats = null;

	/**
	 * The cancelHandler called by BBButtons to abort asynchronous handling
	 * @field cancelHandler
	 * @private
	 **/
	var cancelHandler = null;

	/**
	 * The element's CSS class
	 * @field className
	 * @private
	 **/
	var className = 'bbbutton';


	/**
	 * Set an attribute to the BBCode-Button
	 * @method setAttribute
	 * @param {String} name The name of the attribute
	 * @param {mixed} value The value to set this attribute to (can be anything!)
	 **/
	this.setAttribute = function( name, value )
	{
		attributes[name] = value;

		// update element if already created
		// this actually is not necessary since the object is passed by reference
		if( button )
			button.bbAttributes = attributes;
	};


	/**
	 * Set the format to the BBCode-Button
	 * @method setFormat
	 * @param {Array} fs An array of Objects of the Form
	 *		{ 'format':"%(varname) - helloworld", 'requires':['varname'] }
	 **/
	this.setFormat = function( fs )
	{
		formats = fs;

		// update element if already created
		if( button )
			button.bbFormat = fs;
	};


	/**
	 * Set the title of the button
	 * @method setTitle
	 * @param {String} s The title of the Button
	 **/
	this.setTitle = function( s )
	{
		title = s;

		// update element if already created
		if( button )
			button.title = s;
	};


	/**
	 * Set the BBCode-TagName of the button
	 * @method setTagName
	 * @param {String} s The BBCode-TagName of the Button
	 **/
	this.setTagName = function( s )
	{
		tagName = s;

		// update element if already created
		if( button )
			button.bbTagName = s;
	};


	/**
	 * Set the callback-function to construct the code of the button
	 * @method setGenerateHandler
	 * @param {Function} s The callback-function to construct the code
	 **/
	this.setGenerateHandler = function( s )
	{
		generateHandler = s;

		// update element if already created
		if( button )
			button.bbGenerate = s;
	};


	/**
	 * Set the callback-function to cancel the construction of the code
	 * @method setCancelHandler
	 * @param {Function} s The callback-function to cancel the asynchronous handling
	 **/
	this.setCancelHandler = function( s )
	{
		cancelHandler = s;

		// update element if already created
		if( button )
			button.bbCancel = s;
	};


	/**
	 * Set the multiline selection recognition of the button
	 * @method setSingleLineSelection
	 * @param {boolean} s Selections cannot be multiline if set to true (default is false)
	 **/
	this.setSingleLineSelection = function( s )
	{
		singleLineSelection = s;

		// update element if already created
		if( button )
			button.singleLineSelection = s;
	};


	/**
	 * Set the CSS class of the button's element
	 * @method setClassName
	 * @param {String} s Name of the CSS class
	 **/
	this.setClassName = function( s )
	{
		className = s;

		// update element if already created
		if( button )
			button.className = s;
	};


	/**
	 * Create (or take an external) element for user-interaction for the button.
	 * Enhance either created or supplied element with BBButtons-specific attributes and methods
	 * @method toElement
	 * @param {Element} [element] Created but not yet to DOM deployed Element to set as button's element
	 * @returns {Element} the constructed Element suiting the needs of BBButtons
	 **/
	this.toElement = function( element )
	{
		// do not create multiple elements for the same button
		if( button )
			return button;

		// if no element is supplied, create a default input-button
		if( typeof element == 'undefined' )
		{
			button = document.createElement('button');
			button.innerHTML = title;
			button.className = className;
		}
		else
			button = element;

		// if no generateHandler is supplied, assign the default BBCodeHandler (bbhandle_default)
		if( !generateHandler )
			button.bbGenerate = bbhandle_default;
		else
			button.bbGenerate = generateHandler;

		// assign cancelHandler if one is specified
		if( cancelHandler )
			button.bbCancel = cancelHandler;

		// set information for BBButtons handling-system
		button.bbSingleLineSelection = singleLineSelection;
		button.bbTagName = tagName;
		button.bbAttributes = attributes;
		button.bbFormat = formats;

		return button;
	};
}


/*******************************************************************************
 *                D E F A U L T   B B C O D E   H A N D L E R S	               *
 * These functions are only the basic package of bbcodeHandlers given you by   *
 * the hand to see what can be done very easily in pretty a generic way.       *
 * The true power of BBButtons reveals itself once you start experimenting     *
 * with asnychronous callbacks, for example to create your own input-masks.    *
 * Or maybe even work up some really handy AJAX stuff to access your database  *
 * and speed up administrating your own documents.                             *
 ******************************************************************************/


/**
 * <p>Construct the code for default-BBCodes (e.g. "[tag]foobar[/tag]").</p>
 * <p>Will look for the BBCode-Button attribute <em>Format</em>.</p>
 * @function bbhandle_default
 * @param {String} selectedText The text selected, supplied by BBButtons
 * @param {BBButtons} bbb The BBButtons instance the handler is running in
 * @returns {String} The constructed code
 **/
var bbhandle_default = function( selectedText, bbb)
{
	var vars = {
		'tag' : this.bbTagName,
		'text' : selectedText
	};

	for( i in this.bbAttributes )
	{
		vars[i] = this.bbAttributes[i];
	}

	if( typeof this.bbFormat == 'undefined' )
		this.bbFormat = null;

	return bbformat( this.bbFormat, vars, [
		{ 'format' : '[%(tag)]%(text)[/%(tag)]' }
		] );
};


/**
 * <p>Construct the code for link-BBCodes (e.g. "[link=http://rodneyrehm.de]my home[/link]").</p>
 * <p>Will look for the BBCode-Button attribute <em>Format</em>.</p>
 * @function bbhandle_link
 * @param {String} selectedText The text selected, supplied by BBButtons
 * @param {BBButtons} bbb The BBButtons instance the handler is running in
 * @returns {String} The constructed code
 **/
var bbhandle_link = function( selectedText, bbb )
{
	var mail= new RegExp('^[a-z0-9][a-z0-9_\\\-\\\.]*@[a-z0-9-\\\.]*\\\.[a-z]{2,4}$', 'i');

	var vars = {
		'tag' : this.bbTagName
	};

	for( i in this.bbAttributes )
	{
		vars[i] = this.bbAttributes[i];
	}

	// check if selectedText is a link of any sort
	var href = null;
	if( selectedText.indexOf('http') === 0
		|| selectedText.indexOf('ftp') === 0
		|| selectedText.indexOf('news') === 0
		|| selectedText.indexOf('mailto') === 0
		|| selectedText.indexOf('nntp') === 0
		|| selectedText.indexOf('telnet') === 0
		|| selectedText.indexOf('gopher') === 0 )
	{
		href = selectedText;
	}
	else if( selectedText.indexOf('www') === 0 )
	{
		href = 'http://' + selectedText;
	}
	else if ( mail.test(selectedText) )
	{
		href = 'mailto:' + selectedText;
	}


	/* selectedText is a link */
	if( href )
	{
		var title = prompt('Bezeichnung eingeben:');

		// user hit cancel, so abort
		if( title === null )
			return null;

		vars.url = href;

		if( title )
			vars.text = title;
	}
	/* selectedText is a title */
	else
	{
		href = prompt('URL eingeben:', 'http://');

		// user hit cancel or ommitted input, so abort
		if( !href || href == 'http://' )
			return null;

		if( href.indexOf('://') == -1 )
			href = 'http://' + href;

		var title = selectedText;
		if( !title )
			title = prompt('Bezeichnung eingeben:');

		// user hit cancel, so abort
		if( title === null )
			return null;

		vars.url = href;

		if( title )
			vars.text = title;
	}

	var t = bbformat( this.bbFormat, vars, [
		{ 'format' : '[%(tag)=%(url)]%(text)[/%(tag)]', 'required' : ['text'] },
		{ 'format' : '[%(tag)]%(url)[/%(tag)]' }
		] );

	// if there was a selection normally return the constructed code
	if( selectedText )
		return t;

	// insert constructed code with the respective length and then fail the handler
	// to stop BBButtons executing processTag()
	bbb.insertText( t, t.length );
	return null;
};


/**
 * <p>Construct the code for image-BBCodes (e.g. "[image=http://rodneyrehm.de/me.jpg]My horrible face[/image]").</p>
 * <p>Will look for the BBCode-Button attribute <em>Format</em>.</p>
 * @function bbhandle_image
 * @param {String} selectedText The text selected, supplied by BBButtons
 * @param {BBButtons} bbb The BBButtons instance the handler is running in
 * @returns {String} The constructed code
 **/
var bbhandle_image = function( selectedText, bbb )
{
	var vars = {
		'tag' : this.bbTagName
	};

	for( i in this.bbAttributes )
	{
		vars[i] = this.bbAttributes[i];
	}

	/* selectedText is a link */
	if( selectedText.indexOf('http') === 0
		|| selectedText.indexOf('ftp') === 0
		|| selectedText.indexOf('www') === 0 )
	{
		var href = selectedText;

		if( selectedText.indexOf('://') == -1 )
			href = 'http://' + href;

		var alt = prompt('Alternativtext eingeben:');

		// user hit cancel, so abort
		if( alt === null )
			return null;

		vars.url = href;

		if( alt )
			vars.text = alt;
	}
	/* selectedText is a title */
	else
	{
		var href = prompt('Bild URL eingeben:', 'http://');

		// user hit cancel or ommitted input, so abort
		if( !href || href == 'http://' )
			return null;

		var alt = selectedText;
		if( !alt )
			alt = prompt('Alternativtext eingeben:');

		// user hit cancel, so abort
		if( alt === null )
			return null;

		if( selectedText.indexOf('://') == -1 )
			href = 'http://' + href;

		vars.url = href;

		if( alt )
			vars.text = alt;
	}

	var t = bbformat( this.bbFormat, vars, [
		{ 'format' : '[%(tag)=%(url)]%(text)[/%(tag)]', 'required' : ['text'] },
		{ 'format' : '[%(tag)]%(url)[/%(tag)]' }
		] );

	// if there was a selection normally return the constructed code
	if( selectedText )
		return t;

	// insert constructed code with the respective length and then fail the handler
	// to stop BBButtons executing processTag()
	bbb.insertText( t, t.length );
	return null;
};


/**
 * Merge the best-matching formatstring with data supplied in vars
 * @function bbformat
 * @param {Array} [formatstrings] An Array of Objects (of type: { 'format': "theFormat", 'requires': ['var1','var2'] })
 *		which supplies the format of the code to return using the function bbformat
 * @param {BBButtons} bbb The BBButtons instance the handler is running in
 * @param {Array} [defaults] Same structure as formatstrings. These formats
 *		are the default-formats and used if formatstrings is empty
 * @returns {String} The constructed code
 **/
var bbformat = function( formatstrings, vars, defaults )
{
	if( !formatstrings || !formatstrings.length )
		formatstrings = defaults;

	// find appropriate formatstring
	var formatstring;
	for( var i = 0; i < formatstrings.length; i++ )
	{
		var misses = 0;

		if( !formatstrings[i].required )
		{
			formatstring = formatstrings[i]['format'];
			break;
		}

		for( var r = 0; r < formatstrings[i].required.length; r++ )
		{
			if( typeof vars[ formatstrings[i].required[r] ] == 'undefined' )
			{
				misses = 1;
				break;
			}
		}

		if( !misses )
		{
			formatstring = formatstrings[i]['format'];
			break;
		}
	}

	var st = new StringTemplate();
	return st.eval(formatstring, vars, true );
};


/**
 * Default character counter handler to update the
 * visualization of the number of remaining chars.
 * This function needs to be assigned to an element by the attribute-name "updateCount".
 *
 * @function bbCharCountHandler
 * @param {Element} input The input element get length from
 * @param {Number} maxLength the maximum allowed number of characters
 **/
var bbCharCountHandler = function( input, maxLength )
{
	if( !maxLength )
		return;

	if( maxLength < input.value.length )
	{
		input.value = input.value.substr( 0, maxLength );
		alert( 'Limit von '+ maxLength +' Zeichen erreicht' );
	}

	var ccMessage = (maxLength - input.value.length) + ' Zeichen übrig';

	if( this.tagName == 'INPUT')
		this.value = ccMessage;
	else
		this.innerHTML = ccMessage;
};
