/*******************************************************************************
 * Copyright (c) 2000, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Pierre-Yves B., pyvesdev@gmail.com - Bug 121634: [find/replace] status bar must show the string being searched when "String Not Found"
 *******************************************************************************/

package org.eclipse.ui.texteditor;


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ResourceBundle;
import java.util.regex.PatternSyntaxException;

import org.osgi.framework.FrameworkUtil;

import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.dialogs.IDialogSettings;

import org.eclipse.jface.text.FindReplaceDocumentAdapter;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension3;
import org.eclipse.jface.text.TextUtilities;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.texteditor.NLSUtility;


/**
 * An action which finds the next/previous occurrence of the last search or the
 * current selection if present.
 * <p>
 * This class may be instantiated; it is not intended to be subclassed.
 * </p>
 *
 * @since 2.0
 * @noextend This class is not intended to be subclassed by clients.
 */
public class FindNextAction extends ResourceAction implements IUpdate {

	/** The action's target */
	private IFindReplaceTarget fTarget;
	/** The part the action is bound to */
	private IWorkbenchPart fWorkbenchPart;
	/** The workbench window */
	private IWorkbenchWindow fWorkbenchWindow;
	/** The dialog settings to retrieve the last search */
	private IDialogSettings fDialogSettings;
	/** The find history as initially given in the dialog settings. */
	private List<String> fFindHistory= new ArrayList<>();
	/** The find string as initially given in the dialog settings. */
	private String fFindString;
	/** The search direction as initially given in the dialog settings. */
	private boolean fForward;
	/** The wrapping flag as initially given in the dialog settings. */
	private boolean fWrapInit;
	/** The case flag as initially given in the dialog settings. */
	private boolean fCaseInit;
	/** The whole word flag as initially given in the dialog settings. */
	private boolean fWholeWordInit;
	/**
	 * The regExSearch flag as initially given in the dialog settings.
	 *
	 * @since 3.0
	 */
	private boolean fRegExSearch;
	/**
	 * The last selection set by find/replace.
	 *
	 * @since 3.0
	 */
	private String fSelection;

	/**
	 * Creates a new find/replace action for the given workbench part.
	 * The action configures its visual representation from the given
	 * resource bundle.
	 *
	 * @param bundle the resource bundle
	 * @param prefix a prefix to be prepended to the various resource keys
	 *   (described in <code>ResourceAction</code> constructor), or
	 *   <code>null</code> if none
	 * @param workbenchPart the workbench part
	 * @param forward the search direction
	 * @see ResourceAction#ResourceAction(ResourceBundle, String)
	 */
	public FindNextAction(ResourceBundle bundle, String prefix, IWorkbenchPart workbenchPart, boolean forward) {
		super(bundle, prefix);
		fWorkbenchPart= workbenchPart;
		fForward= forward;
		update();
	}

	/**
	 * Creates a new find/replace action for the given workbench window.
	 * The action configures its visual representation from the given
	 * resource bundle.
	 *
	 * @param bundle the resource bundle
	 * @param prefix a prefix to be prepended to the various resource keys
	 *   (described in <code>ResourceAction</code> constructor), or
	 *   <code>null</code> if none
	 * @param workbenchWindow the workbench window
	 * @param forward the search direction
	 * @see ResourceAction#ResourceAction(ResourceBundle, String)
	 *
	 * @deprecated use FindReplaceAction(ResourceBundle, String, IWorkbenchPart, boolean) instead
	 */
	@Deprecated
	public FindNextAction(ResourceBundle bundle, String prefix, IWorkbenchWindow workbenchWindow, boolean forward) {
		super(bundle, prefix);
		fWorkbenchWindow= workbenchWindow;
		fForward= forward;
		update();
	}

	/**
	 * Returns the find string based on the selection or the find history.
	 * @return the find string
	 */
	private String getFindString() {
		String fullSelection= fTarget.getSelectionText();
		String firstLine= getFirstLine(fullSelection);
		if ((firstLine.isEmpty() || fRegExSearch && fullSelection.equals(fSelection)) && !fFindHistory.isEmpty())
			return fFindHistory.get(0);
		else if (fRegExSearch && !fullSelection.isEmpty())
			return FindReplaceDocumentAdapter.escapeForRegExPattern(fullSelection);
		else
			return firstLine;
	}

