/**
 * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.html or http://ckeditor.com/license
 */

(function() {
	var isReplace;

	function findEvaluator( node ) {
		return node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 && ( !isReplace || !node.isReadOnly() );
	}

	// Elements which break characters been considered as sequence.
	function nonCharactersBoundary( node ) {
		return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$empty, CKEDITOR.dtd.$nonEditable ) ) );
	}

	// Get the cursor object which represent both current character and it's dom
	// position thing.
	var cursorStep = function() {
			return {
				textNode: this.textNode,
				offset: this.offset,
				character: this.textNode ? this.textNode.getText().charAt( this.offset ) : null,
				hitMatchBoundary: this._.matchBoundary
			};
		};

	var pages = [ 'find', 'replace' ],
		fieldsMapping = [
			[ 'txtFindFind', 'txtFindReplace' ],
			[ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
			[ 'txtFindWordChk', 'txtReplaceWordChk' ],
			[ 'txtFindCyclic', 'txtReplaceCyclic' ] ];

	// Synchronize corresponding filed values between 'replace' and 'find' pages.
	// @param {String} currentPageId	The page id which receive values.
	function syncFieldsBetweenTabs( currentPageId ) {
		var sourceIndex, targetIndex, sourceField, targetField;

		sourceIndex = currentPageId === 'find' ? 1 : 0;
		targetIndex = 1 - sourceIndex;
		var i,
			l = fieldsMapping.length;
		for ( i = 0; i < l; i++ ) {
			sourceField = this.getContentElement( pages[ sourceIndex ], fieldsMapping[ i ][ sourceIndex ] );
			targetField = this.getContentElement( pages[ targetIndex ], fieldsMapping[ i ][ targetIndex ] );

			targetField.setValue( sourceField.getValue() );
		}
	}

	var findDialog = function( editor, startupPage ) {
			// Style object for highlights: (#5018)
			// 1. Defined as full match style to avoid compromising ordinary text color styles.
			// 2. Must be apply onto inner-most text to avoid conflicting with ordinary text color styles visually.
			var highlightStyle = new CKEDITOR.style( CKEDITOR.tools.extend({
				attributes: { 'data-cke-highlight':1 },
				fullMatch: 1, ignoreReadonly: 1, childRule: function() {
					return 0;
				} }, editor.config.find_highlight, true ) );

			// Iterator which walk through the specified range char by char. By
			// default the walking will not stop at the character boundaries, until
			// the end of the range is encountered.
			// @param { CKEDITOR.dom.range } range
			// @param {Boolean} matchWord Whether the walking will stop at character boundary.
			var characterWalker = function( range, matchWord ) {
					var self = this;
					var walker = new CKEDITOR.dom.walker( range );
					walker.guard = matchWord ? nonCharactersBoundary : function( node ) {
						!nonCharactersBoundary( node ) && ( self._.matchBoundary = true );
					};
					walker[ 'evaluator' ] = findEvaluator;
					walker.breakOnFalse = 1;

					if ( range.startContainer.type == CKEDITOR.NODE_TEXT ) {
						this.textNode = range.startContainer;
						this.offset = range.startOffset - 1;
					}

					this._ = {
						matchWord: matchWord,
						walker: walker,
						matchBoundary: false
					};
				};

			characterWalker.prototype = {
				next: function() {
					return this.move();
				},

				back: function() {
					return this.move( true );
				},

				move: function( rtl ) {
					var currentTextNode = this.textNode;
					// Already at the end of document, no more character available.
					if ( currentTextNode === null )
						return cursorStep.call( this );

					this._.matchBoundary = false;

					// There are more characters in the text node, step forward.
					if ( currentTextNode && rtl && this.offset > 0 ) {
						this.offset--;
						return cursorStep.call( this );
					} else if ( currentTextNode && this.offset < currentTextNode.getLength() - 1 ) {
						this.offset++;
						return cursorStep.call( this );
					} else {
						currentTextNode = null;
						// At the end of the text node, walking foward for the next.
						while ( !currentTextNode ) {
							currentTextNode = this._.walker[ rtl ? 'previous' : 'next' ].call( this._.walker );

							// Stop searching if we're need full word match OR
							// already reach document end.
							if ( this._.matchWord && !currentTextNode || this._.walker._.end )
								break;
						}
						// Found a fresh text node.
						this.textNode = currentTextNode;
						if ( currentTextNode )
							this.offset = rtl ? currentTextNode.getLength() - 1 : 0;
						else
							this.offset = 0;
					}

					return cursorStep.call( this );
				}

			};

			/**
			 * A range of cursors which represent a trunk of characters which try to
			 * match, it has the same length as the pattern  string.
			 *
			 * **Note:** This class isn't accessible from global scope.
			 *
			 * @private
			 * @class CKEDITOR.plugins.find.characterRange
			 * @constructor Creates a characterRange class instance.
			 */
			var characterRange = function( characterWalker, rangeLength ) {
					this._ = {
						walker: characterWalker,
						cursors: [],
						rangeLength: rangeLength,
						highlightRange: null,
						isMatched: 0
					};
				};

			characterRange.prototype = {
				/**
				 * Translate this range to {@link CKEDITOR.dom.range}.
				 */
				toDomRange: function() {
					var range = editor.createRange();
					var cursors = this._.cursors;
					if ( cursors.length < 1 ) {
						var textNode = this._.walker.textNode;
						if ( textNode )
							range.setStartAfter( textNode );
						else
							return null;
					} else {
						var first = cursors[ 0 ],
							last = cursors[ cursors.length - 1 ];

						range.setStart( first.textNode, first.offset );
						range.setEnd( last.textNode, last.offset + 1 );
					}

					return range;
				},

				/**
				 * Reflect the latest changes from dom range.
				 */
				updateFromDomRange: function( domRange ) {
					var cursor,
						walker = new characterWalker( domRange );
					this._.cursors = [];
					do {
						cursor = walker.next();
						if ( cursor.character ) this._.cursors.push( cursor );
					}
					while ( cursor.character );
					this._.rangeLength = this._.cursors.length;
				},

				setMatched: function() {
					this._.isMatched = true;
				},

				clearMatched: function() {
					this._.isMatched = false;
				},

				isMatched: function() {
					return this._.isMatched;
				},

				/**
				 * Hightlight the current matched chunk of text.
				 */
				highlight: function() {
					// Do not apply if nothing is found.
					if ( this._.cursors.length < 1 )
						return;

					// Remove the previous highlight if there's one.
					if ( this._.highlightRange )
						this.removeHighlight();

					// Apply the highlight.
					var range = this.toDomRange(),
						bookmark = range.createBookmark();
					highlightStyle.applyToRange( range );
					range.moveToBookmark( bookmark );
					this._.highlightRange = range;

					// Scroll the editor to the highlighted area.
					var element = range.startContainer;
					if ( element.type != CKEDITOR.NODE_ELEMENT )
						element = element.getParent();
					element.scrollIntoView();

					// Update the character cursors.
					this.updateFromDomRange( range );
				},

				/**
				 * Remove highlighted find result.
				 */
				removeHighlight: function() {
					if ( !this._.highlightRange )
						return;

					var bookmark = this._.highlightRange.createBookmark();
					highlightStyle.removeFromRange( this._.highlightRange );
					this._.highlightRange.moveToBookmark( bookmark );
					this.updateFromDomRange( this._.highlightRange );
					this._.highlightRange = null;
				},

				isReadOnly: function() {
					if ( !this._.highlightRange )
						return 0;

					return this._.highlightRange.startContainer.isReadOnly();
				},

				moveBack: function() {
					var retval = this._.walker.back(),
						cursors = this._.cursors;

					if ( retval.hitMatchBoundary )
						this._.cursors = cursors = [];

					cursors.unshift( retval );
					if ( cursors.length > this._.rangeLength )
						cursors.pop();

					return retval;
				},

				moveNext: function() {
					var retval = this._.walker.next(),
						cursors = this._.cursors;

					// Clear the cursors queue if we've crossed a match boundary.
					if ( retval.hitMatchBoundary )
						this._.cursors = cursors = [];

					cursors.push( retval );
					if ( cursors.length > this._.rangeLength )
						cursors.shift();

					return retval;
				},

				getEndCharacter: function() {
					var cursors = this._.cursors;
					if ( cursors.length < 1 )
						return null;

					return cursors[ cursors.length - 1 ].character;
				},

				getNextCharacterRange: function( maxLength ) {
					var lastCursor, nextRangeWalker,
						cursors = this._.cursors;

					if ( ( lastCursor = cursors[ cursors.length - 1 ] ) && lastCursor.textNode )
						nextRangeWalker = new characterWalker( getRangeAfterCursor( lastCursor ) );
					// In case it's an empty range (no cursors), figure out next range from walker (#4951).
					else
						nextRangeWalker = this._.walker;

					return new characterRange( nextRangeWalker, maxLength );
				},

				getCursors: function() {
					return this._.cursors;
				}
			};


			// The remaining document range after the character cursor.
			function getRangeAfterCursor( cursor, inclusive ) {
				var range = editor.createRange();
				range.setStart( cursor.textNode, ( inclusive ? cursor.offset : cursor.offset + 1 ) );
				range.setEndAt( editor.editable(), CKEDITOR.POSITION_BEFORE_END );
				return range;
			}

			// The document range before the character cursor.
			function getRangeBeforeCursor( cursor ) {
				var range = editor.createRange();
				range.setStartAt( editor.editable(), CKEDITOR.POSITION_AFTER_START );
				range.setEnd( cursor.textNode, cursor.offset );
				return range;
			}

			var KMP_NOMATCH = 0,
				KMP_ADVANCED = 1,
				KMP_MATCHED = 2;

			// Examination the occurrence of a word which implement KMP algorithm.
			var kmpMatcher = function( pattern, ignoreCase ) {
					var overlap = [ -1 ];
					if ( ignoreCase )
						pattern = pattern.toLowerCase();
					for ( var i = 0; i < pattern.length; i++ ) {
						overlap.push( overlap[ i ] + 1 );
						while ( overlap[ i + 1 ] > 0 && pattern.charAt( i ) != pattern.charAt( overlap[ i + 1 ] - 1 ) )
							overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1;
					}

					this._ = {
						overlap: overlap,
						state: 0,
						ignoreCase: !!ignoreCase,
						pattern: pattern
					};
				};

			kmpMatcher.prototype = {
				feedCharacter: function( c ) {
					if ( this._.ignoreCase )
						c = c.toLowerCase();

					while ( true ) {
						if ( c == this._.pattern.charAt( this._.state ) ) {
							this._.state++;
							if ( this._.state == this._.pattern.length ) {
								this._.state = 0;
								return KMP_MATCHED;
							}
							return KMP_ADVANCED;
						} else if ( !this._.state )
							return KMP_NOMATCH;
						else
							this._.state = this._.overlap[ this._.state ];
					}

					return null;
				},

				reset: function() {
					this._.state = 0;
				}
			};

			var wordSeparatorRegex = /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;

			var isWordSeparator = function( c ) {
					if ( !c )
						return true;
					var code = c.charCodeAt( 0 );
					return ( code >= 9 && code <= 0xd ) || ( code >= 0x2000 && code <= 0x200a ) || wordSeparatorRegex.test( c );
				};

			var finder = {
				searchRange: null,
				matchRange: null,
				find: function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun ) {
					if ( !this.matchRange )
						this.matchRange = new characterRange( new characterWalker( this.searchRange ), pattern.length );
					else {
						this.matchRange.removeHighlight();
						this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
					}

					var matcher = new kmpMatcher( pattern, !matchCase ),
						matchState = KMP_NOMATCH,
						character = '%';

					while ( character !== null ) {
						this.matchRange.moveNext();
						while ( ( character = this.matchRange.getEndCharacter() ) ) {
							matchState = matcher.feedCharacter( character );
							if ( matchState == KMP_MATCHED )
								break;
							if ( this.matchRange.moveNext().hitMatchBoundary )
								matcher.reset();
						}

						if ( matchState == KMP_MATCHED ) {
							if ( matchWord ) {
								var cursors = this.matchRange.getCursors(),
									tail = cursors[ cursors.length - 1 ],
									head = cursors[ 0 ];

								var rangeBefore = getRangeBeforeCursor( head ),
									rangeAfter = getRangeAfterCursor( tail );

								// The word boundary checks requires to trim the text nodes. (#9036)
								rangeBefore.trim();
								rangeAfter.trim();

								var headWalker = new characterWalker( rangeBefore, true ),
									tailWalker = new characterWalker( rangeAfter, true );

								if ( !( isWordSeparator( headWalker.back().character ) && isWordSeparator( tailWalker.next().character ) ) )
									continue;
							}
							this.matchRange.setMatched();
							if ( highlightMatched !== false )
								this.matchRange.highlight();
							return true;
						}
					}

					this.matchRange.clearMatched();
					this.matchRange.removeHighlight();
					// Clear current session and restart with the default search
					// range.
					// Re-run the finding once for cyclic.(#3517)
					if ( matchCyclic && !cyclicRerun ) {
						this.searchRange = getSearchRange( 1 );
						this.matchRange = null;
						return arguments.callee.apply( this, Array.prototype.slice.call( arguments ).concat( [ true ] ) );
					}

					return false;
				},

				// Record how much replacement occurred toward one replacing.
				replaceCounter: 0,

				replace: function( dialog, pattern, newString, matchCase, matchWord, matchCyclic, isReplaceAll ) {
					isReplace = 1;

					// Successiveness of current replace/find.
					var result = 0;

					// 1. Perform the replace when there's already a match here.
					// 2. Otherwise perform the find but don't replace it immediately.
					if ( this.matchRange && this.matchRange.isMatched() && !this.matchRange._.isReplaced && !this.matchRange.isReadOnly() ) {
						// Turn off highlight for a while when saving snapshots.
						this.matchRange.removeHighlight();
						var domRange = this.matchRange.toDomRange();
						var text = editor.document.createText( newString );
						if ( !isReplaceAll ) {
							// Save undo snaps before and after the replacement.
							var selection = editor.getSelection();
							selection.selectRanges( [ domRange ] );
							editor.fire( 'saveSnapshot' );
						}
						domRange.deleteContents();
						domRange.insertNode( text );
						if ( !isReplaceAll ) {
							selection.selectRanges( [ domRange ] );
							editor.fire( 'saveSnapshot' );
						}
						this.matchRange.updateFromDomRange( domRange );
						if ( !isReplaceAll )
							this.matchRange.highlight();
						this.matchRange._.isReplaced = true;
						this.replaceCounter++;
						result = 1;
					} else
						result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );

					isReplace = 0;

					return result;
				}
			};

			// The range in which find/replace happened, receive from user
			// selection prior.
			function getSearchRange( isDefault ) {
				var searchRange,
					sel = editor.getSelection(),
					editable = editor.editable();

				if ( sel && !isDefault ) {
					searchRange = sel.getRanges()[ 0 ].clone();
					searchRange.collapse( true );
				} else {
					searchRange = editor.createRange();
					searchRange.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
				}
				searchRange.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
				return searchRange;
			}

			var lang = editor.lang.find;
			return {
				title: lang.title,
				resizable: CKEDITOR.DIALOG_RESIZE_NONE,
				minWidth: 350,
				minHeight: 170,
				buttons: [ CKEDITOR.dialog.cancelButton ], // Cancel button only.
				contents: [
					{
					id: 'find',
					label: lang.find,
					title: lang.find,
					accessKey: '',
					elements: [
						{
						type: 'hbox',
						widths: [ '230px', '90px' ],
						children: [
							{
							type: 'text',
							id: 'txtFindFind',
							label: lang.findWhat,
							isChanged: false,
							labelLayout: 'horizontal',
							accessKey: 'F'
						},
							{
							type: 'button',
							id: 'btnFind',
							align: 'left',
							style: 'width:100%',
							label: lang.find,
							onClick: function() {
								var dialog = this.getDialog();
								if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ), dialog.getValueOf( 'find', 'txtFindCaseChk' ), dialog.getValueOf( 'find', 'txtFindWordChk' ), dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
									alert( lang.notFoundMsg );
							}
						}
						]
					},
						{
						type: 'fieldset',
						label: CKEDITOR.tools.htmlEncode( lang.findOptions ),
						style: 'margin-top:29px',
						children: [
							{
							type: 'vbox',
							padding: 0,
							children: [
								{
								type: 'checkbox',
								id: 'txtFindCaseChk',
								isChanged: false,
								label: lang.matchCase
							},
								{
								type: 'checkbox',
								id: 'txtFindWordChk',
								isChanged: false,
								label: lang.matchWord
							},
								{
								type: 'checkbox',
								id: 'txtFindCyclic',
								isChanged: false,
								'default': true,
								label: lang.matchCyclic
							}
							]
						}
						]
					}
					]
				},
					{
					id: 'replace',
					label: lang.replace,
					accessKey: 'M',
					elements: [
						{
						type: 'hbox',
						widths: [ '230px', '90px' ],
						children: [
							{
							type: 'text',
							id: 'txtFindReplace',
							label: lang.findWhat,
							isChanged: false,
							labelLayout: 'horizontal',
							accessKey: 'F'
						},
							{
							type: 'button',
							id: 'btnFindReplace',
							align: 'left',
							style: 'width:100%',
							label: lang.replace,
							onClick: function() {
								var dialog = this.getDialog();
								if ( !finder.replace( dialog, dialog.getValueOf( 'replace', 'txtFindReplace' ), dialog.getValueOf( 'replace', 'txtReplace' ), dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ), dialog.getValueOf( 'replace', 'txtReplaceWordChk' ), dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
									alert( lang.notFoundMsg );
							}
						}
						]
					},
						{
						type: 'hbox',
						widths: [ '230px', '90px' ],
						children: [
							{
							type: 'text',
							id: 'txtReplace',
							label: lang.replaceWith,
							isChanged: false,
							labelLayout: 'horizontal',
							accessKey: 'R'
						},
							{
							type: 'button',
							id: 'btnReplaceAll',
							align: 'left',
							style: 'width:100%',
							label: lang.replaceAll,
							isChanged: false,
							onClick: function() {
								var dialog = this.getDialog();
								var replaceNums;

								finder.replaceCounter = 0;

								// Scope to full document.
								finder.searchRange = getSearchRange( 1 );
								if ( finder.matchRange ) {
									finder.matchRange.removeHighlight();
									finder.matchRange = null;
								}
								editor.fire( 'saveSnapshot' );
								while ( finder.replace( dialog, dialog.getValueOf( 'replace', 'txtFindReplace' ), dialog.getValueOf( 'replace', 'txtReplace' ), dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ), dialog.getValueOf( 'replace', 'txtReplaceWordChk' ), false, true ) ) {
									/*jsl:pass*/
								}

								if ( finder.replaceCounter ) {
									alert( lang.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
									editor.fire( 'saveSnapshot' );
								} else
									alert( lang.notFoundMsg );
							}
						}
						]
					},
						{
						type: 'fieldset',
						label: CKEDITOR.tools.htmlEncode( lang.findOptions ),
						children: [
							{
							type: 'vbox',
							padding: 0,
							children: [
								{
								type: 'checkbox',
								id: 'txtReplaceCaseChk',
								isChanged: false,
								label: lang.matchCase
							},
								{
								type: 'checkbox',
								id: 'txtReplaceWordChk',
								isChanged: false,
								label: lang.matchWord
							},
								{
								type: 'checkbox',
								id: 'txtReplaceCyclic',
								isChanged: false,
								'default': true,
								label: lang.matchCyclic
							}
							]
						}
						]
					}
					]
				}
				],
				onLoad: function() {
					var dialog = this;

					// Keep track of the current pattern field in use.
					var patternField, wholeWordChkField;

					// Ignore initial page select on dialog show
					var isUserSelect = 0;
					this.on( 'hide', function() {
						isUserSelect = 0;
					});
					this.on( 'show', function() {
						isUserSelect = 1;
					});

					this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc ) {
						return function( pageId ) {
							originalFunc.call( dialog, pageId );

							var currPage = dialog._.tabs[ pageId ];
							var patternFieldInput, patternFieldId, wholeWordChkFieldId;
							patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
							wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';

							patternField = dialog.getContentElement( pageId, patternFieldId );
							wholeWordChkField = dialog.getContentElement( pageId, wholeWordChkFieldId );

							// Prepare for check pattern text filed 'keyup' event
							if ( !currPage.initialized ) {
								patternFieldInput = CKEDITOR.document.getById( patternField._.inputId );
								currPage.initialized = true;
							}

							// Synchronize fields on tab switch.
							if ( isUserSelect )
								syncFieldsBetweenTabs.call( this, pageId );
						};
					});

				},
				onShow: function() {
					// Establish initial searching start position.
					finder.searchRange = getSearchRange();

					// Fill in the find field with selected text.
					var selectedText = this.getParentEditor().getSelection().getSelectedText(),
						patternFieldId = ( startupPage == 'find' ? 'txtFindFind' : 'txtFindReplace' );

					var field = this.getContentElement( startupPage, patternFieldId );
					field.setValue( selectedText );
					field.select();

					this.selectPage( startupPage );

					this[ ( startupPage == 'find' && this._.editor.readOnly ? 'hide' : 'show' ) + 'Page' ]( 'replace' );
				},
				onHide: function() {
					var range;
					if ( finder.matchRange && finder.matchRange.isMatched() ) {
						finder.matchRange.removeHighlight();
						editor.focus();

						range = finder.matchRange.toDomRange();
						if ( range )
							editor.getSelection().selectRanges( [ range ] );
					}

					// Clear current session before dialog close
					delete finder.matchRange;
				},
				onFocus: function() {
					if ( startupPage == 'replace' )
						return this.getContentElement( 'replace', 'txtFindReplace' );
					else
						return this.getContentElement( 'find', 'txtFindFind' );
				}
			};
		};

	CKEDITOR.dialog.add( 'find', function( editor ) {
		return findDialog( editor, 'find' );
	});

	CKEDITOR.dialog.add( 'replace', function( editor ) {
		return findDialog( editor, 'replace' );
	});
})();
