/*******************************************************************************
 * Copyright (c) 2005, 2009 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 API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.refactoring.reorg;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Comparator;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.OperationHistoryFactory;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.operation.IRunnableWithProgress;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IEditingSupport;
import org.eclipse.jface.text.IEditingSupportRegistry;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewerExtension6;
import org.eclipse.jface.text.IUndoManager;
import org.eclipse.jface.text.IUndoManagerExtension;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;

import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

import org.eclipse.ltk.core.refactoring.RefactoringCore;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardPage;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.refactoring.IJavaRefactorings;
import org.eclipse.jdt.core.refactoring.descriptors.RenameJavaElementDescriptor;

import org.eclipse.jdt.internal.corext.dom.LinkedNodeFinder;
import org.eclipse.jdt.internal.corext.dom.NodeFinder;
import org.eclipse.jdt.internal.corext.refactoring.rename.RenamingNameSuggestor;
import org.eclipse.jdt.internal.corext.util.JavaConventionsUtil;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;

import org.eclipse.jdt.ui.SharedASTProvider;
import org.eclipse.jdt.ui.refactoring.RenameSupport;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
import org.eclipse.jdt.internal.ui.javaeditor.EditorHighlightingSynchronizer;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jdt.internal.ui.refactoring.DelegateUIHelper;
import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedNamesAssistProposal.DeleteBlockingExitPolicy;

public class RenameLinkedMode {

	private class FocusEditingSupport implements IEditingSupport {
		public boolean ownsFocusShell() {
			if (fInfoPopup == null)
				return false;
			if (fInfoPopup.ownsFocusShell()) {
				return true;
			}

			Shell editorShell= fEditor.getSite().getShell();
			Shell activeShell= editorShell.getDisplay().getActiveShell();
			if (editorShell == activeShell)
				return true;
			return false;
		}

		public boolean isOriginator(DocumentEvent event, IRegion subjectRegion) {
			return false; //leave on external modification outside positions
		}
	}

	private class EditorSynchronizer implements ILinkedModeListener {
		public void left(LinkedModeModel model, int flags) {
			linkedModeLeft();
			if ( (flags & ILinkedModeListener.UPDATE_CARET) != 0) {
				doRename(fShowPreview);
			}
		}

		public void resume(LinkedModeModel model, int flags) {
		}

		public void suspend(LinkedModeModel model) {
		}
	}

	private class ExitPolicy extends DeleteBlockingExitPolicy {
		public ExitPolicy(IDocument document) {
			super(document);
		}

		public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
			fShowPreview= (event.stateMask & SWT.CTRL) != 0
							&& (event.character == SWT.CR || event.character == SWT.LF);
			return super.doExit(model, event, offset, length);
		}
	}


	private static RenameLinkedMode fgActiveLinkedMode;

	private final CompilationUnitEditor fEditor;
	private final IJavaElement fJavaElement;

	private RenameInformationPopup fInfoPopup;

	private Point fOriginalSelection;
	private String fOriginalName;

	private LinkedPosition fNamePosition;
	private LinkedModeModel fLinkedModeModel;
	private LinkedPositionGroup fLinkedPositionGroup;
	private final FocusEditingSupport fFocusEditingSupport;
	private boolean fShowPreview;

	/**
	 * The operation on top of the undo stack when the rename is {@link #start()}ed, or
	 * <code>null</code> if rename has not been started or the undo stack was empty.
	 * 
	 * @since 3.5
	 */
	private IUndoableOperation fStartingUndoOperation;


	public RenameLinkedMode(IJavaElement element, CompilationUnitEditor editor) {
		Assert.isNotNull(element);
		Assert.isNotNull(editor);
		fEditor= editor;
		fJavaElement= element;
		fFocusEditingSupport= new FocusEditingSupport();
	}

	public static RenameLinkedMode getActiveLinkedMode() {
		if (fgActiveLinkedMode != null) {
			ISourceViewer viewer= fgActiveLinkedMode.fEditor.getViewer();
			if (viewer != null) {
				StyledText textWidget= viewer.getTextWidget();
				if (textWidget != null && ! textWidget.isDisposed()) {
					return fgActiveLinkedMode;
				}
			}
			// make sure we don't hold onto the active linked mode if anything went wrong with canceling:
			fgActiveLinkedMode= null;
		}
		return null;
	}

	public void start() {
		if (getActiveLinkedMode() != null) {
			// for safety; should already be handled in RenameJavaElementAction
			fgActiveLinkedMode.startFullDialog();
			return;
		}

		ISourceViewer viewer= fEditor.getViewer();
		IDocument document= viewer.getDocument();
		fOriginalSelection= viewer.getSelectedRange();
		int offset= fOriginalSelection.x;

		try {
			CompilationUnit root= SharedASTProvider.getAST(getCompilationUnit(), SharedASTProvider.WAIT_YES, null);

			fLinkedPositionGroup= new LinkedPositionGroup();
			ASTNode selectedNode= NodeFinder.perform(root, fOriginalSelection.x, fOriginalSelection.y);
			if (! (selectedNode instanceof SimpleName)) {
				return; // TODO: show dialog
			}
			SimpleName nameNode= (SimpleName) selectedNode;

			if (viewer instanceof ITextViewerExtension6) {
				IUndoManager undoManager= ((ITextViewerExtension6)viewer).getUndoManager();
				if (undoManager instanceof IUndoManagerExtension) {
					IUndoManagerExtension undoManagerExtension= (IUndoManagerExtension)undoManager;
					IUndoContext undoContext= undoManagerExtension.getUndoContext();
					IOperationHistory operationHistory= OperationHistoryFactory.getOperationHistory();
					fStartingUndoOperation= operationHistory.getUndoOperation(undoContext);
				}
			}
			
			fOriginalName= nameNode.getIdentifier();
			final int pos= nameNode.getStartPosition();
			ASTNode[] sameNodes= LinkedNodeFinder.findByNode(root, nameNode);

			//TODO: copied from LinkedNamesAssistProposal#apply(..):
			// sort for iteration order, starting with the node @ offset
			Arrays.sort(sameNodes, new Comparator() {
				public int compare(Object o1, Object o2) {
					return rank((ASTNode) o1) - rank((ASTNode) o2);
				}
				/**
				 * Returns the absolute rank of an <code>ASTNode</code>. Nodes
				 * preceding <code>pos</code> are ranked last.
				 *
				 * @param node the node to compute the rank for
				 * @return the rank of the node with respect to the invocation offset
				 */
				private int rank(ASTNode node) {
					int relativeRank= node.getStartPosition() + node.getLength() - pos;
					if (relativeRank < 0)
						return Integer.MAX_VALUE + relativeRank;
					else
						return relativeRank;
				}
			});
			for (int i= 0; i < sameNodes.length; i++) {
				ASTNode elem= sameNodes[i];
				LinkedPosition linkedPosition= new LinkedPosition(document, elem.getStartPosition(), elem.getLength(), i);
				if (i == 0)
					fNamePosition= linkedPosition;
				fLinkedPositionGroup.addPosition(linkedPosition);
			}

			fLinkedModeModel= new LinkedModeModel();
			fLinkedModeModel.addGroup(fLinkedPositionGroup);
			fLinkedModeModel.forceInstall();
			fLinkedModeModel.addLinkingListener(new EditorHighlightingSynchronizer(fEditor));
			fLinkedModeModel.addLinkingListener(new EditorSynchronizer());

			LinkedModeUI ui= new EditorLinkedModeUI(fLinkedModeModel, viewer);
			ui.setExitPosition(viewer, offset, 0, Integer.MAX_VALUE);
			ui.setExitPolicy(new ExitPolicy(document));
			ui.enter();

			viewer.setSelectedRange(fOriginalSelection.x, fOriginalSelection.y); // by default, full word is selected; restore original selection

			if (viewer instanceof IEditingSupportRegistry) {
				IEditingSupportRegistry registry= (IEditingSupportRegistry) viewer;
				registry.register(fFocusEditingSupport);
			}

			openSecondaryPopup();
//			startAnimation();
			fgActiveLinkedMode= this;

		} catch (BadLocationException e) {
			JavaPlugin.log(e);
		}
	}

