/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *******************************************************************************/
package org.eclipse.ltk.internal.core.refactoring;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.OperationHistoryFactory;
import org.eclipse.core.commands.operations.TriggeredOperations;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.IUndoManager;
import org.eclipse.ltk.core.refactoring.IUndoManagerListener;
import org.eclipse.ltk.core.refactoring.IValidationCheckResultQuery;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;

public class UndoManager2 implements IUndoManager {

	private class OperationHistoryListener implements IOperationHistoryListener {
		@Override
		public void historyNotification(OperationHistoryEvent event) {
			IUndoableOperation op= event.getOperation();
			if (op instanceof TriggeredOperations) {
				op= ((TriggeredOperations)op).getTriggeringOperation();
			}
			UndoableOperation2ChangeAdapter changeOperation= null;
			if (op instanceof UndoableOperation2ChangeAdapter) {
				changeOperation= (UndoableOperation2ChangeAdapter)op;
			}
			if (changeOperation == null)
				return;
			Change change= changeOperation.getChange();
			switch(event.getEventType()) {
				case OperationHistoryEvent.ABOUT_TO_EXECUTE:
				case OperationHistoryEvent.ABOUT_TO_UNDO:
				case OperationHistoryEvent.ABOUT_TO_REDO:
					fireAboutToPerformChange(change);
					break;
				case OperationHistoryEvent.DONE:
				case OperationHistoryEvent.UNDONE:
				case OperationHistoryEvent.REDONE:
					fireChangePerformed(change);
					fireUndoStackChanged();
					fireRedoStackChanged();
					break;
				case OperationHistoryEvent.OPERATION_NOT_OK:
					fireChangePerformed(change);
					break;
				case OperationHistoryEvent.OPERATION_ADDED:
					// would be better to have different events for this
					fireUndoStackChanged();
					fireRedoStackChanged();
					break;
				case OperationHistoryEvent.OPERATION_REMOVED:
					// would be better to have different events for this
					fireUndoStackChanged();
					fireRedoStackChanged();
					break;
			}
		}
	}

	private static class NullQuery implements IValidationCheckResultQuery {
		@Override
		public boolean proceed(RefactoringStatus status) {
			return true;
		}
		@Override
		public void stopped(RefactoringStatus status) {
			// do nothing
		}
	}

	private static class QueryAdapter implements IAdaptable {
		private IValidationCheckResultQuery fQuery;
		public QueryAdapter(IValidationCheckResultQuery query) {
			fQuery= query;
		}
		@SuppressWarnings("unchecked")
		@Override
		public <T> T getAdapter(Class<T> adapter) {
			if (IValidationCheckResultQuery.class.equals(adapter))
				return (T) fQuery;
			return null;
		}
	}

	private IOperationHistory fOperationHistory;
	private IOperationHistoryListener fOperationHistoryListener;

	private boolean fIsOpen;
	private TriggeredOperations fActiveOperation;

	private ListenerList<IUndoManagerListener> fListeners;

	public UndoManager2() {
		fOperationHistory= OperationHistoryFactory.getOperationHistory();
	}

	@Override
	public void addListener(IUndoManagerListener listener) {
		if (fListeners == null) {
			fListeners= new ListenerList<>(ListenerList.IDENTITY);
			fOperationHistoryListener= new OperationHistoryListener();
			fOperationHistory.addOperationHistoryListener(fOperationHistoryListener);
		}
		fListeners.add(listener);
	}

	@Override
	public void removeListener(IUndoManagerListener listener) {
		if (fListeners == null)
			return;
		fListeners.remove(listener);
		if (fListeners.size() == 0) {
			fOperationHistory.removeOperationHistoryListener(fOperationHistoryListener);
			fListeners= null;
			fOperationHistoryListener= null;
		}
	}

	@Override
	public void aboutToPerformChange(Change change) {
		IUndoableOperation operation= new UndoableOperation2ChangeAdapter(change);
		operation.addContext(RefactoringCorePlugin.getUndoContext());
		fActiveOperation= new TriggeredOperations(operation, fOperationHistory);
		fActiveOperation.addContext(RefactoringCorePlugin.getUndoContext());
    	fOperationHistory.openOperation(fActiveOperation, IOperationHistory.EXECUTE);
    	fIsOpen= true;
	}

	/**
	 * @param change the change
	 * @deprecated use #changePerformed(Change, boolean) instead
	 */
	@Deprecated
	@Override
	public void changePerformed(Change change) {
		changePerformed(change, true);
	}

	@Override
	public void changePerformed(Change change, boolean successful) {
		if (fIsOpen && fActiveOperation != null) {
			fOperationHistory.closeOperation(successful, false, IOperationHistory.EXECUTE);
	        fIsOpen= false;
		}
	}

	@Override
	public void addUndo(String name, Change change) {
		if (fActiveOperation != null) {
			UndoableOperation2ChangeAdapter operation= (UndoableOperation2ChangeAdapter)fActiveOperation.getTriggeringOperation();
			operation.setUndoChange(change);
			operation.setLabel(name);
			fOperationHistory.add(fActiveOperation);
			fActiveOperation= null;
		}
	}

	@Override
	public boolean anythingToUndo() {
		return fOperationHistory.canUndo(RefactoringCorePlugin.getUndoContext());
	}

