/*******************************************************************************
 * Copyright (c) 2005, 2014 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
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.rename;

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

import org.eclipse.cdt.core.CConventions;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNodeSelector;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.gnu.cpp.GPPLanguage;
import org.eclipse.cdt.core.dom.parser.AbstractCLikeLanguage;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.model.IWorkingCopy;
import org.eclipse.cdt.internal.core.model.ASTCache.ASTRunnable;
import org.eclipse.cdt.internal.ui.editor.ASTProvider;
import org.eclipse.cdt.internal.ui.editor.CEditor;
import org.eclipse.cdt.internal.ui.editor.EditorHighlightingSynchronizer;
import org.eclipse.cdt.internal.ui.search.LinkedNamesFinder;
import org.eclipse.cdt.internal.ui.text.correction.proposals.LinkedNamesAssistProposal.DeleteBlockingExitPolicy;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.IWorkingCopyManager;
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.resources.IFile;
import org.eclipse.core.resources.IResource;
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.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
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.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
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.ui.IEditorInput;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

public class RenameLinkedMode {

	private class FocusEditingSupport implements IEditingSupport {
		@Override
		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;
		}

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

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

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

		@Override
		public void suspend(LinkedModeModel model) {
		}
	}

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

		@Override
		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 CEditor fEditor;

	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.
	 */
	private IUndoableOperation fStartingUndoOperation;
	private IRegion[] fLocations;

	public RenameLinkedMode(CEditor editor) {
		Assert.isNotNull(editor);
		fEditor = editor;
		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 CRenameAction
			fgActiveLinkedMode.startFullDialog();
			return;
		}

		ISourceViewer viewer = fEditor.getViewer();
		fOriginalSelection = viewer.getSelectedRange();
		final int offset = fOriginalSelection.x;

		try {
			fLocations = null;
			Point selection = viewer.getSelectedRange();
			final int secectionOffset = selection.x;
			final int selectionLength = selection.y;
			final IDocument document = viewer.getDocument();

			ASTProvider.getASTProvider().runOnAST(fEditor.getInputCElement(), ASTProvider.WAIT_ACTIVE_ONLY,
					new NullProgressMonitor(), new ASTRunnable() {
						@Override
						public IStatus runOnAST(ILanguage lang, IASTTranslationUnit astRoot) throws CoreException {
							if (astRoot == null)
								return Status.CANCEL_STATUS;

							IASTNodeSelector selector = astRoot.getNodeSelector(null);
							IASTName name = selector.findEnclosingName(secectionOffset, selectionLength);
							if (name != null) {
								fOriginalName = name.toString();
								fLocations = LinkedNamesFinder.findByName(astRoot, name);
							}
							return Status.OK_STATUS;
						}
					});

			if (fLocations == null || fLocations.length == 0) {
				return;
			}

			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);
				}
			}

			// Sort the locations starting with the one @ offset.
			Arrays.sort(fLocations, new Comparator<IRegion>() {
				@Override
				public int compare(IRegion n1, IRegion n2) {
					return rank(n1) - rank(n2);
				}

				/**
				 * Returns the absolute rank of a location. Location preceding <code>offset</code>
				 * are ranked last.
				 *
				 * @param location the location to compute the rank for
				 * @return the rank of the location with respect to the invocation offset
				 */
				private int rank(IRegion location) {
					int relativeRank = location.getOffset() + location.getLength() - offset;
					if (relativeRank < 0)
						return Integer.MAX_VALUE + relativeRank;
					else
						return relativeRank;
				}
			});

			fLinkedPositionGroup = new LinkedPositionGroup();
			for (int i = 0; i < fLocations.length; i++) {
				IRegion item = fLocations[i];
				LinkedPosition linkedPosition = new LinkedPosition(document, item.getOffset(), item.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.setExitPolicy(new ExitPolicy(document));
			ui.setExitPosition(viewer, offset, 0, LinkedPositionGroup.NO_STOP);
			ui.enter();

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

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

			openSecondaryPopup();
			fgActiveLinkedMode = this;
		} catch (BadLocationException e) {
			CUIPlugin.log(e);
		}
	}

	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 {
				executed = renameSupport.perform(shell, fEditor.getSite().getWorkbenchWindow());
			}
			if (executed) {
				restoreFullSelection();
			}
			reconcile();
		} catch (CoreException e) {
			CUIPlugin.log(e);
		} catch (InterruptedException e) {
			// canceling is OK -> redo text changes in that case?
		} catch (InvocationTargetException e) {
			CUIPlugin.log(e);
		} catch (BadLocationException e) {
			CUIPlugin.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() {
					@Override
					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, CUIPlugin.getPluginId(),
					RenameMessages.RenameLinkedMode_error_saving_editor, e));
		} catch (InterruptedException e) {
			// canceling is OK
			return null;
		} finally {
			reconcile();
		}

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

		if (newName.length() == 0)
			return null;

		IWorkingCopy workingCopy = getWorkingCopy();
		IResource resource = workingCopy.getResource();
		if (!(resource instanceof IFile)) {
			return null;
		}
		CRefactoringArgument arg = new CRefactoringArgument((IFile) resource, fOriginalSelection.x,
				fOriginalSelection.y);
		CRenameProcessor processor = new CRenameProcessor(CRefactory.getInstance(), arg);
		processor.setReplacementText(newName);
		CRenameRefactoringPreferences preferences = new CRenameRefactoringPreferences();
		processor.setSelectedOptions(preferences.getOptions());
		processor.setExhaustiveSearchScope(preferences.getScope());
		processor.setWorkingSetName(preferences.getWorkingSet());
		return RenameSupport.create(processor);
	}

	private void reconcile() throws CModelException {
		IWorkingCopy workingCopy = getWorkingCopy();
		synchronized (workingCopy) {
			workingCopy.reconcile();
		}
	}

	private IWorkingCopy getWorkingCopy() {
		IEditorInput input = fEditor.getEditorInput();
		IWorkingCopyManager manager = CUIPlugin.getDefault().getWorkingCopyManager();
		return manager.getWorkingCopy(input);
	}

	public void startFullDialog() {
		cancel();

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

	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;
			return CConventions.validateIdentifier(newName, getLanguage()).isOK();
		} catch (BadLocationException e) {
			return false;
		}
	}

	private AbstractCLikeLanguage getLanguage() {
		ITranslationUnit tu = fEditor.getInputCElement();
		ILanguage language = null;
		try {
			language = tu.getLanguage();
		} catch (CoreException e) {
		}
		if (language instanceof AbstractCLikeLanguage) {
			return (AbstractCLikeLanguage) language;
		}
		return GPPLanguage.getDefault();
	}

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