//	private void startAnimation() {
//		//TODO:
//		// - switch off if animations disabled
//		// - show rectangle around target for 500ms after animation
//		Shell shell= fEditor.getSite().getShell();
//		StyledText textWidget= fEditor.getViewer().getTextWidget();
//
//		// from popup:
//		Rectangle startRect= fPopup.getBounds();
//
//		// from editor:
////		Point startLoc= textWidget.getParent().toDisplay(textWidget.getLocation());
////		Point startSize= textWidget.getSize();
////		Rectangle startRect= new Rectangle(startLoc.x, startLoc.y, startSize.x, startSize.y);
//
//		// from hell:
////		Rectangle startRect= shell.getClientArea();
//
//		Point caretLocation= textWidget.getLocationAtOffset(textWidget.getCaretOffset());
//		Point displayLocation= textWidget.toDisplay(caretLocation);
//		Rectangle targetRect= new Rectangle(displayLocation.x, displayLocation.y, 0, 0);
//
//		RectangleAnimation anim= new RectangleAnimation(shell, startRect, targetRect);
//		anim.schedule();
//	}

	void doRename(boolean showPreview) {
		cancel();

		Image image= null;
		Label label= null;

		fShowPreview|= showPreview;
		try {
			ISourceViewer viewer= fEditor.getViewer();
			if (viewer instanceof SourceViewer) {
				SourceViewer sourceViewer= (SourceViewer) viewer;
				Control viewerControl= sourceViewer.getControl();
				if (viewerControl instanceof Composite) {
					Composite composite= (Composite) viewerControl;
					Display display= composite.getDisplay();

					// Flush pending redraw requests:
					while (! display.isDisposed() && display.readAndDispatch()) {
					}

					// Copy editor area:
					GC gc= new GC(composite);
					Point size;
					try {
						size= composite.getSize();
						image= new Image(gc.getDevice(), size.x, size.y);
						gc.copyArea(image, 0, 0);
					} finally {
						gc.dispose();
						gc= null;
					}

					// Persist editor area while executing refactoring:
					label= new Label(composite, SWT.NONE);
					label.setImage(image);
					label.setBounds(0, 0, size.x, size.y);
					label.moveAbove(null);
				}
			}

			String newName= fNamePosition.getContent();
			if (fOriginalName.equals(newName))
				return;
			RenameSupport renameSupport= undoAndCreateRenameSupport(newName);
			if (renameSupport == null)
				return;

			Shell shell= fEditor.getSite().getShell();
			boolean executed;
			if (fShowPreview) { // could have been updated by undoAndCreateRenameSupport(..)
				executed= renameSupport.openDialog(shell, true);
			} else {
				renameSupport.perform(shell, fEditor.getSite().getWorkbenchWindow());
				executed= true;
			}
			if (executed) {
				restoreFullSelection();
			}
			JavaModelUtil.reconcile(getCompilationUnit());
		} catch (CoreException ex) {
			JavaPlugin.log(ex);
		} catch (InterruptedException ex) {
			// canceling is OK -> redo text changes in that case?
		} catch (InvocationTargetException ex) {
			JavaPlugin.log(ex);
		} catch (BadLocationException e) {
			JavaPlugin.log(e);
		} finally {
			if (label != null)
				label.dispose();
			if (image != null)
				image.dispose();
		}
	}

	public void cancel() {
		if (fLinkedModeModel != null) {
			fLinkedModeModel.exit(ILinkedModeListener.NONE);
		}
		linkedModeLeft();
	}

	private void restoreFullSelection() {
		if (fOriginalSelection.y != 0) {
			int originalOffset= fOriginalSelection.x;
			LinkedPosition[] positions= fLinkedPositionGroup.getPositions();
			for (int i= 0; i < positions.length; i++) {
				LinkedPosition position= positions[i];
				if (! position.isDeleted() && position.includes(originalOffset)) {
					fEditor.getViewer().setSelectedRange(position.offset, position.length);
					return;
				}
			}
		}
	}

	private RenameSupport undoAndCreateRenameSupport(String newName) throws CoreException {
		// Assumption: the linked mode model should be shut down by now.

		final ISourceViewer viewer= fEditor.getViewer();

		try {
			if (! fOriginalName.equals(newName)) {
				fEditor.getSite().getWorkbenchWindow().run(false, true, new IRunnableWithProgress() {
					public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
						if (viewer instanceof ITextViewerExtension6) {
							IUndoManager undoManager= ((ITextViewerExtension6)viewer).getUndoManager();
							if (undoManager instanceof IUndoManagerExtension) {
								IUndoManagerExtension undoManagerExtension= (IUndoManagerExtension)undoManager;
								IUndoContext undoContext= undoManagerExtension.getUndoContext();
								IOperationHistory operationHistory= OperationHistoryFactory.getOperationHistory();
								while (undoManager.undoable()) {
									if (fStartingUndoOperation != null && fStartingUndoOperation.equals(operationHistory.getUndoOperation(undoContext)))
										return;
									undoManager.undo();
								}
							}
						}
					}
				});
			}
		} catch (InvocationTargetException e) {
			throw new CoreException(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), ReorgMessages.RenameLinkedMode_error_saving_editor, e));
		} catch (InterruptedException e) {
			// canceling is OK
			return null;
		} finally {
			JavaModelUtil.reconcile(getCompilationUnit());
		}

		viewer.setSelectedRange(fOriginalSelection.x, fOriginalSelection.y);

		RenameJavaElementDescriptor descriptor= createRenameDescriptor(fJavaElement, newName);
		RenameSupport renameSupport= RenameSupport.create(descriptor);
		return renameSupport;
	}

	private ICompilationUnit getCompilationUnit() {
		return (ICompilationUnit) EditorUtility.getEditorInputJavaElement(fEditor, false);
	}

	public void startFullDialog() {
		cancel();

		try {
			String newName= fNamePosition.getContent();
			RenameSupport renameSupport= undoAndCreateRenameSupport(newName);
			if (renameSupport != null)
				renameSupport.openDialog(fEditor.getSite().getShell());
		} catch (CoreException e) {
			JavaPlugin.log(e);
		} catch (BadLocationException e) {
			JavaPlugin.log(e);
		}
	}

	/**
	 * Creates a rename descriptor.
	 * 
	 * @param javaElement element to rename 
	 * @param newName new name
	 * @return a rename descriptor with current settings as used in the refactoring dialogs
	 * @throws JavaModelException if an error occurs while accessing the element
	 */
	private RenameJavaElementDescriptor createRenameDescriptor(IJavaElement javaElement, String newName) throws JavaModelException {
		String contributionId;
		// see RefactoringExecutionStarter#createRenameSupport(..):
		int elementType= javaElement.getElementType();
		switch (elementType) {
			case IJavaElement.JAVA_PROJECT:
				contributionId= IJavaRefactorings.RENAME_JAVA_PROJECT;
				break;
			case IJavaElement.PACKAGE_FRAGMENT_ROOT:
				contributionId= IJavaRefactorings.RENAME_SOURCE_FOLDER;
				break;
			case IJavaElement.PACKAGE_FRAGMENT:
				contributionId= IJavaRefactorings.RENAME_PACKAGE;
				break;
			case IJavaElement.COMPILATION_UNIT:
				contributionId= IJavaRefactorings.RENAME_COMPILATION_UNIT;
				break;
			case IJavaElement.TYPE:
				contributionId= IJavaRefactorings.RENAME_TYPE;
				break;
			case IJavaElement.METHOD:
				final IMethod method= (IMethod) javaElement;
				if (method.isConstructor())
					return createRenameDescriptor(method.getDeclaringType(), newName);
				else
					contributionId= IJavaRefactorings.RENAME_METHOD;
				break;
			case IJavaElement.FIELD:
				IField field= (IField) javaElement;
				if (field.isEnumConstant())
					contributionId= IJavaRefactorings.RENAME_ENUM_CONSTANT;
				else
					contributionId= IJavaRefactorings.RENAME_FIELD;
				break;
			case IJavaElement.TYPE_PARAMETER:
				contributionId= IJavaRefactorings.RENAME_TYPE_PARAMETER;
				break;
			case IJavaElement.LOCAL_VARIABLE:
				contributionId= IJavaRefactorings.RENAME_LOCAL_VARIABLE;
				break;
			default:
				return null;
		}

		RenameJavaElementDescriptor descriptor= (RenameJavaElementDescriptor) RefactoringCore.getRefactoringContribution(contributionId).createDescriptor();
		descriptor.setJavaElement(javaElement);
		descriptor.setNewName(newName);
		if (elementType != IJavaElement.PACKAGE_FRAGMENT_ROOT)
			descriptor.setUpdateReferences(true);

		IDialogSettings javaSettings= JavaPlugin.getDefault().getDialogSettings();
		IDialogSettings refactoringSettings= javaSettings.getSection(RefactoringWizardPage.REFACTORING_SETTINGS); //TODO: undocumented API
		if (refactoringSettings == null) {
			refactoringSettings= javaSettings.addNewSection(RefactoringWizardPage.REFACTORING_SETTINGS);
		}

		switch (elementType) {
			case IJavaElement.METHOD:
			case IJavaElement.FIELD:
				descriptor.setDeprecateDelegate(refactoringSettings.getBoolean(DelegateUIHelper.DELEGATE_DEPRECATION));
				descriptor.setKeepOriginal(refactoringSettings.getBoolean(DelegateUIHelper.DELEGATE_UPDATING));
		}
		switch (elementType) {
			case IJavaElement.TYPE:
//			case IJavaElement.COMPILATION_UNIT: // TODO
				descriptor.setUpdateSimilarDeclarations(refactoringSettings.getBoolean(RenameRefactoringWizard.TYPE_UPDATE_SIMILAR_ELEMENTS));
				int strategy;
				try {
					strategy= refactoringSettings.getInt(RenameRefactoringWizard.TYPE_SIMILAR_MATCH_STRATEGY);
				} catch (NumberFormatException e) {
					strategy= RenamingNameSuggestor.STRATEGY_EXACT;
				}
				descriptor.setMatchStrategy(strategy);
		}
		switch (elementType) {
			case IJavaElement.PACKAGE_FRAGMENT:
				descriptor.setUpdateHierarchy(refactoringSettings.getBoolean(RenameRefactoringWizard.PACKAGE_RENAME_SUBPACKAGES));
		}
		switch (elementType) {
			case IJavaElement.PACKAGE_FRAGMENT:
			case IJavaElement.TYPE:
				String fileNamePatterns= refactoringSettings.get(RenameRefactoringWizard.QUALIFIED_NAMES_PATTERNS);
				if (fileNamePatterns != null && fileNamePatterns.length() != 0) {
					descriptor.setFileNamePatterns(fileNamePatterns);
					boolean updateQualifiedNames= refactoringSettings.getBoolean(RenameRefactoringWizard.UPDATE_QUALIFIED_NAMES);
					descriptor.setUpdateQualifiedNames(updateQualifiedNames);
					fShowPreview|= updateQualifiedNames;
				}
		}
		switch (elementType) {
			case IJavaElement.PACKAGE_FRAGMENT:
			case IJavaElement.TYPE:
			case IJavaElement.FIELD:
				boolean updateTextualOccurrences= refactoringSettings.getBoolean(RenameRefactoringWizard.UPDATE_TEXTUAL_MATCHES);
				descriptor.setUpdateTextualOccurrences(updateTextualOccurrences);
				fShowPreview|= updateTextualOccurrences;
		}
		switch (elementType) {
			case IJavaElement.FIELD:
				descriptor.setRenameGetters(refactoringSettings.getBoolean(RenameRefactoringWizard.FIELD_RENAME_GETTER));
				descriptor.setRenameSetters(refactoringSettings.getBoolean(RenameRefactoringWizard.FIELD_RENAME_SETTER));
		}
		return descriptor;
	}

	private void linkedModeLeft() {
		fgActiveLinkedMode= null;
		if (fInfoPopup != null) {
			fInfoPopup.close();
		}

		ISourceViewer viewer= fEditor.getViewer();
		if (viewer instanceof IEditingSupportRegistry) {
			IEditingSupportRegistry registry= (IEditingSupportRegistry) viewer;
			registry.unregister(fFocusEditingSupport);
		}
	}

	private void openSecondaryPopup() {
		fInfoPopup= new RenameInformationPopup(fEditor, this);
		fInfoPopup.open();
	}

	public boolean isCaretInLinkedPosition() {
		return getCurrentLinkedPosition() != null;
	}

	public LinkedPosition getCurrentLinkedPosition() {
		Point selection= fEditor.getViewer().getSelectedRange();
		int start= selection.x;
		int end= start + selection.y;
		LinkedPosition[] positions= fLinkedPositionGroup.getPositions();
		for (int i= 0; i < positions.length; i++) {
			LinkedPosition position= positions[i];
			if (position.includes(start) && position.includes(end))
				return position;
		}
		return null;
	}

	public boolean isEnabled() {
		try {
			String newName= fNamePosition.getContent();
			if (fOriginalName.equals(newName))
				return false;
			/*
			 * TODO: use JavaRenameProcessor#checkNewElementName(String)
			 * but make sure implementations don't access outdated Java Model
			 * (cache all necessary information before starting linked mode).
			 */
			return JavaConventionsUtil.validateIdentifier(newName, fJavaElement).isOK();
		} catch (BadLocationException e) {
			return false;
		}

	}

	public boolean isOriginalName() {
		try {
			String newName= fNamePosition.getContent();
			return fOriginalName.equals(newName);
		} catch (BadLocationException e) {
			return false;
		}
	}

}
