/*
 * atd.core.js - A building block to create a front-end for AtD
 * Author      : Raphael Mudge, Automattic
 * License     : LGPL
 * Project     : http://www.afterthedeadline.com/developers.slp
 * Contact     : raffi@automattic.com
 */

/* jshint sub: true, devel: true, onevar: false, smarttabs: true */
/* exported EXPORTED_SYMBOLS, atd_sprintf */

/* EXPORTED_SYMBOLS is set so this file can be a JavaScript Module */
var EXPORTED_SYMBOLS = ['AtDCore'];

function AtDCore() {
	/* these are the categories of errors AtD should ignore */
	this.ignore_types = ['Bias Language', 'Cliches', 'Complex Expression', 'Diacritical Marks', 'Double Negatives', 'Hidden Verbs', 'Jargon Language', 'Passive voice', 'Phrases to Avoid', 'Redundant Expression'];

	/* these are the phrases AtD should ignore */
	this.ignore_strings = {};

	/* Localized strings */
	// Back-compat, not used
	this.i18n = {};
}

/*
 * Internationalization Functions
 */

AtDCore.prototype.getLang = function( key, defaultk ) {
	return ( window.AtD_l10n_r0ar && window.AtD_l10n_r0ar[key] ) || defaultk;
};

AtDCore.prototype.addI18n = function( obj ) {
	// Back-compat
	window.AtD_l10n_r0ar = obj;
};

/*
 * Setters
 */

AtDCore.prototype.setIgnoreStrings = function(string) {
	var parent = this;

	this.map(string.split(/,\s*/g), function(string) {
		parent.ignore_strings[string] = 1;
	});
};

AtDCore.prototype.showTypes = function(string) {
	var show_types = string.split(/,\s*/g);
	var types = {};

	/* set some default types that we want to make optional */

		/* grammar checker options */
	types['Double Negatives']     = 1;
	types['Hidden Verbs']         = 1;
	types['Passive voice']        = 1;
	types['Bias Language']        = 1;

		/* style checker options */
	types['Cliches']              = 1;
	types['Complex Expression']   = 1;
	types['Diacritical Marks']    = 1;
	types['Jargon Language']      = 1;
	types['Phrases to Avoid']     = 1;
	types['Redundant Expression'] = 1;

        var ignore_types = [];

        this.map(show_types, function(string) {
                types[string] = undefined;
        });

        this.map(this.ignore_types, function(string) {
                if (types[string] !== undefined) {
                        ignore_types.push(string);
		}
        });

        this.ignore_types = ignore_types;
};

/*
 * Error Parsing Code
 */

AtDCore.prototype.makeError = function(error_s, tokens, type, seps/*, pre*/) {
	var struct = {};
	struct.type = type;
	struct.string = error_s;
	struct.tokens = tokens;

	if (new RegExp('\\b' + error_s + '\\b').test(error_s)) {
		struct.regexp = new RegExp('(?!'+error_s+'<)\\b' + error_s.replace(/\s+/g, seps) + '\\b');
	}
	else if (new RegExp(error_s + '\\b').test(error_s)) {
		struct.regexp = new RegExp('(?!'+error_s+'<)' + error_s.replace(/\s+/g, seps) + '\\b');
	}
	else if (new RegExp('\\b' + error_s).test(error_s)) {
		struct.regexp = new RegExp('(?!'+error_s+'<)\\b' + error_s.replace(/\s+/g, seps));
	}
	else {
		struct.regexp = new RegExp('(?!'+error_s+'<)' + error_s.replace(/\s+/g, seps));
	}

	struct.used   = false; /* flag whether we've used this rule or not */

	return struct;
};

