//------------------------------------------------------------------------------
// Copyright (c) 2005, 2006 IBM Corporation and others.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// Contributors:
// IBM Corporation - initial implementation
//------------------------------------------------------------------------------
package org.eclipse.epf.richtext.actions;

import org.eclipse.epf.common.utils.XMLUtil;
import org.eclipse.epf.richtext.IRichText;
import org.eclipse.epf.richtext.RichTextCommand;
import org.eclipse.epf.richtext.RichTextEditor;
import org.eclipse.epf.richtext.RichTextImages;
import org.eclipse.epf.richtext.RichTextPlugin;
import org.eclipse.epf.richtext.RichTextResources;
import org.eclipse.epf.richtext.dialogs.FindReplaceDialog;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;

/**
 * Finds and replaces text in a rich text control.
 * 
 * @author Kelvin Low
 * @since 1.0
 */
public class FindReplaceAction extends RichTextAction {

	/**
	 * Finds text sub action.
	 */
	public static final int FIND_TEXT = 1;

	/**
	 * Replaces text sub action.
	 */
	public static final int REPLACE_TEXT = 2;

	/**
	 * Replaces and finds text sub action.
	 */
	public static final int REPLACE_FIND_TEXT = 3;

	/**
	 * Replaces all sub action.
	 */
	public static final int REPLACE_ALL_TEXT = 4;

	/**
	 * Find match in a forward direction.
	 */
	public static final int FORWARD_MATCH = 1;

	/**
	 * Find match in a backward direction.
	 */
	public static final int BACKWARD_MATCH = -1;

	/**
	 * Whole word match.
	 */
	public static final int WHOLE_WORD_MATCH = 2;

	/**
	 * Case sensitive match.
	 */
	public static final int CASE_SENSITIVE_MATCH = 4;

	// Encoded single quote.
	private static final String ENCODED_SINGLE_QUOTE = "%sq%"; //$NON-NLS-1$

	private IRichText richText;

	private StyledText styledText;

	protected boolean foundMatch = false;

	/**
	 * Creates a new instance.
	 */
	public FindReplaceAction() {
		super();
		setImage(RichTextImages.IMG_FIND_REPLACE);
		setDisabledImage(RichTextImages.DISABLED_IMG_FIND_REPLACE);
		setToolTipText(RichTextResources.findReplaceAction_toolTipText); //$NON-NLS-1$
		setEnabled(true);
	}

	/**
	 * Returns <code>true</code> if this action should be disabled when the
	 * rich text editor is in readonly mode.
	 */
	public boolean disableInReadOnlyMode() {
		return false;
	}

	public boolean disableInSourceMode() {
		return false;
	}

	/**
	 * Executes the action.
	 * 
	 * @param richText
	 *            a rich text control
	 */
	public void execute(IRichText richText) {
		this.richText = richText;
		if (richText != null) {
			try {
				if (richText instanceof RichTextEditor
						&& ((RichTextEditor) richText).isHTMLTabSelected()) {
					styledText = ((RichTextEditor) richText).getSourceEdit();
				}
				FindReplaceDialog dialog = new FindReplaceDialog(Display
						.getCurrent().getActiveShell(), this, !richText
						.getEditable());
				dialog.open();
			} catch (Exception e) {
				RichTextPlugin.getDefault().getLogger().logError(e);
			}
		}
	}

	/**
	 * Returns <code>true</code> if a match is found.
	 * 
	 * @return <code>true</code> if a match is found.
	 */
	public boolean getFoundMatch() {
		return foundMatch;
	}

	/**
	 * Executes the action.
	 * 
	 * @param subAction
	 *            the sub action to execute
	 * @param findText
	 *            the find text
	 * @param replaceText
	 *            the replace text
	 * @param matchDir
	 *            the match direction; the value can either be
	 *            <code>FIND_FORWARD</code> or <code>FIND_BACKWARD</code>.
	 * @param matchOptions
	 *            the match options
	 */
	public void run(int subAction, String findText, String replaceText,
			int matchDir, int matchOptions) {
		if (styledText == null) {
			if (findText.indexOf("'") != -1) { //$NON-NLS-1$
				findText = findText.replaceAll("'", ENCODED_SINGLE_QUOTE); //$NON-NLS-1$
			}
			if (replaceText.indexOf("'") != -1) { //$NON-NLS-1$
				replaceText = replaceText.replaceAll("'", ENCODED_SINGLE_QUOTE); //$NON-NLS-1$
			}
		}
		try {
			foundMatch = false;
			int status = 0;
			switch (subAction) {
			case FIND_TEXT:
				status = findText(findText, matchDir, matchOptions);
				break;
			case REPLACE_TEXT:
				status = replaceText(replaceText, matchDir, matchOptions);
				break;
			case REPLACE_FIND_TEXT:
				status = replaceFindText(findText, replaceText, matchDir,
						matchOptions);
				break;
			case REPLACE_ALL_TEXT:
				replaceAll(findText, replaceText, matchOptions);
				break;
			}
			if (status > 0)
				foundMatch = true;
		} catch (Exception e) {
			RichTextPlugin.getDefault().getLogger().logError(e);
		}
	}

	/**
	 * Escapes the given text.
	 * 
	 * @param text
	 *            text to be escaped
	 */
	private static String escape(String text) {
		if (text == null || text.length() == 0)
			return ""; //$NON-NLS-1$
		StringBuffer sb = new StringBuffer();
		int textSize = text.length();
		for (int i = 0; i < textSize; i++) {
			char ch = text.charAt(i);
			switch (ch) {
			case '<':
				sb.append(XMLUtil.XML_LT);
				break;
			case '>':
				sb.append(XMLUtil.XML_GT);
				break;
			case '&':
				sb.append(XMLUtil.XML_AMP);
				break;
			default:
				sb.append(ch);
				break;
			}
		}
		return sb.toString();
	}

