/**

 * Tag-closer extension for CodeMirror.

 *

 * This extension adds a "closeTag" utility function that can be used with key bindings to 

 * insert a matching end tag after the ">" character of a start tag has been typed.  It can

 * also complete "</" if a matching start tag is found.  It will correctly ignore signal

 * characters for empty tags, comments, CDATA, etc.

 *

 * The function depends on internal parser state to identify tags.  It is compatible with the

 * following CodeMirror modes and will ignore all others:

 * - htmlmixed

 * - xml

 *

 * See demos/closetag.html for a usage example.

 * 

 * @author Nathan Williams <nathan@nlwillia.net>

 * Contributed under the same license terms as CodeMirror.

 */

(function() {

	/** Option that allows tag closing behavior to be toggled.  Default is true. */

	CodeMirror.defaults['closeTagEnabled'] = true;

	

	/** Array of tag names to add indentation after the start tag for.  Default is the list of block-level html tags. */

	CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul'];



	/**

	 * Call during key processing to close tags.  Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.

	 * - cm: The editor instance.

	 * - ch: The character being processed.

	 * - indent: Optional.  Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option.

	 *   Pass false to disable indentation.  Pass an array to override the default list of tag names.

	 */

	CodeMirror.defineExtension("closeTag", function(cm, ch, indent) {

		if (!cm.getOption('closeTagEnabled')) {

			throw CodeMirror.Pass;

		}

		

		var mode = cm.getOption('mode');

		

		if (mode == 'text/html') {

		

			/*

			 * Relevant structure of token:

			 *

			 * htmlmixed

			 * 		className

			 * 		state

			 * 			htmlState

			 * 				type

			 * 				context

			 * 					tagName

			 * 			mode

			 * 

			 * xml

			 * 		className

			 * 		state

			 * 			tagName

			 * 			type

			 */

		

			var pos = cm.getCursor();

			var tok = cm.getTokenAt(pos);

			var state = tok.state;

			

			if (state.mode && state.mode != 'html') {

				throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode.

			}

			

			if (ch == '>') {

				var type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml

				

				if (tok.className == 'tag' && type == 'closeTag') {

					throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.

				}

			

				cm.replaceSelection('>'); // Mode state won't update until we finish the tag.

				pos = {line: pos.line, ch: pos.ch + 1};

				cm.setCursor(pos);

		

				tok = cm.getTokenAt(cm.getCursor());

				state = tok.state;

				type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml



				if (tok.className == 'tag' && type != 'selfcloseTag') {

					var tagName = state.htmlState ? state.htmlState.context.tagName : state.tagName; // htmlmixed : xml

					if (tagName.length > 0) {

						insertEndTag(cm, indent, pos, tagName);

					}

					return;

				}

				

				// Undo the '>' insert and allow cm to handle the key instead.

				cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos);

				cm.replaceSelection("");

			

			} else if (ch == '/') {

				if (tok.className == 'tag' && tok.string == '<') {

					var tagName = state.htmlState ? (state.htmlState.context ? state.htmlState.context.tagName : '') : state.context.tagName; // htmlmixed : xml # extra htmlmized check is for '</' edge case

					if (tagName.length > 0) {

						completeEndTag(cm, pos, tagName);

						return;

					}

				}

			}

		

		}

		

		throw CodeMirror.Pass; // Bubble if not handled

	});



	function insertEndTag(cm, indent, pos, tagName) {

		if (shouldIndent(cm, indent, tagName)) {

			cm.replaceSelection('\n\n</' + tagName + '>', 'end');

			cm.indentLine(pos.line + 1);

			cm.indentLine(pos.line + 2);

			cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length});

		} else {

			cm.replaceSelection('</' + tagName + '>');

			cm.setCursor(pos);

		}

	}

	

	function shouldIndent(cm, indent, tagName) {

		if (typeof indent == 'undefined' || indent == null || indent == true) {

			indent = cm.getOption('closeTagIndent');

		}

		if (!indent) {

			indent = [];

		}

		return indexOf(indent, tagName.toLowerCase()) != -1;

	}

	

	// C&P from codemirror.js...would be nice if this were visible to utilities.

	function indexOf(collection, elt) {

		if (collection.indexOf) return collection.indexOf(elt);

		for (var i = 0, e = collection.length; i < e; ++i)

			if (collection[i] == elt) return i;

		return -1;

	}



	function completeEndTag(cm, pos, tagName) {

		cm.replaceSelection('/' + tagName + '>');

		cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 });

	}

	

})();