AtDCore.prototype.addToErrorStructure = function(errors, list, type, seps) {
	var parent = this;

	this.map(list, function(error) {
		var tokens = error['word'].split(/\s+/);
		var pre    = error['pre'];
		var first  = tokens[0];

		if (errors['__' + first] === undefined) {
			errors['__' + first] = {};
			errors['__' + first].pretoks  = {};
			errors['__' + first].defaults = [];
		}

		if (pre === '') {
			errors['__' + first].defaults.push(parent.makeError(error['word'], tokens, type, seps, pre));
		} else {
			if (errors['__' + first].pretoks['__' + pre] === undefined) {
				errors['__' + first].pretoks['__' + pre] = [];
			}

			errors['__' + first].pretoks['__' + pre].push(parent.makeError(error['word'], tokens, type, seps, pre));
		}
	});
};

AtDCore.prototype.buildErrorStructure = function(spellingList, enrichmentList, grammarList) {
	var seps   = this._getSeparators();
	var errors = {};

	this.addToErrorStructure(errors, spellingList, 'hiddenSpellError', seps);
	this.addToErrorStructure(errors, grammarList, 'hiddenGrammarError', seps);
	this.addToErrorStructure(errors, enrichmentList, 'hiddenSuggestion', seps);
	return errors;
};

AtDCore.prototype._getSeparators = function() {
	var re = '', i;
	var str = '"s!#$%&()*+,./:;<=>?@[\\]^_{|}';

	// Build word separator regexp
	for (i=0; i<str.length; i++) {
		re += '\\' + str.charAt(i);
	}

	return '(?:(?:[\xa0' + re  + '])|(?:\\-\\-))+';
};

AtDCore.prototype.processXML = function(responseXML) {

	/* types of errors to ignore */
	var types = {};

	this.map(this.ignore_types, function(type) {
		types[type] = 1;
	});

	/* save suggestions in the editor object */
	this.suggestions = [];

	/* process through the errors */
	var errors = responseXML.getElementsByTagName('error');

	/* words to mark */
	var grammarErrors    = [];
	var spellingErrors   = [];
	var enrichment       = [];

	for (var i = 0; i < errors.length; i++) {
		if (errors[i].getElementsByTagName('string').item(0).firstChild !== null) {
			var errorString      = errors[i].getElementsByTagName('string').item(0).firstChild.data;
			var errorType        = errors[i].getElementsByTagName('type').item(0).firstChild.data;
			var errorDescription = errors[i].getElementsByTagName('description').item(0).firstChild.data;

			var errorContext;

			if (errors[i].getElementsByTagName('precontext').item(0).firstChild !== null) {
				errorContext = errors[i].getElementsByTagName('precontext').item(0).firstChild.data;
			} else {
				errorContext = '';
			}

			/* create a hashtable with information about the error in the editor object, we will use this later
			   to populate a popup menu with information and suggestions about the error */

			if (this.ignore_strings[errorString] === undefined) {
				var suggestion = {};
				suggestion['description'] = errorDescription;
				suggestion['suggestions'] = [];

				/* used to find suggestions when a highlighted error is clicked on */
				suggestion['matcher']     = new RegExp('^' + errorString.replace(/\s+/, this._getSeparators()) + '$');

				suggestion['context']     = errorContext;
				suggestion['string']      = errorString;
				suggestion['type']        = errorType;

				this.suggestions.push(suggestion);

				if (errors[i].getElementsByTagName('suggestions').item(0) !== null) {
					var suggestions = errors[i].getElementsByTagName('suggestions').item(0).getElementsByTagName('option');
					for (var j = 0; j < suggestions.length; j++) {
						suggestion['suggestions'].push(suggestions[j].firstChild.data);
					}
				}

				/* setup the more info url */
				if (errors[i].getElementsByTagName('url').item(0) !== null) {
					var errorUrl = errors[i].getElementsByTagName('url').item(0).firstChild.data;
					suggestion['moreinfo'] = errorUrl + '&theme=tinymce';
				}

				if (types[errorDescription] === undefined) {
					if (errorType === 'suggestion') {
						enrichment.push({ word: errorString, pre: errorContext });
					}

					if (errorType === 'grammar') {
						grammarErrors.push({ word: errorString, pre: errorContext });
					}
				}

				if (errorType === 'spelling' || errorDescription === 'Homophone') {
					spellingErrors.push({ word: errorString, pre: errorContext });
				}

				if (errorDescription === 'Cliches') {
					suggestion['description'] = 'Clichés'; /* done here for backwards compatability with current user settings */
				}

				if (errorDescription === 'Spelling') {
					suggestion['description'] = this.getLang('menu_title_spelling', 'Spelling');
				}

				if (errorDescription === 'Repeated Word') {
					suggestion['description'] = this.getLang('menu_title_repeated_word', 'Repeated Word');
				}

				if (errorDescription === 'Did you mean...') {
					suggestion['description'] = this.getLang('menu_title_confused_word', 'Did you mean...');
				}
			} // end if ignore[errorString] == undefined
		} // end if
	} // end for loop

	var errorStruct;
        var ecount = spellingErrors.length + grammarErrors.length + enrichment.length;

	if (ecount > 0) {
		errorStruct = this.buildErrorStructure(spellingErrors, enrichment, grammarErrors);
	} else {
		errorStruct = undefined;
	}

	/* save some state in this object, for retrieving suggestions later */
	return { errors: errorStruct, count: ecount, suggestions: this.suggestions };
};

