/*******************************************************************************
 * Copyright (c) 2005, 2018 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
 *     Lars Vogel <Lars.Vogel@gmail.com> - Bug 427961
 *******************************************************************************/
package org.eclipse.ui.examples.undo.views;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.IOperationApprover;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.examples.undo.AddBoxOperation;
import org.eclipse.ui.examples.undo.Box;
import org.eclipse.ui.examples.undo.Boxes;
import org.eclipse.ui.examples.undo.ClearBoxesOperation;
import org.eclipse.ui.examples.undo.MoveBoxOperation;
import org.eclipse.ui.examples.undo.PromptingUserApprover;
import org.eclipse.ui.examples.undo.UndoExampleMessages;
import org.eclipse.ui.examples.undo.UndoPlugin;
import org.eclipse.ui.examples.undo.preferences.PreferenceConstants;
import org.eclipse.ui.operations.RedoActionHandler;
import org.eclipse.ui.operations.UndoActionHandler;
import org.eclipse.ui.part.ViewPart;

public class BoxView extends ViewPart {
	/*
	 * The canvas to paint the boxes on.
	 */
	private Canvas paintCanvas;

	/*
	 * The gc used for drawing the rubber band.
	 */
	private GC gc;

	/*
	 * The model, a group of boxes
	 */
	private Boxes boxes = new Boxes();

	/*
	 * Undo and redo actions
	 */
	private UndoActionHandler undoAction;

	private RedoActionHandler redoAction;

	private IAction clearBoxesAction;

	/*
	 * Private undo context for box operations
	 */
	private IUndoContext undoContext;

	/*
	 * Operation approver for approving undo and redo
	 */
	private IOperationApprover operationApprover;

	/*
	 * Property change listener for the undo limit preference.
	 */
	private IPropertyChangeListener propertyChangeListener;

	/*
	 * True if a click-drag is in progress
	 */
	private boolean dragInProgress = false;

	/*
	 * True if a click-move is in progress
	 */
	private boolean moveInProgress = false;

	/*
	 * The box that is being moved.
	 */
	private Box movingBox;

	/*
	 * The diff between a moving box and the track position.
	 */
	int diffX, diffY;

	/*
	 * The position of the first click in a click-drag
	 */
	private Point anchorPosition = new Point(-1, -1);

	/*
	 * A temporary point in a drag or move operation
	 */
	private Point tempPosition = new Point(-1, -1);

	/*
	 * The rubber band position (the last recorded temp position)
	 */
	private Point rubberbandPosition = new Point(-1, -1);

	/*
	 * Construct a BoxView.
	 */
	public BoxView() {
		super();
		initializeOperationHistory();
	}

	/*
	 * Create the canvas on which boxes are drawn and hook up all actions and
	 * listeners.
	 */
	@Override
	public void createPartControl(Composite parent) {
		paintCanvas = new Canvas(parent, SWT.BORDER | SWT.V_SCROLL
				| SWT.H_SCROLL | SWT.NO_REDRAW_RESIZE);

		// set up a GC for drawing the tracking rectangle
		gc = new GC(paintCanvas);
		gc.setForeground(paintCanvas.getForeground());
		gc.setLineStyle(SWT.LINE_SOLID);

		// add listeners
		addListeners();

		// create actions and hook them up to the menus and toolbar
		makeActions();
		hookContextMenu();
		createGlobalActionHandlers();
		contributeToActionBars();
	}

