/*******************************************************************************
 * Copyright (c) 2011 Tasktop Technologies.
 * 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:
 *     David Green - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.wikitext.ui.editor;

import java.util.Stack;

import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IExecutionListener;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.texteditor.IWorkbenchActionDefinitionIds;

/**
 * Implementation based on {@link org.eclipse.ui.texteditor.IncrementalFindTarget}
 * 
 * @author David Green
 */
public class FindAndReplaceTarget implements IFindReplaceTarget, IFindReplaceTargetExtension, VerifyKeyListener,
		MouseListener, FocusListener, ISelectionChangedListener, ITextListener, IExecutionListener {

	private final ITextViewer textViewer;

	private final IFindReplaceTarget findReplaceTarget;

	private final IFindReplaceTargetExtension findReplaceTargetExtension;

	private boolean installed;

	private boolean searching;

	private final Stack<Result> state = new Stack<Result>();

	private String findString = ""; //$NON-NLS-1$

	private int index = 0;

	private boolean findHit = true;

	private String previousFindString = ""; //$NON-NLS-1$

	private static final class Result {

		private final Point selection;

		private final String findString;

		private final int index;

		private final boolean findHit;

		public Result(Point selection, String findString, int index, boolean findHit) {
			this.findString = findString;
			this.index = index;
			this.selection = new Point(selection.x, selection.y);
			this.findHit = findHit;
		}

		@Override
		public String toString() {
			return "Result [selection=" + selection + ", findString=" + findString + ", index=" + index + ", findHit=" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
					+ findHit + "]"; //$NON-NLS-1$
		}

	}

	public FindAndReplaceTarget(ITextViewer textViewer) {
		this.textViewer = textViewer;
		findReplaceTarget = textViewer.getFindReplaceTarget();
		if (findReplaceTarget instanceof IFindReplaceTargetExtension) {
			findReplaceTargetExtension = (IFindReplaceTargetExtension) findReplaceTarget;
		} else {
			findReplaceTargetExtension = null;
		}
	}

	public boolean canPerformFind() {
		return findReplaceTarget.canPerformFind();
	}

	public int findAndSelect(int widgetOffset, String findString, boolean searchForward, boolean caseSensitive,
			boolean wholeWord) {
		return findReplaceTarget.findAndSelect(widgetOffset, findString, searchForward, caseSensitive, wholeWord);
	}

	public Point getSelection() {
		return findReplaceTarget.getSelection();
	}

	public String getSelectionText() {
		return findReplaceTarget.getSelectionText();
	}

	public boolean isEditable() {
		return findReplaceTarget.isEditable();
	}

	public void replaceSelection(String text) {
		findReplaceTarget.replaceSelection(text);
	}

	public void beginSession() {
		if (installed) {
			next();
			return;
		}
		clearState();

		index = 0;
		findHit = true;
		findString = ""; //$NON-NLS-1$

		StyledText textWidget = textViewer.getTextWidget();
		if (textWidget != null && !textWidget.isDisposed()) {
			index = textWidget.getCaretOffset();
			textWidget.setSelection(index);
		}

		install();

		if (findReplaceTargetExtension != null) {
			findReplaceTargetExtension.beginSession();
		}
	}

	private void next() {
		if (findHit) {
			saveState();
		}
		repeatSearch();
	}

	private void saveState() {
		Result newState = new Result(new Point(index, index + findString.length()), findString, index, findHit);

		state.push(newState);
	}

	private void clearState() {
		state.clear();
	}

	private void restoreState() {
		StyledText textWidget = textViewer.getTextWidget();
		if (textWidget == null || textWidget.isDisposed()) {
			return;
		}

		Result result = null;
		if (!state.isEmpty()) {
			result = state.pop();
		}

		if (result == null) {
			textWidget.getDisplay().beep();
			return;
		}

		findString = result.findString;
		index = result.index;
		findHit = result.findHit;

		textWidget.setSelection(result.selection);
		textWidget.showSelection();
	}

	private void addSearchCharacter(char character) {
		findString += character;

		doFind();
	}

	private void repeatSearch() {
		if (findString.length() == 0) {
			findString = previousFindString;
		}
		if (findString.length() == 0) {
			findHit = true;
			return;
		}
		// check for wrap search
		if (!findHit) {
			index = 0;
		} else {
			StyledText textWidget = textViewer.getTextWidget();
			index = textWidget.getCaretOffset();
		}
		doFind();
	}

	private void doFind() {

		boolean hasUpperCase = false;
		for (int x = 0; x < findString.length(); ++x) {
			char c = findString.charAt(x);
			if (Character.isUpperCase(c) && Character.toLowerCase(c) != c) {
				hasUpperCase = true;
				break;
			}
		}

		searching = true;

		StyledText textWidget = textViewer.getTextWidget();

		textWidget.setRedraw(false);

		int foundIndex = findReplaceTarget.findAndSelect(index, findString, true, hasUpperCase, false);

		textWidget.setRedraw(true);

		boolean findHit = foundIndex >= 0;

		if (findHit) {
			index = foundIndex;
		} else if (this.findHit) {
			textWidget.getDisplay().beep();
		}
		this.findHit = findHit;

		searching = false;
	}

	public void endSession() {
		if (findReplaceTargetExtension != null) {
			findReplaceTargetExtension.endSession();
		}
	}

	public IRegion getScope() {
		if (findReplaceTargetExtension != null) {
			return findReplaceTargetExtension.getScope();
		}
		return null;
	}

	public void setScope(IRegion scope) {
		if (findReplaceTargetExtension != null) {
			findReplaceTargetExtension.setScope(scope);
		}
	}

	public Point getLineSelection() {
		if (findReplaceTargetExtension != null) {
			return findReplaceTargetExtension.getLineSelection();
		}
		return null;
	}

	public void setSelection(int offset, int length) {

		if (findReplaceTargetExtension != null) {
			findReplaceTargetExtension.setSelection(offset, length);
		}
	}

	public void setScopeHighlightColor(Color color) {
		if (findReplaceTargetExtension != null) {
			findReplaceTargetExtension.setScopeHighlightColor(color);
		}
	}

	public void setReplaceAllMode(boolean replaceAll) {
		if (findReplaceTargetExtension != null) {
			findReplaceTargetExtension.setReplaceAllMode(replaceAll);
		}
	}

	private void install() {
		if (installed) {
			return;
		}

		StyledText textWidget = textViewer.getTextWidget();
		if (textWidget == null) {
			return;
		}
		textViewer.addTextListener(this);
		textWidget.addMouseListener(this);
		textWidget.addFocusListener(this);

		ISelectionProvider selectionProvider = textViewer.getSelectionProvider();
		if (selectionProvider != null) {
			selectionProvider.addSelectionChangedListener(this);
		}

		if (textViewer instanceof ITextViewerExtension) {
			((ITextViewerExtension) textViewer).prependVerifyKeyListener(this);
		} else {
			textWidget.addVerifyKeyListener(this);
		}

		ICommandService commandService = (ICommandService) PlatformUI.getWorkbench().getAdapter(ICommandService.class);
		if (commandService != null) {
			commandService.addExecutionListener(this);
		}

		installed = true;
	}

	private void stop() {
		if (findString.length() > 0) {
			previousFindString = findString;
			findString = ""; //$NON-NLS-1$
		}
		index = 0;
		findHit = true;
		clearState();

		uninstall();
	}

	private void uninstall() {
		if (!installed) {
			return;
		}

		StyledText textWidget = textViewer.getTextWidget();
		if (textWidget == null) {
			return;
		}

		textViewer.removeTextListener(this);
		textWidget.removeMouseListener(this);
		textWidget.removeFocusListener(this);

		ISelectionProvider selectionProvider = textViewer.getSelectionProvider();
		if (selectionProvider != null) {
			selectionProvider.removeSelectionChangedListener(this);
		}

		if (textViewer instanceof ITextViewerExtension) {
			((ITextViewerExtension) textViewer).removeVerifyKeyListener(this);
		} else {
			textWidget.removeVerifyKeyListener(this);
		}

		ICommandService commandService = (ICommandService) PlatformUI.getWorkbench().getAdapter(ICommandService.class);
		if (commandService != null) {
			commandService.removeExecutionListener(this);
		}
		installed = false;
	}

	public void notHandled(String commandId, NotHandledException exception) {
		// ignore
	}

	public void postExecuteFailure(String commandId, ExecutionException exception) {
		// ignore
	}

	public void postExecuteSuccess(String commandId, Object returnValue) {
		// ignore
	}

	public void preExecute(String commandId, ExecutionEvent event) {
		if (!IWorkbenchActionDefinitionIds.FIND_INCREMENTAL.equals(commandId)) {
			stop();
		}
	}

	public void textChanged(TextEvent event) {
		if (event.getDocumentEvent() != null) {
			stop();
		}
	}

	public void focusGained(FocusEvent e) {
		stop();
	}

	public void focusLost(FocusEvent e) {
		stop();
	}

	public void mouseDoubleClick(MouseEvent e) {
		stop();
	}

	public void mouseDown(MouseEvent e) {
		stop();
	}

	public void mouseUp(MouseEvent e) {
		stop();
	}

	public void verifyKey(VerifyEvent event) {
		if (!event.doit) {
			return;
		}
		if (event.character == 0) {
			switch (event.keyCode) {
			case SWT.ARROW_LEFT:
			case SWT.ARROW_RIGHT:
			case SWT.ARROW_UP:
			case SWT.HOME:
			case SWT.END:
			case SWT.PAGE_DOWN:
			case SWT.PAGE_UP:
				stop();
				break;
			case SWT.ARROW_DOWN:
				next();
				event.doit = false;
				break;
			}
		} else {
			switch (event.character) {
			case SWT.ESC:
			case SWT.CR:
				stop();
				event.doit = false;
				break;
			case SWT.BS:
			case SWT.DEL:
				restoreState();
				event.doit = false;
				break;

			default:
				if (event.stateMask == 0 || event.stateMask == SWT.SHIFT) {
					saveState();
					addSearchCharacter(event.character);
					event.doit = false;
				}
			}
		}
	}

	public void selectionChanged(SelectionChangedEvent event) {
		if (!searching) {
			stop();
		}
	}
}
