/* Highlight module for wikiEditor */

( function ( $ ) {

$.wikiEditor.modules.highlight = {

	/**
	 * Core Requirements
	 */
	req: [ 'iframe' ],

	/**
	 * Configuration
	 */
	cfg: {
		styleVersion: 3
	},

	/**
	 * Internally used event handlers
	 */
	evt: {
		delayedChange: function ( context, event ) {
			if ( event.data.scope == 'realchange' ) {
				$.wikiEditor.modules.highlight.fn.scan( context );
				$.wikiEditor.modules.highlight.fn.mark( context, event.data.scope );
			}
		},
		ready: function ( context, event ) {
			$.wikiEditor.modules.highlight.fn.scan( context );
			$.wikiEditor.modules.highlight.fn.mark( context, 'ready' );
		}
	},

	/**
	 * Internally used functions
	 */
	fn: {
		/**
		 * Creates a highlight module within a wikiEditor
		 *
		 * @param config Configuration object to create module from
		 */
		create: function ( context, config ) {
			context.modules.highlight.markersStr = '';
		},
		/**
		 * Scans text division for tokens
		 *
		 * @param division
		 */
		scan: function ( context, division ) {
			var tokenArray, text, module, exp,
				left, right, match;
			/*jshint eqnull: true */

			// Remove all existing tokens
			tokenArray = context.modules.highlight.tokenArray = [];
			// Scan text for new tokens
			text = context.fn.getContents();
			// Perform a scan for each module which provides any expressions to scan for
			// FIXME: This traverses the entire string once for every regex. Investigate
			// whether |-concatenating regexes then traversing once is faster.
			for ( module in context.modules ) {
				if ( module in $.wikiEditor.modules && 'exp' in $.wikiEditor.modules[module] ) {
					for ( exp in $.wikiEditor.modules[module].exp ) {
						// Prepare configuration
						var regex = $.wikiEditor.modules[module].exp[exp].regex;
						var label = $.wikiEditor.modules[module].exp[exp].label;
						var markAfter = $.wikiEditor.modules[module].exp[exp].markAfter || false;
						// Search for tokens
						var offset = 0;
						while ( ( match = text.substr( offset ).match( regex ) ) != null ) {
							right = ( left = offset + match.index ) + match[0].length;
							tokenArray[tokenArray.length] = {
								offset: markAfter ? right : left,
								label: label,
								tokenStart: left,
								match: match
							};
							// Move to the right of this match
							offset = right;
						}
					}
				}
			}
			// Sort by start
			tokenArray.sort( function ( a, b ) {
				return a.tokenStart - b.tokenStart;
			} );
			// Let the world know, a scan just happened!
			context.fn.trigger( 'scan' );
		},

		/**
		 * Marks up text with HTML
		 *
		 * @param division
		 * @param tokens
		 */
		// FIXME: What do division and tokens do?
		// TODO: Document the scan() and mark() APIs somewhere
		mark: function ( context, division, tokens ) {
			var i, subtracted, oldLength, j, o;

			// Reset markers
			var markers = [];

			// Recycle markers that will be skipped in this run
			if ( context.modules.highlight.markers && division !== '' ) {
				for ( i = 0; i < context.modules.highlight.markers.length; i++ ) {
					if ( context.modules.highlight.markers[i].skipDivision == division ) {
						markers.push( context.modules.highlight.markers[i] );
					}
				}
			}
			context.modules.highlight.markers = markers;

			// Get all markers
			context.fn.trigger( 'mark' );
			markers.sort( function ( a, b ) {
				return a.start - b.start || a.end - b.end;
			} );

			// Serialize the markers array to a string and compare it with the one stored in the previous run - if they're
			// equal, there's no markers to change
			var markersStr = '';
			for ( i = 0; i < markers.length; i++ ) {
				markersStr += markers[i].start + ',' + markers[i].end + ',' + markers[i].type + ',';
			}
			if ( context.modules.highlight.markersStr == markersStr ) {
				// No change, bail out
				return;
			}
			context.modules.highlight.markersStr = markersStr;

			// Traverse the iframe DOM, inserting markers where they're needed - store visited markers here so we know which
			// markers should be removed
			var visited = [], v = 0;
			for ( i = 0; i < markers.length; i++ ) {
				if ( typeof markers[i].skipDivision !== 'undefined' && ( division == markers[i].skipDivision ) ) {
					continue;
				}

				// We want to isolate each marker, so we may need to split textNodes if a marker starts or ends halfway one.
				var start = markers[i].start;
				var s = context.fn.getOffset( start );
				if ( !s ) {
					// This shouldn't happen
					continue;
				}
				var startNode = s.node;

				// Don't wrap leading BRs, produces undesirable results
				// FIXME: It's also possible that the offset is a bit high because getOffset() has incremented .length to
				// fake the newline caused by startNode being in a P. In this case, prevent the textnode splitting below
				// from making startNode an empty textnode, IE barfs on that
				while ( startNode.nodeName === 'BR' || s.offset === startNode.nodeValue.length ) {
					start++;
					s = context.fn.getOffset( start );
					startNode = s.node;
				}

				// The next marker starts somewhere in this textNode or at this BR
				if ( s.offset > 0 && s.node.nodeName == '#text' ) {
					// Split off the prefix - this leaves the prefix in the current node and puts the rest in a new node
					// which is our start node
					var newStartNode = startNode.splitText( s.offset < s.node.nodeValue.length ?
						s.offset : s.node.nodeValue.length - 1
					);
					var oldStartNode = startNode;
					startNode = newStartNode;
					// Update offset objects. We don't need purgeOffsets(), simply manipulating the existing offset objects
					// will suffice
					// FIXME: This manipulates context.offsets directly, which is ugly, but the performance improvement vs.
					// purgeOffsets() is worth it - this code doesn't set lastTextNode to newStartNode for offset objects
					// with lastTextNode == oldStartNode, but that doesn't really matter
					subtracted = s.offset;
					oldLength = s.length;

					// Update offset objects referring to oldStartNode
					for ( j = start - subtracted; j < start; j++ ) {
						if ( j in context.offsets ) {
							o = context.offsets[j];
							o.node = oldStartNode;
							o.length = subtracted;
						}
					}
					// Update offset objects referring to newStartNode
					for ( j = start; j < start - subtracted + oldLength; j++ ) {
						if ( j in context.offsets ) {
							o = context.offsets[j];
							o.node = newStartNode;
							o.offset -= subtracted;
							o.length -= subtracted;
							o.lastTextNode = oldStartNode;
						}
					}
				}
				var end = markers[i].end;
				// To avoid ending up at the first char of the next node, we grab the offset for end - 1 and add one to the
				// offset
				var e = context.fn.getOffset( end - 1 );
				if ( !e ) {
					// This shouldn't happen
					continue;
				}
				var endNode = e.node;
				if ( e.offset + 1 < e.length - 1 && endNode.nodeName == '#text' ) {
					// Split off the suffix. This puts the suffix in a new node and leaves the rest in endNode
					var oldEndNode = endNode;
					var newEndNode = endNode.splitText( e.offset + 1 );
					// Update offset objects
					subtracted = e.offset + 1;
					oldLength = e.length;

					// Update offset objects referring to oldEndNode
					for ( j = end - subtracted; j < end; j++ ) {
						if ( j in context.offsets ) {
							o = context.offsets[j];
							o.node = oldEndNode;
							o.length = subtracted;
						}
					}
					// We have to insert this one, as it might not exist: we didn't call getOffset( end )
					context.offsets[end] = {
						'node': newEndNode,
						'offset': 0,
						'length': oldLength - subtracted,
						'lastTextNode': oldEndNode
					};
					// Update offset objects referring to newEndNode
					for ( j = end + 1; j < end - subtracted + oldLength; j++ ) {
						if ( j in context.offsets ) {
							o = context.offsets[j];
							o.node = newEndNode;
							o.offset -= subtracted;
							o.length -= subtracted;
							o.lastTextNode = oldEndNode;
						}
					}
				}
				// Don't wrap trailing BRs, doing that causes weird issues
				if ( endNode.nodeName == 'BR' ) {
					endNode = e.lastTextNode;
				}
				// If startNode and endNode have different parents, we need to pull endNode and all textnodes in between
				// into startNode's parent and replace </p><p> with <br>
				if ( startNode.parentNode !== endNode.parentNode ) {
					var startP = $( startNode ).closest( 'p' ).get( 0 );
					var t = new context.fn.rawTraverser( startNode, startP, context.$content.get( 0 ), false );
					var afterStart = startNode.nextSibling;
					var lastP = startP;
					var nextT = t.next();
					while ( nextT && t.node !== endNode ) {
						t = nextT;
						nextT = t.next();
						// If t.node has a different parent, merge t.node.parentNode with startNode.parentNode
						if ( t.node.parentNode !== startNode.parentNode ) {
							var oldParent = t.node.parentNode;
							if ( afterStart ) {
								if ( lastP !== t.inP ) {
									// We're entering a new <p>, insert a <br>
									startNode.parentNode.insertBefore(
										startNode.ownerDocument.createElement( 'br' ),
										afterStart
									);
								}
								// A <p> with just a <br> in it is an empty line, so let's not bother with unwrapping it
								if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) {
									// Move all children of oldParent into startNode's parent
									while ( oldParent.firstChild ) {
										startNode.parentNode.insertBefore( oldParent.firstChild, afterStart );
									}
								}
							} else {
								if ( lastP !== t.inP ) {
									// We're entering a new <p>, insert a <br>
									startNode.parentNode.appendChild(
										startNode.ownerDocument.createElement( 'br' )
									);
								}
								// A <p> with just a <br> in it is an empty line, so let's not bother with unwrapping it
								if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) {
									// Move all children of oldParent into startNode's parent
									while ( oldParent.firstChild ) {
										startNode.parentNode.appendChild( oldParent.firstChild );
									}
								}
							}
							// Remove oldParent, which is now empty
							oldParent.parentNode.removeChild( oldParent );
						}
						lastP = t.inP;
					}
					// Moving nodes around like this invalidates offset objects
					// TODO: Update offset objects ourselves for performance. Requires rewriting this code block to be
					// offset-based rather than traverser-based
				}
				// Now wrap everything between startNode and endNode (may be equal).
				var ca1 = startNode, ca2 = endNode;
				if ( ca1 && ca2 && ca1.parentNode ) {
					var anchor = markers[i].getAnchor( ca1, ca2 );
					if ( !anchor ) {
						var commonAncestor = ca1.parentNode;
						if ( markers[i].anchor == 'wrap') {
							// We have to store things like .parentNode and .nextSibling because appendChild() changes these
							var newNode = ca1.ownerDocument.createElement( 'span' );
							var nextNode = ca2.nextSibling;
							// Append all nodes between ca1 and ca2 (inclusive) to newNode
							var n = ca1;
							while ( n !== nextNode ) {
								var ns = n.nextSibling;
								newNode.appendChild( n );
								n = ns;
							}
							// Insert newNode in the right place
							if ( nextNode ) {
								commonAncestor.insertBefore( newNode, nextNode );
							} else {
								commonAncestor.appendChild( newNode );
							}
							anchor = newNode;
						} else if ( markers[i].anchor == 'tag' ) {
							anchor = commonAncestor;
						}
						$( anchor ).data( 'marker', markers[i] ).addClass( 'wikiEditor-highlight' );
						// Allow the module adding this marker to manipulate it
						markers[i].afterWrap( anchor, markers[i] );

					} else {
						// Update the marker object
						$( anchor ).data( 'marker', markers[i] );
						if ( typeof markers[i].onSkip == 'function' ) {
							markers[i].onSkip( anchor );
						}
					}
					visited[v++] = anchor;
				}
			}
			// Remove markers that were previously inserted but weren't passed to this function - visited[] contains the
			// visited elements in order and find() and each() preserve order
			j = 0;
			context.$content.find( '.wikiEditor-highlight' ).each( function () {
				if ( visited[j] == this ) {
					// This marker is legit, leave it in
					j++;
					return true;
				}
				// Remove this marker
				var marker = $(this).data( 'marker' );
				if ( marker && typeof marker.skipDivision !== 'undefined' && ( division === marker.skipDivision ) ) {
					// Don't remove these either
					return true;
				}
				if ( marker && typeof marker.beforeUnwrap === 'function' )
					marker.beforeUnwrap( this );
				if ( ( marker && marker.anchor === 'tag' ) || $(this).is( 'p' ) ) {
					// Remove all classes
					$(this).removeAttr( 'class' );
				} else {
					// Assume anchor == 'wrap'
					$(this).replaceWith( this.childNodes );
				}
				context.fn.purgeOffsets();
			});

		}
	}
};

}( jQuery ) );