	/**
	 * Returns the status line manager of the active editor.
	 * @return the status line manager of the active editor
	 */
	private IStatusLineManager getStatusLineManager() {
		IEditorPart editor= fWorkbenchPart.getSite().getPage().getActiveEditor();
		if (editor == null)
			return null;

		return editor.getEditorSite().getActionBars().getStatusLineManager();
	}

	/**
	 * Sets the "no matches found" error message to the status line.
	 *
	 * @since 3.0
	 */
	private void statusNotFound() {
		fWorkbenchPart.getSite().getShell().getDisplay().beep();

		IStatusLineManager manager= getStatusLineManager();
		if (manager == null)
			return;

		String msg= NLSUtility.format(EditorMessages.FindNext_Status_noMatch_label, fFindString);
		manager.setMessage(msg);
	}

	/**
	 * Clears the status line.
	 */
	private void statusClear() {
		IStatusLineManager manager= getStatusLineManager();
		if (manager == null)
			return;

		manager.setErrorMessage(""); //$NON-NLS-1$
		manager.setMessage(""); //$NON-NLS-1$
	}

	@Override
	public void run() {
		if (fTarget != null) {
			readConfiguration();

			fFindString= getFindString();
			if (fFindString == null) {
				statusNotFound();
				return;
			}

			boolean wholeWord= fWholeWordInit && !fRegExSearch && isWord(fFindString);

			statusClear();
			if (!findNext(fFindString, fForward, fCaseInit, fWrapInit, wholeWord, fRegExSearch))
				statusNotFound();

			writeConfiguration();
		}
	}

	/**
	 * Tests whether each character in the given string is a letter.
	 *
	 * @param str the string to check
	 * @return <code>true</code> if the given string is a word
	 * @since 3.2
	 */
	private boolean isWord(String str) {
		if (str == null || str.isEmpty())
			return false;

		for (int i= 0; i < str.length(); i++) {
			if (!Character.isJavaIdentifierPart(str.charAt(i)))
				return false;
		}
		return true;
	}

	@Override
	public void update() {

		if (fWorkbenchPart == null && fWorkbenchWindow != null)
			fWorkbenchPart= fWorkbenchWindow.getPartService().getActivePart();

		if (fWorkbenchPart != null)
			fTarget= fWorkbenchPart.getAdapter(IFindReplaceTarget.class);
		else
			fTarget= null;

		setEnabled(fTarget != null && fTarget.canPerformFind());
	}

	/*
	 * @see FindReplaceDialog#findIndex(String, int, boolean, boolean, boolean, boolean)
	 * @since 3.0
	 */
	private int findIndex(String findString, int startPosition, boolean forwardSearch, boolean caseSensitive, boolean wrapSearch, boolean wholeWord, boolean regExSearch) {

		if (forwardSearch) {
			if (wrapSearch) {
				int index= findAndSelect(startPosition, findString, true, caseSensitive, wholeWord, regExSearch);
				if (index == -1) {
					beep();
					index= findAndSelect(-1, findString, true, caseSensitive, wholeWord, regExSearch);
				}
				return index;
			}
			return findAndSelect(startPosition, findString, true, caseSensitive, wholeWord, regExSearch);
		}

		// backward
		if (wrapSearch) {
			int index= findAndSelect(startPosition - 1, findString, false, caseSensitive, wholeWord, regExSearch);
			if (index == -1) {
				beep();
				index= findAndSelect(-1, findString, false, caseSensitive, wholeWord, regExSearch);
			}
			return index;
		}
		return findAndSelect(startPosition - 1, findString, false, caseSensitive, wholeWord, regExSearch);
	}

	/**
	 * Returns whether the specified  search string can be found using the given options.
	 *
	 * @param findString the string to search for
	 * @param forwardSearch the search direction
	 * @param caseSensitive should the search honor cases
	 * @param wrapSearch	should the search wrap to the start/end if end/start reached
	 * @param wholeWord does the find string represent a complete word
	 * @param regExSearch if <code>true</code> findString represents a regular expression
	 * @return <code>true</code> if the find string can be found using the given options
	 * @since 3.0
	 */
	private boolean findNext(String findString, boolean forwardSearch, boolean caseSensitive, boolean wrapSearch, boolean wholeWord, boolean regExSearch) {

		Point r= fTarget.getSelection();
		int findReplacePosition= r.x;
		if (forwardSearch)
			findReplacePosition += r.y;

		int index= findIndex(findString, findReplacePosition, forwardSearch, caseSensitive, wrapSearch, wholeWord, regExSearch);

		if (index != -1)
			return true;

		return false;
	}