	@Override
	public String peekUndoName() {
		IUndoableOperation op= fOperationHistory.getUndoOperation(RefactoringCorePlugin.getUndoContext());
		if (op == null)
			return null;
		return op.getLabel();
	}

	@Override
	public void performUndo(IValidationCheckResultQuery query, IProgressMonitor pm) throws CoreException {
		IUndoableOperation undo= fOperationHistory.getUndoOperation(RefactoringCorePlugin.getUndoContext());
		UndoableOperation2ChangeAdapter changeOperation= getUnwrappedOperation(undo);
		if (changeOperation == null)
			throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(),
				IStatus.ERROR, RefactoringCoreMessages.UndoManager2_no_change, null));
		if (query == null)
			query= new NullQuery();
		try {
			fOperationHistory.undoOperation(undo, pm, new QueryAdapter(query));
		} catch (ExecutionException e) {
			handleException(e);
		}
	}

	@Override
	public boolean anythingToRedo() {
		return fOperationHistory.canRedo(RefactoringCorePlugin.getUndoContext());
	}

	@Override
	public String peekRedoName() {
		IUndoableOperation op= fOperationHistory.getRedoOperation(RefactoringCorePlugin.getUndoContext());
		if (op == null)
			return null;
		return op.getLabel();
	}

	@Override
	public void performRedo(IValidationCheckResultQuery query, IProgressMonitor pm) throws CoreException {
		IUndoableOperation redo= fOperationHistory.getRedoOperation(RefactoringCorePlugin.getUndoContext());
		UndoableOperation2ChangeAdapter changeOperation= getUnwrappedOperation(redo);
		if (changeOperation == null)
			throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(),
				IStatus.ERROR, RefactoringCoreMessages.UndoManager2_no_change, null));
		if (query == null)
			query= new NullQuery();
		try {
			fOperationHistory.redoOperation(redo, pm, new QueryAdapter(query));
		} catch (ExecutionException e) {
			handleException(e);
		}
	}

	private UndoableOperation2ChangeAdapter getUnwrappedOperation(IUndoableOperation operation) {
		IUndoableOperation result= operation;
		if (result instanceof TriggeredOperations) {
			result= ((TriggeredOperations)result).getTriggeringOperation();
		}
		if (result instanceof UndoableOperation2ChangeAdapter) {
			return (UndoableOperation2ChangeAdapter)result;
		}
		return null;
	}

	@Override
	public void flush() {
		if (fActiveOperation != null) {
			if (fIsOpen) {
				fOperationHistory.closeOperation(false, false, IOperationHistory.EXECUTE);
			}
			/* the triggering operation is invalid, but we must ensure that any
	         * other operations executed while it was open remain in the undo
	         * history.  We accomplish this by adding the invalid operation,
	         * since disposing the context later will cause it to be broken up into
	         * its atomic parts.
	         */
			fOperationHistory.add(fActiveOperation);
		}
		fActiveOperation= null;
		fIsOpen= false;
		fOperationHistory.dispose(RefactoringCorePlugin.getUndoContext(), true, true, false);
	}

	@Override
	public void shutdown() {
		// nothing to do since we have a shared undo manager anyways.
	}

	private void handleException(ExecutionException e) throws CoreException {
		Throwable cause= e.getCause();
		if (cause instanceof CoreException) {
			throw (CoreException)cause;
		} else {
			throw new CoreException(new Status(
				IStatus.ERROR, RefactoringCorePlugin.getPluginId(),IStatus.ERROR,
				RefactoringCoreMessages.RefactoringCorePlugin_internal_error,
				e));
		}
	}

	//---- event firing methods -------------------------------------------------

	private void fireAboutToPerformChange(final Change change) {
		if (fListeners == null)
			return;
		for (final IUndoManagerListener listener : fListeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.aboutToPerformChange(UndoManager2.this, change);
				}
				@Override
				public void handleException(Throwable exception) {
					RefactoringCorePlugin.log(exception);
				}
			});
		}
	}

	private void fireChangePerformed(final Change change) {
		if (fListeners == null)
			return;
		for (final IUndoManagerListener listener : fListeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.changePerformed(UndoManager2.this, change);
				}
				@Override
				public void handleException(Throwable exception) {
					RefactoringCorePlugin.log(exception);
				}
			});
		}
	}

	private void fireUndoStackChanged() {
		if (fListeners == null)
			return;
		for (final IUndoManagerListener listener : fListeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.undoStackChanged(UndoManager2.this);
				}
				@Override
				public void handleException(Throwable exception) {
					RefactoringCorePlugin.log(exception);
				}
			});
		}
	}

	private void fireRedoStackChanged() {
		if (fListeners == null)
			return;
		for (final IUndoManagerListener listener : fListeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.redoStackChanged(UndoManager2.this);
				}
				@Override
				public void handleException(Throwable exception) {
					RefactoringCorePlugin.log(exception);
				}
			});
		}
	}

	//---- testing methods ---------------------------------------------

	public boolean testHasNumberOfUndos(int number) {
		return fOperationHistory.getUndoHistory(RefactoringCorePlugin.getUndoContext()).length == number;
	}

	public boolean testHasNumberOfRedos(int number) {
		return fOperationHistory.getRedoHistory(RefactoringCorePlugin.getUndoContext()).length == number;
	}
}