AtDCore.prototype.findSuggestion = function(element) {
        var text = element.innerHTML;
        var context = ( this.getAttrib(element, 'pre') + '' ).replace(/[\\,!\\?\\."\s]/g, '');
        if (this.getAttrib(element, 'pre') === undefined) {
           alert(element.innerHTML);
        }

	var errorDescription;
	var len = this.suggestions.length;

	for (var i = 0; i < len; i++) {
		if ((context === '' || context === this.suggestions[i]['context']) && this.suggestions[i]['matcher'].test(text)) {
			errorDescription = this.suggestions[i];
			break;
		}
	}
	return errorDescription;
};

/*
 * TokenIterator class
 */

function TokenIterator(tokens) {
	this.tokens = tokens;
	this.index  = 0;
	this.count  = 0;
	this.last   = 0;
}

TokenIterator.prototype.next = function() {
	var current = this.tokens[this.index];
	this.count = this.last;
	this.last += current.length + 1;
	this.index++;

	/* strip single quotes from token, AtD does this when presenting errors */
	if (current !== '') {
		if (current[0] === '\'') {
			current = current.substring(1, current.length);
		}

		if (current[current.length - 1] === '\'') {
			current = current.substring(0, current.length - 1);
		}
	}

	return current;
};

TokenIterator.prototype.hasNext = function() {
	return this.index < this.tokens.length;
};

TokenIterator.prototype.hasNextN = function(n) {
	return (this.index + n) < this.tokens.length;
};

TokenIterator.prototype.skip = function(m, n) {
	this.index += m;
	this.last += n;

	if (this.index < this.tokens.length) {
		this.count = this.last - this.tokens[this.index].length;
	}
};

TokenIterator.prototype.getCount = function() {
	return this.count;
};

TokenIterator.prototype.peek = function(n) {
	var peepers = [];
	var end = this.index + n;
	for (var x = this.index; x < end; x++) {
		peepers.push(this.tokens[x]);
	}
	return peepers;
};

/*
 *  code to manage highlighting of errors
 */
AtDCore.prototype.markMyWords = function(container_nodes, errors) {
	var seps = new RegExp(this._getSeparators()),
		nl = [],
		ecount = 0, /* track number of highlighted errors */
		parent = this,
		bogus = this._isTinyMCE ? ' data-mce-bogus="1"' : '',
		emptySpan = '<span class="mceItemHidden"' + bogus + '>&nbsp;</span>',
		textOnlyMode;

	/**
	 * Split a text node into an ordered list of siblings:
	 * - text node to the left of the match
	 * - the element replacing the match
	 * - text node to the right of the match
	 *
	 * We have to leave the text to the left and right of the match alone
	 * in order to prevent XSS
	 *
	 * @return array
	 */
	function splitTextNode( textnode, regexp, replacement ) {
		var text = textnode.nodeValue,
			index = text.search( regexp ),
			match = text.match( regexp ),
			captured = [],
			cursor;

		if ( index < 0 || ! match.length ) {
			return [ textnode ];
		}

		if ( index > 0 ) {
			// capture left text node
			captured.push( document.createTextNode( text.substr( 0, index ) ) );
		}

		// capture the replacement of the matched string
		captured.push( parent.create( match[0].replace( regexp, replacement ) ) );

		cursor = index + match[0].length;

		if ( cursor < text.length ) {
			// capture right text node
			captured.push( document.createTextNode( text.substr( cursor ) ) );
		}

		return captured;
	}

	function _isInPre( node ) {
		if ( node ) {
			while ( node.parentNode ) {
				if ( node.nodeName === 'PRE' ) {
					return true;
				}
				node = node.parentNode;
			}
		}

		return false;
	}

	/* Collect all text nodes */
	/* Our goal--ignore nodes that are already wrapped */

	this._walk( container_nodes, function( n ) {
		if ( n.nodeType === 3 && ! parent.isMarkedNode( n ) && ! _isInPre( n ) ) {
			nl.push( n );
		}
	});

	/* walk through the relevant nodes */

	var iterator;

	this.map( nl, function( n ) {
		var v;

		if ( n.nodeType === 3 ) {
			v = n.nodeValue; /* we don't want to mangle the HTML so use the actual encoded string */
			var tokens = n.nodeValue.split( seps ); /* split on the unencoded string so we get access to quotes as " */
			var previous = '';

			var doReplaces = [];

			iterator = new TokenIterator(tokens);

			while ( iterator.hasNext() ) {
				var token = iterator.next();
				var current  = errors['__' + token];

				var defaults;

				if ( current !== undefined && current.pretoks !== undefined ) {
					defaults = current.defaults;
					current = current.pretoks['__' + previous];

					var done = false;
					var prev, curr;

					prev = v.substr(0, iterator.getCount());
					curr = v.substr(prev.length, v.length);

					var checkErrors = function( error ) {
						if ( error !== undefined && ! error.used && foundStrings[ '__' + error.string ] === undefined && error.regexp.test( curr ) ) {
							foundStrings[ '__' + error.string ] = 1;
							doReplaces.push([ error.regexp, '<span class="'+error.type+'" pre="'+previous+'"' + bogus + '>$&</span>' ]);

							error.used = true;
							done = true;
						}
					}; // jshint ignore:line

					var foundStrings = {};

					if (current !== undefined) {
						previous = previous + ' ';
						parent.map(current, checkErrors);
					}

					if (!done) {
						previous = '';
						parent.map(defaults, checkErrors);
					}
				}

				previous = token;
			} // end while

			/* do the actual replacements on this span */
			if ( doReplaces.length > 0 ) {
				var newNode = n;

				for ( var x = 0; x < doReplaces.length; x++ ) {
					var regexp = doReplaces[x][0], result = doReplaces[x][1];

					/* it's assumed that this function is only being called on text nodes (nodeType == 3), the iterating is necessary
					   because eventually the whole thing gets wrapped in an mceItemHidden span and from there it's necessary to
					   handle each node individually. */
					var bringTheHurt = function( node ) {
						var span, splitNodes;

						if ( node.nodeType === 3 ) {
							ecount++;

							/* sometimes IE likes to ignore the space between two spans, solution is to insert a placeholder span with
							   a non-breaking space.  The markup removal code substitutes this span for a space later */
							if ( parent.isIE() && node.nodeValue.length > 0 && node.nodeValue.substr(0, 1) === ' ' ) {
								return parent.create( emptySpan + node.nodeValue.substr( 1, node.nodeValue.length - 1 ).replace( regexp, result ), false );
							} else {
								if ( textOnlyMode ) {
									return parent.create( node.nodeValue.replace( regexp, result ), false );
								}

								span = parent.create( '<span />' );
								if ( typeof textOnlyMode === 'undefined' ) {
									// cache this to avoid adding / removing nodes unnecessarily
									textOnlyMode = typeof span.appendChild !== 'function';
									if ( textOnlyMode ) {
										parent.remove( span );
										return parent.create( node.nodeValue.replace( regexp, result ), false );
									}
								}

								// "Visual" mode
								splitNodes = splitTextNode( node, regexp, result );
								for ( var i = 0; i < splitNodes.length; i++ ) {
									span.appendChild( splitNodes[i] );
								}

								node = span;
								return node;
							}
						}
						else {
							var contents = parent.contents(node);

							for ( var y = 0; y < contents.length; y++ ) {
								if ( contents[y].nodeType === 3 && regexp.test( contents[y].nodeValue ) ) {
									var nnode;

									if ( parent.isIE() && contents[y].nodeValue.length > 0 && contents[y].nodeValue.substr(0, 1) === ' ') {
										nnode = parent.create( emptySpan + contents[y].nodeValue.substr( 1, contents[y].nodeValue.length - 1 ).replace( regexp, result ), true );
									} else {
										nnode = parent.create( contents[y].nodeValue.replace( regexp, result ), true );
									}

									parent.replaceWith( contents[y], nnode );
									parent.removeParent( nnode );

									ecount++;

									return node; /* we did a replacement so we can call it quits, errors only get used once */
								}
							}

							return node;
						}
					}; // jshint ignore:line

					newNode = bringTheHurt(newNode);
				}

				parent.replaceWith(n, newNode);
			}
		}
	});

	return ecount;
};

AtDCore.prototype._walk = function(elements, f) {
	var i;
	for (i = 0; i < elements.length; i++) {
		f.call(f, elements[i]);
		this._walk(this.contents(elements[i]), f);
	}
};

AtDCore.prototype.removeWords = function(node, w) {
	var count = 0;
	var parent = this;

	this.map(this.findSpans(node).reverse(), function(n) {
		if (n && (parent.isMarkedNode(n) || parent.hasClass(n, 'mceItemHidden') || parent.isEmptySpan(n)) ) {
			if (n.innerHTML === '&nbsp;') {
				var nnode = document.createTextNode(' '); /* hax0r */
				parent.replaceWith(n, nnode);
			} else if (!w || n.innerHTML === w) {
				parent.removeParent(n);
				count++;
			}
		}
	});

	return count;
};

AtDCore.prototype.isEmptySpan = function(node) {
	return (this.getAttrib(node, 'class') === '' && this.getAttrib(node, 'style') === '' && this.getAttrib(node, 'id') === '' && !this.hasClass(node, 'Apple-style-span') && this.getAttrib(node, 'mce_name') === '');
};

AtDCore.prototype.isMarkedNode = function(node) {
	return (this.hasClass(node, 'hiddenGrammarError') || this.hasClass(node, 'hiddenSpellError') || this.hasClass(node, 'hiddenSuggestion'));
};

/*
 * Context Menu Helpers
 */
AtDCore.prototype.applySuggestion = function(element, suggestion) {
	if (suggestion === '(omit)') {
		this.remove(element);
	}
	else {
		var node = this.create(suggestion);
		this.replaceWith(element, node);
		this.removeParent(node);
	}
};

/*
 * Check for an error
 */
AtDCore.prototype.hasErrorMessage = function(xmlr) {
	return (xmlr !== undefined && xmlr.getElementsByTagName('message').item(0) !== null);
};

AtDCore.prototype.getErrorMessage = function(xmlr) {
	return xmlr.getElementsByTagName('message').item(0);
};

/* this should always be an error, alas... not practical */
AtDCore.prototype.isIE = function() {
	return navigator.appName === 'Microsoft Internet Explorer';
};

// TODO: this doesn't seem used anywhere in AtD, moved here from install_atd_l10n.js for eventual back-compat 
/* a quick poor man's sprintf */
function atd_sprintf(format, values) {
	var result = format;
	for (var x = 0; x < values.length; x++) {
		result = result.replace(new RegExp('%' + (x + 1) + '\\$', 'g'), values[x]);
	}
	return result;
}