/*******************************************************************************
 * 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.cdt.debug.internal.ui.actions;

import java.util.Iterator;

import org.eclipse.cdt.debug.ui.CDebugUIPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.ui.IActionDelegate2;
import org.eclipse.ui.INullSelectionListener;
import org.eclipse.ui.IViewActionDelegate;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;

public abstract class AbstractDebugActionDelegate
		implements IWorkbenchWindowActionDelegate, IViewActionDelegate, IActionDelegate2, INullSelectionListener {

	/**
	 * The underlying action for this delegate
	 */
	private IAction fAction;
	/**
	 * This action's view part, or <code>null</code>
	 * if not installed in a view.
	 */
	private IViewPart fViewPart;

	/**
	 * Cache of the most recent seletion
	 */
	private IStructuredSelection fSelection = StructuredSelection.EMPTY;

	/**
	 * Whether this delegate has been initialized
	 */
	private boolean fInitialized = false;

	/**
	 * The window associated with this action delegate
	 * May be <code>null</code>
	 */
	protected IWorkbenchWindow fWindow;

	/**
	 * Background job for this action, or <code>null</code> if none.
	 */
	private DebugRequestJob fBackgroundJob = null;

	class DebugRequestJob extends Job {

		private Object[] fElements = null;

		/**
		 * Constructs a new job to perform a debug request (for example, step)
		 * in the background.
		 *
		 * @param name job name
		 */
		public DebugRequestJob(String name) {
			super(name);
			setPriority(Job.INTERACTIVE);
		}

		/* (non-Javadoc)
		 * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
		 */
		@Override
		protected IStatus run(IProgressMonitor monitor) {
			MultiStatus status = new MultiStatus(CDebugUIPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED,
					getStatusMessage(), null);
			for (int i = 0; i < fElements.length; i++) {
				Object element = fElements[i];
				try {
					doAction(element);
				} catch (DebugException e) {
					status.merge(e.getStatus());
				}
			}
			return status;
		}

		/**
		 * Sets the selection to operate on.
		 *
		 * @param elements
		 */
		public void setTargets(Object[] elements) {
			fElements = elements;
		}

	}

	/**
	 * It's crucial that delegate actions have a zero-arg constructor so that
	 * they can be reflected into existence when referenced in an action set
	 * in the plugin's plugin.xml file.
	 */
	public AbstractDebugActionDelegate() {
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#dispose()
	 */
	@Override
	public void dispose() {
		if (getWindow() != null) {
			getWindow().getSelectionService().removeSelectionListener(IDebugUIConstants.ID_DEBUG_VIEW, this);
		}
		fBackgroundJob = null;
		fSelection = null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#init(org.eclipse.ui.IWorkbenchWindow)
	 */
	@Override
	public void init(IWorkbenchWindow window) {
		// listen to selection changes in the debug view
		setWindow(window);
		window.getSelectionService().addSelectionListener(IDebugUIConstants.ID_DEBUG_VIEW, this);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
	 */
	@Override
	public void run(IAction action) {
		if (action.isEnabled()) {
			IStructuredSelection selection = getSelection();
			// disable the action so it cannot be run again until an event or selection change
			// updates the enablement
			action.setEnabled(false);
			if (isRunInBackground()) {
				runInBackground(action, selection);
			} else {
				runInForeground(selection);
			}
		}
	}

	/**
	 * Runs this action in a background job.
	 */
	private void runInBackground(IAction action, IStructuredSelection selection) {
		if (fBackgroundJob == null) {
			fBackgroundJob = new DebugRequestJob(action.getText());
		}
		fBackgroundJob.setTargets(selection.toArray());
		fBackgroundJob.schedule();
	}

	/**
	 * Runs this action in the UI thread.
	 */
	private void runInForeground(final IStructuredSelection selection) {
		final MultiStatus status = new MultiStatus(CDebugUIPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED,
				getStatusMessage(), null);
		BusyIndicator.showWhile(Display.getCurrent(), () -> {
			Iterator<?> selectionIter = selection.iterator();
			while (selectionIter.hasNext()) {
				Object element = selectionIter.next();
				try {
					doAction(element);
				} catch (DebugException e) {
					status.merge(e.getStatus());
				}
			}
		});
		reportErrors(status);
	}

	private void reportErrors(final MultiStatus ms) {
		if (!ms.isOK()) {
			IWorkbenchWindow window = CDebugUIPlugin.getActiveWorkbenchWindow();
			if (window != null) {
				ErrorDialog.openError(window.getShell(), getErrorDialogTitle(), getErrorDialogMessage(), ms);
			} else {
				CDebugUIPlugin.log(ms);
			}
		}
	}

	/**
	 * Returns whether or not this action should be run in the background.
	 * Subclasses may override.
	 * @return whether or not this action should be run in the background
	 */
	protected boolean isRunInBackground() {
		return false;
	}

	/**
	 * AbstractDebugActionDelegates come in 2 flavors: IViewActionDelegate,
	 * IWorkbenchWindowActionDelegate delegates.
	 * </p>
	 * <ul>
	 * <li>IViewActionDelegate delegate: getView() != null</li>
	 * <li>IWorkbenchWindowActionDelegate: getView == null</li>
	 * </ul>
	 * <p>
	 * Only want to call update(action, selection) for IViewActionDelegates.
	 * An initialize call to update(action, selection) is made for all flavors to set the initial
	 * enabled state of the underlying action.
	 * IWorkbenchWindowActionDelegate's listen to selection changes
	 * in the debug view only.
	 * </p>
	 *
	 * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction, org.eclipse.jface.viewers.ISelection)
	 */
	@Override
	public void selectionChanged(IAction action, ISelection s) {
		boolean wasInitialized = initialize(action, s);
		if (!wasInitialized) {
			if (getView() != null) {
				update(action, s);
			}
		}
	}

	protected void update(IAction action, ISelection s) {
		if (s instanceof IStructuredSelection) {
			IStructuredSelection ss = (IStructuredSelection) s;
			action.setEnabled(getEnableStateForSelection(ss));
			setSelection(ss);
		} else {
			action.setEnabled(false);
			setSelection(StructuredSelection.EMPTY);
		}
	}

	/**
	 * Performs the specific action on this element.
	 */
	protected abstract void doAction(Object element) throws DebugException;

	/**
	 * Returns the String to use as an error dialog title for
	 * a failed action. Default is to return null.
	 */
	protected String getErrorDialogTitle() {
		return null;
	}

	/**
	 * Returns the String to use as an error dialog message for
	 * a failed action. This message appears as the "Message:" in
	 * the error dialog for this action.
	 * Default is to return null.
	 */
	protected String getErrorDialogMessage() {
		return null;
	}

	/**
	 * Returns the String to use as a status message for
	 * a failed action. This message appears as the "Reason:"
	 * in the error dialog for this action.
	 * Default is to return the empty String.
	 */
	protected String getStatusMessage() {
		return ""; //$NON-NLS-1$
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IViewActionDelegate#init(org.eclipse.ui.IViewPart)
	 */
	@Override
	public void init(IViewPart view) {
		fViewPart = view;
	}

	/**
	 * Returns this action's view part, or <code>null</code>
	 * if not installed in a view.
	 *
	 * @return view part or <code>null</code>
	 */
	protected IViewPart getView() {
		return fViewPart;
	}

	/**
	 * Initialize this delegate, updating this delegate's
	 * presentation.
	 * As well, all of the flavors of AbstractDebugActionDelegates need to
	 * have the initial enabled state set with a call to update(IAction, ISelection).
	 *
	 * @param action the presentation for this action
	 * @return whether the action was initialized
	 */
	protected boolean initialize(IAction action, ISelection selection) {
		if (!isInitialized()) {
			setAction(action);
			if (getView() == null) {
				//update on the selection in the debug view
				IWorkbenchWindow window = getWindow();
				if (window != null && window.getShell() != null && !window.getShell().isDisposed()) {
					IWorkbenchPage page = window.getActivePage();
					if (page != null) {
						selection = page.getSelection(IDebugUIConstants.ID_DEBUG_VIEW);
					}
				}
			}
			update(action, selection);
			setInitialized(true);
			return true;
		}
		return false;
	}

	/**
	 * Returns the most recent selection
	 *
	 * @return structured selection
	 */
	protected IStructuredSelection getSelection() {
		return fSelection;
	}

	/**
	 * Sets the most recent selection
	 *
	 * @parm selection structured selection
	 */
	private void setSelection(IStructuredSelection selection) {
		fSelection = selection;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.ISelectionListener#selectionChanged(org.eclipse.ui.IWorkbenchPart, org.eclipse.jface.viewers.ISelection)
	 */
	@Override
	public void selectionChanged(IWorkbenchPart part, ISelection selection) {
		update(getAction(), selection);
	}

	protected void setAction(IAction action) {
		fAction = action;
	}

	protected IAction getAction() {
		return fAction;
	}

	protected void setView(IViewPart viewPart) {
		fViewPart = viewPart;
	}

	protected boolean isInitialized() {
		return fInitialized;
	}

	protected void setInitialized(boolean initialized) {
		fInitialized = initialized;
	}

	protected IWorkbenchWindow getWindow() {
		return fWindow;
	}

	protected void setWindow(IWorkbenchWindow window) {
		fWindow = window;
	}

	/**
	 * Return whether the action should be enabled or not based on the given selection.
	 */
	protected boolean getEnableStateForSelection(IStructuredSelection selection) {
		if (selection.size() == 0) {
			return false;
		}
		Iterator<?> itr = selection.iterator();
		while (itr.hasNext()) {
			Object element = itr.next();
			if (!isEnabledFor(element)) {
				return false;
			}
		}
		return true;
	}

	protected boolean isEnabledFor(Object element) {
		return true;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IActionDelegate2#runWithEvent(org.eclipse.jface.action.IAction, org.eclipse.swt.widgets.Event)
	 */
	@Override
	public void runWithEvent(IAction action, Event event) {
		run(action);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IActionDelegate2#init(org.eclipse.jface.action.IAction)
	 */
	@Override
	public void init(IAction action) {
	}
}