	private int findText(String findText, int matchDir, int matchOptions) {
		int status = 0;
		if (styledText != null) {
			status = styledTextFindTextAndSelect(findText, matchDir,
					matchOptions);
		} else {
			status = richText
					.executeCommand(RichTextCommand.FIND_TEXT, new String[] {
							findText, "" + matchDir, "" + matchOptions }); //$NON-NLS-1$ //$NON-NLS-2$				
		}
		return status;
	}

	private int replaceText(String replaceText, int matchDir, int matchOptions) {
		int status = 0;
		if (styledText != null) {
			status = styledTextReplaceTextAndSelect(replaceText);
		} else {
			status = richText.executeCommand(RichTextCommand.REPLACE_TEXT,
					new String[] { replaceText,
							"" + matchDir, "" + matchOptions }); //$NON-NLS-1$ //$NON-NLS-2$					
		}
		return status;
	}

	private int replaceFindText(String findText, String replaceText,
			int matchDir, int matchOptions) {
		int status = 0;
		if (styledText != null) {
			styledTextReplaceTextAndSelect(replaceText);
			status = styledTextFindTextAndSelect(findText, matchDir,
					matchOptions);
		} else {
			richText.executeCommand(RichTextCommand.REPLACE_TEXT, new String[] {
					replaceText, "" + matchDir, "" + matchOptions }); //$NON-NLS-1$ //$NON-NLS-2$
			status = richText
					.executeCommand(RichTextCommand.FIND_TEXT, new String[] {
							findText, "" + matchDir, "" + matchOptions }); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return status;
	}

	private void replaceAll(String findText, String replaceText,
			int matchOptions) {
		if (styledText != null) {
			styledTextReplaceAll(findText, replaceText, matchOptions);
		} else {
			richText.executeCommand(RichTextCommand.REPLACE_ALL_TEXT,
					new String[] { escape(findText), escape(replaceText),
							"" + matchOptions }); //$NON-NLS-1$
		}
	}

	private int styledTextFindTextAndSelect(String findText, int matchDir,
			int matchOptions) {
		Point selectionOffset = styledText.getSelectionRange();
		int firstSelectedOffset = selectionOffset.x;
		int lastSelectedOffset = selectionOffset.x + selectionOffset.y - 1;
		String htmlText = styledText.getText();
		int indexOfMatch = -1;
		if ((matchOptions & CASE_SENSITIVE_MATCH) == 0) {
			// TODO: use toUpperCase(Locale) once library has locale attribute
			htmlText = htmlText.toUpperCase();
			findText = findText.toUpperCase();
		}
		do {
			if (indexOfMatch != -1) {
				lastSelectedOffset = indexOfMatch + 1;
				firstSelectedOffset = indexOfMatch - 1;
			}
			if (matchDir == FORWARD_MATCH) {
				indexOfMatch = htmlText.indexOf(findText,
						lastSelectedOffset + 1);
			} else {
				indexOfMatch = htmlText.lastIndexOf(findText,
						firstSelectedOffset - 1);
			}
		} while (indexOfMatch != -1
				&& ((matchOptions & WHOLE_WORD_MATCH) == WHOLE_WORD_MATCH)
				&& isPartOfWord(htmlText, indexOfMatch, findText.length()));
		if (indexOfMatch != -1) {
			styledText.setSelectionRange(indexOfMatch, findText.length());
			styledTextScrollToPosition(indexOfMatch);
		} else {
			String selectedText = styledText.getSelectionText();
			if ((matchOptions & CASE_SENSITIVE_MATCH) == 0) {
				selectedText = selectedText.toUpperCase();
			}
			if (selectedText.equals(findText)) {
				indexOfMatch = styledText.getSelectionRange().x;
			}
		}
		return indexOfMatch;

	}

	private int styledTextReplaceTextAndSelect(String replaceText) {
		Point selectionOffset = styledText.getSelectionRange();
		styledText.replaceTextRange(selectionOffset.x, selectionOffset.y,
				replaceText);
		styledText.setSelectionRange(selectionOffset.x, replaceText.length());

		return 1;
	}

	private void styledTextReplaceAll(String findText, String replaceText,
			int matchOptions) {
		styledText.setSelectionRange(0, 0);
		while (styledTextFindTextAndSelect(findText, FORWARD_MATCH,
				matchOptions) != -1) {
			styledTextReplaceTextAndSelect(replaceText);
		}
	}

	private boolean isWordChar(char c) {
		if (Character.isLetterOrDigit(c))
			return true;
		return false;
	}

	private boolean isPartOfWord(String text, int index, int length) {
		if (index > 0)
			if (isWordChar(text.charAt(index - 1)))
				return true;
		if (text.length() >= index + length)
			if (isWordChar(text.charAt(index + length)))
				return true;
		return false;
	}

	private void styledTextScrollToPosition(int index) {
		int selectionStartLine = styledText.getLineAtOffset(index);
		int numLinesShown;
		if (styledText.getLineHeight() != 0) {
			numLinesShown = styledText.getClientArea().height
					/ styledText.getLineHeight();
		} else {
			numLinesShown = 1;
		}
		styledText.setTopIndex(selectionStartLine - (numLinesShown / 2));
	}

}