	private void beep() {
		Shell shell= null;
		if (fWorkbenchPart != null)
			shell= fWorkbenchPart.getSite().getShell();
		else if (fWorkbenchWindow != null)
			shell= fWorkbenchWindow.getShell();

		if (shell != null && !shell.isDisposed())
			shell.getDisplay().beep();
	}

	/**
	 * Searches for a string starting at the given offset and using the specified search
	 * directives. If a string has been found it is selected and its start offset is
	 * returned.
	 *
	 * @param offset the offset at which searching starts
	 * @param findString the string which should be found
	 * @param forwardSearch the direction of the search
	 * @param caseSensitive <code>true</code> performs a case sensitive search, <code>false</code> an insensitive search
	 * @param wholeWord if <code>true</code> only occurrences are reported in which the findString stands as a word by itself
	 * @param regExSearch if <code>true</code> findString represents a regular expression
	 * @return the position of the specified string, or -1 if the string has not been found
	 * @since 3.0
	 */
	private int findAndSelect(int offset, String findString, boolean forwardSearch, boolean caseSensitive, boolean wholeWord, boolean regExSearch) {
		if (fTarget instanceof IFindReplaceTargetExtension3) {
			try {
				return ((IFindReplaceTargetExtension3)fTarget).findAndSelect(offset, findString, forwardSearch, caseSensitive, wholeWord, regExSearch);
			} catch (PatternSyntaxException ex) {
				return -1;
			}
		}
		return fTarget.findAndSelect(offset, findString, forwardSearch, caseSensitive, wholeWord);
	}

	//--------------- configuration handling --------------

	/**
	 * Returns the dialog settings object used to share state
	 * between several find/replace dialogs.
	 *
	 * @return the dialog settings to be used
	 */
	private IDialogSettings getDialogSettings() {
		IDialogSettings settings = PlatformUI.getDialogSettingsProvider(FrameworkUtil.getBundle(FindNextAction.class))
				.getDialogSettings();
		fDialogSettings= settings.getSection(FindReplaceDialog.class.getName());
		if (fDialogSettings == null)
			fDialogSettings= settings.addNewSection(FindReplaceDialog.class.getName());
		return fDialogSettings;
	}

	/**
	 * Initializes itself from the dialog settings with the same state
	 * as at the previous invocation.
	 */
	private void readConfiguration() {
		IDialogSettings s= getDialogSettings();

		fWrapInit= s.get("wrap") == null || s.getBoolean("wrap"); //$NON-NLS-1$ //$NON-NLS-2$
		fCaseInit= s.getBoolean("casesensitive"); //$NON-NLS-1$
		fWholeWordInit= s.getBoolean("wholeword"); //$NON-NLS-1$
		fRegExSearch= s.getBoolean("isRegEx"); //$NON-NLS-1$
		fSelection= s.get("selection"); //$NON-NLS-1$

		String[] findHistory= s.getArray("findhistory"); //$NON-NLS-1$
		if (findHistory != null) {
			fFindHistory.clear();
			Collections.addAll(fFindHistory, findHistory);
		}
	}

	/**
	 * Stores its current configuration in the dialog store.
	 */
	private void writeConfiguration() {
		if (fFindString == null)
			return;

		IDialogSettings s= getDialogSettings();
		s.put("selection", fTarget.getSelectionText()); //$NON-NLS-1$

		if (!fFindHistory.isEmpty() && fFindString.equals(fFindHistory.get(0)))
			return;

		int index= fFindHistory.indexOf(fFindString);
		if (index != -1)
			fFindHistory.remove(index);
		fFindHistory.add(0, fFindString);

		while (fFindHistory.size() > 8)
			fFindHistory.remove(8);
		String[] names= new String[fFindHistory.size()];
		fFindHistory.toArray(names);
		s.put("findhistory", names); //$NON-NLS-1$
	}

	/**
	 * Returns the first line of the given selection.
	 *
	 * @param selection the selection
	 * @return the first line of the selection
	 */
	private String getFirstLine(String selection) {
		if (!selection.isEmpty()) {
			int delimiterOffset = TextUtilities.nextDelimiter(selection, 0).delimiterIndex;
			if (delimiterOffset > 0)
				return selection.substring(0, delimiterOffset);
		}
		return selection;
	}
}