	/*
	 * Add listeners to the canvas.
	 */
	private void addListeners() {
		paintCanvas.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseDown(MouseEvent event) {
				if (event.button != 1) {
					return;
				}
				if (dragInProgress || moveInProgress)
				 {
					return; // spurious event
				}

				tempPosition.x = event.x;
				tempPosition.y = event.y;
				Box box = boxes.getBox(event.x, event.y);
				if (box != null) {
					moveInProgress = true;
					movingBox = box;
					anchorPosition.x = box.x1;
					anchorPosition.y = box.y1;
					diffX = event.x - box.x1;
					diffY = event.y - box.y1;
				} else {
					dragInProgress = true;
					anchorPosition.x = event.x;
					anchorPosition.y = event.y;
				}
			}

			@Override
			public void mouseUp(MouseEvent event) {
				if (event.button != 1) {
					resetDrag(true); // abort if right or middle mouse button
					// pressed
					return;
				}
				if (anchorPosition.x == -1)
				 {
					return; // spurious event
				}

				if (dragInProgress) {
					Box box = new Box(anchorPosition.x, anchorPosition.y,
							tempPosition.x, tempPosition.y);
					if (box.getWidth() > 0 && box.getHeight() > 0) {
						try {
							getOperationHistory().execute(
									new AddBoxOperation(
											UndoExampleMessages.BoxView_Add,
											undoContext, boxes, box, paintCanvas),
									null, null);
						} catch (ExecutionException e) {
						}
						dragInProgress = false;
					}
				} else if (moveInProgress) {
					try {
						getOperationHistory().execute(
								new MoveBoxOperation(
										UndoExampleMessages.BoxView_Move,
										undoContext, movingBox, paintCanvas,
										anchorPosition), null, null);
					} catch (ExecutionException e) {
					}
					moveInProgress = false;
					movingBox = null;
				}
				resetDrag(false);

				// redraw everything to clean up the tracking rectangle
				paintCanvas.redraw();
			}

			@Override
			public void mouseDoubleClick(MouseEvent event) {
			}
		});
		paintCanvas.addMouseMoveListener(event -> {
			if (dragInProgress) {
				clearRubberBandSelection();
				tempPosition.x = event.x;
				tempPosition.y = event.y;
				addRubberBandSelection();
			} else if (moveInProgress) {
				clearRubberBandSelection();
				anchorPosition.x = event.x - diffX;
				anchorPosition.y = event.y - diffY;
				tempPosition.x = anchorPosition.x + movingBox.getWidth();
				tempPosition.y = anchorPosition.y + movingBox.getHeight();
				addRubberBandSelection();
			}
		});
		paintCanvas.addPaintListener(event -> {
			event.gc.setForeground(paintCanvas.getForeground());
			boxes.draw(event.gc);
		});

		paintCanvas.addDisposeListener(event -> {
			// dispose the gc
			gc.dispose();
			// dispose listeners
			removeListeners();
		});

		// listen for a change in the undo limit
		propertyChangeListener = event -> {
			if (event.getProperty() == PreferenceConstants.PREF_UNDOLIMIT) {
				int limit = UndoPlugin.getDefault().getPreferenceStore().getInt(PreferenceConstants.PREF_UNDOLIMIT);
				getOperationHistory().setLimit(undoContext, limit);
			}
		};
		UndoPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(
				propertyChangeListener);

	}

	/*
	 * Remove listeners that were registered. Since the control is being
	 * disposed, we are only removing non-control listeners.
	 */
	private void removeListeners() {
		UndoPlugin.getDefault().getPreferenceStore()
				.removePropertyChangeListener(propertyChangeListener);
		getOperationHistory().removeOperationApprover(operationApprover);
	}

	/*
	 * Hook a listener on the menu showing so we can fill the context menu with
	 * our actions.
	 */
	private void hookContextMenu() {
		MenuManager menuMgr = new MenuManager("#PopupMenu");
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(BoxView.this::fillContextMenu);
		Menu menu = menuMgr.createContextMenu(paintCanvas);
		paintCanvas.setMenu(menu);
	}

	/*
	 * Add our actions to the action bars.
	 */
	private void contributeToActionBars() {
		IActionBars bars = getViewSite().getActionBars();
		fillLocalPullDown(bars.getMenuManager());
		fillLocalToolBar(bars.getToolBarManager());
	}

	/*
	 * Add our undo and redo actions to the local pulldown.
	 */
	private void fillLocalPullDown(IMenuManager manager) {
		manager.add(undoAction);
		manager.add(redoAction);
		manager.add(new Separator());
		manager.add(clearBoxesAction);
	}

	/*
	 * Add our undo and redo actions to the context menu.
	 */
	private void fillContextMenu(IMenuManager manager) {
		manager.add(undoAction);
		manager.add(redoAction);
		manager.add(new Separator());
		manager.add(clearBoxesAction);
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
	}

	/*
	 * Add actions to the local toolbar.
	 */
	private void fillLocalToolBar(IToolBarManager manager) {
		manager.add(clearBoxesAction);
	}

	/*
	 * Make any local actions used in the view.
	 */
	private void makeActions() {
		clearBoxesAction = new Action() {
			@Override
			public void run() {
				try {
					getOperationHistory().execute(
							new ClearBoxesOperation(
									UndoExampleMessages.BoxView_ClearBoxes,
									undoContext, boxes, paintCanvas), null,
							null);
				} catch (ExecutionException e) {
				}
			}
		};
		clearBoxesAction.setText(UndoExampleMessages.BoxView_ClearBoxes);
		clearBoxesAction
				.setToolTipText(UndoExampleMessages.BoxView_ClearBoxesToolTipText);
		clearBoxesAction.setImageDescriptor(PlatformUI.getWorkbench()
				.getSharedImages().getImageDescriptor(
						ISharedImages.IMG_TOOL_DELETE));
	}

	/*
	 * Create the global undo and redo action handlers.
	 */
	private void createGlobalActionHandlers() {
		// set up action handlers that operate on the current context
		undoAction = new UndoActionHandler(this.getSite(), undoContext);
		redoAction = new RedoActionHandler(this.getSite(), undoContext);
		IActionBars actionBars = getViewSite().getActionBars();
		actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(),
				undoAction);
		actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(),
				redoAction);
	}

	/*
	 * Set focus to the canvas.
	 */
	@Override
	public void setFocus() {
		paintCanvas.setFocus();
	}

	/*
	 * Reset the drag operation.
	 */
	private void resetDrag(boolean clearRubberband) {
		if (clearRubberband) {
			clearRubberBandSelection();
		}
		dragInProgress = false;
		moveInProgress = false;
		movingBox = null;
		anchorPosition.x = anchorPosition.y = tempPosition.x = tempPosition.y = rubberbandPosition.x = rubberbandPosition.y = -1;
	}

	/*
	 * Clear the existing rubber band selection.
	 */
	private void clearRubberBandSelection() {
		gc.setForeground(paintCanvas.getBackground());
		gc.drawRectangle(anchorPosition.x, anchorPosition.y,
				rubberbandPosition.x - anchorPosition.x, rubberbandPosition.y
						- anchorPosition.y);
		paintCanvas.redraw(anchorPosition.x, anchorPosition.y,
				rubberbandPosition.x - anchorPosition.x, rubberbandPosition.y
						- anchorPosition.y, false);
		paintCanvas.update();
		rubberbandPosition = new Point(-1, -1);
		gc.setForeground(paintCanvas.getForeground());
	}

	/*
	 * Show a rubber band selection.
	 */
	private void addRubberBandSelection() {
		rubberbandPosition = tempPosition;
		gc.drawRectangle(anchorPosition.x, anchorPosition.y,
				rubberbandPosition.x - anchorPosition.x, rubberbandPosition.y
						- anchorPosition.y);
	}

	/*
	 * Initialize the workbench operation history for our undo context.
	 */
	private void initializeOperationHistory() {
		// create a unique undo context to
		// represent this view's undo history
		undoContext = new ObjectUndoContext(this);

		// set the undo limit for this context based on the preference
		int limit = UndoPlugin.getDefault().getPreferenceStore().getInt(
				PreferenceConstants.PREF_UNDOLIMIT);
		getOperationHistory().setLimit(undoContext, limit);

		// Install an operation approver for this undo context that prompts
		// according to a user preference.
		operationApprover = new PromptingUserApprover(undoContext);
		getOperationHistory().addOperationApprover(operationApprover);
	}

	/*
	 * Get the operation history from the workbench.
	 */
	private IOperationHistory getOperationHistory() {
		return PlatformUI.getWorkbench().getOperationSupport()
				.getOperationHistory();
	}

}
