/*******************************************************************************
 * Copyright (c) 2005, 2015 Freescale Semiconductor, Inc.
 *
 * 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:
 *     Freescale Semiconductor, Inc. - initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.debug.internal.ui.views.memory;

import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import org.eclipse.cdt.debug.core.model.provisional.IMemorySpaceAwareMemoryBlockRetrieval;
import org.eclipse.cdt.debug.internal.core.CRequest;
import org.eclipse.cdt.debug.ui.CDebugUIPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IDebugElement;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IMemoryBlockExtension;
import org.eclipse.debug.core.model.IMemoryBlockRetrieval;
import org.eclipse.debug.internal.ui.views.memory.AddMemoryBlockAction;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.debug.ui.actions.IAddMemoryBlocksTarget;
import org.eclipse.debug.ui.contexts.IDebugContextService;
import org.eclipse.debug.ui.memory.IMemoryRendering;
import org.eclipse.debug.ui.memory.IMemoryRenderingContainer;
import org.eclipse.debug.ui.memory.IMemoryRenderingSite;
import org.eclipse.debug.ui.memory.IMemoryRenderingType;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.progress.UIJob;

/**
 * Adds memory blocks to the Memory view.
 *
 * <p>
 * CDT adapter logic will link us to a CMemoryBlockRetrievalExtension
 * if and only if the CDI backend support memory spaces. When this is the case,
 * the platform will call us to add a memory monitor to the Memory view. We
 * must put up a dialog, handle the user input, create the memory blocks
 * with default renderings and add them to the view.
 *
 * <p>
 * @since 3.2
 *
 */
public class AddMemoryBlocks implements IAddMemoryBlocksTarget {

	/** Request object used to get the memory spaces */
	private static class GetMemorySpacesRequest extends CRequest
			implements IMemorySpaceAwareMemoryBlockRetrieval.GetMemorySpacesRequest {
		String[] fMemorySpaces = new String[0];

		@Override
		public String[] getMemorySpaces() {
			return fMemorySpaces;
		}

		@Override
		public void setMemorySpaces(String[] memorySpaceIds) {
			fMemorySpaces = memorySpaceIds;
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.debug.ui.actions.IAddMemoryBlocksTarget#addMemoryBlocks(org.eclipse.ui.IWorkbenchPart, org.eclipse.jface.viewers.ISelection)
	 */
	@Override
	public void addMemoryBlocks(final IWorkbenchPart part, final ISelection selection) throws CoreException {

		if (!(part instanceof IMemoryRenderingSite)) {
			assert false : "unexpected kind of view part"; //$NON-NLS-1$
			return;
		}
		final IMemoryRenderingSite renderingSite = (IMemoryRenderingSite) part;

		final IAdaptable debugViewElement = DebugUITools.getDebugContext();

		IMemoryBlockRetrieval retrieval = debugViewElement.getAdapter(IMemoryBlockRetrieval.class);
		if (retrieval == null && debugViewElement instanceof IDebugElement) {
			// Added logic for CDI (which is based on the standard debug model)
			retrieval = ((IDebugElement) debugViewElement).getDebugTarget();
		}

		// If the backend doesn't support memory spaces, use the standard
		// (platform) dialog for adding a memory block
		if (!(retrieval instanceof IMemorySpaceAwareMemoryBlockRetrieval)) {
			invokePlatformAction(renderingSite);
			return;
		}
		final IMemorySpaceAwareMemoryBlockRetrieval msRetrieval = (IMemorySpaceAwareMemoryBlockRetrieval) retrieval;

		// We still don't really know if this session actually involves any
		// memory spaces. Finding out is not trivial since it requires an
		// asynchronous call.
		final Object context = getContextSelectionForPart(part);
		Job job = new Job("update memory space choices") { //$NON-NLS-1$
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				msRetrieval.getMemorySpaces(context, new GetMemorySpacesRequest() {
					@Override
					public void done() {
						runOnUIThread(() -> {
							if (isSuccess()) {
								String[] memorySpaces = getMemorySpaces();

								// We shouldn't be using the custom dialog
								// if there are none or only one memory
								// spaces involved.
								// https://bugs.eclipse.org/bugs/show_bug.cgi?id=309032#c50
								if (memorySpaces.length >= 2) {
									doAddMemoryBlocks(renderingSite, context, msRetrieval, memorySpaces);
									return;
								}
							}

							// If we get here, then the custom dialog isn't
							// necessary. Use the standard (platform) one
							invokePlatformAction(renderingSite);
						});
					}
				});
				return Status.OK_STATUS;
			}
		};
		job.setSystem(true);
		job.schedule();
	}

	/**
	 * Invoke the platform's Add Memory Block action.
	 *
	 * @param site the rendering site
	 */
	void invokePlatformAction(IMemoryRenderingSite site) {
		// We have to subclass to get access to the protected dispose method
		class PlatformAction extends AddMemoryBlockAction {
			PlatformAction(IMemoryRenderingSite site) {
				super(site);
			}

			@Override
			protected void dispose() {
				super.dispose();
			}
		}
		PlatformAction action = new PlatformAction(site);
		action.run();
		action.dispose();
	}

	private void doAddMemoryBlocks(final IMemoryRenderingSite renderingSite, final Object context,
			final IMemorySpaceAwareMemoryBlockRetrieval retrieval, String[] memorySpaces) {
		Shell shell = CDebugUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getShell();

		// create dialog to ask for expression/address to block
		AddMemoryBlockDialog dialog = new AddMemoryBlockDialog(shell, memorySpaces);
		dialog.open();
		int returnCode = dialog.getReturnCode();
		if (returnCode == Window.CANCEL)
			return;

		String input = dialog.enteredExpression() ? dialog.getExpression() : dialog.getAddress();

		List<String> list = new ArrayList<>();

		if (input.length() == 0)
			list.add(""); //$NON-NLS-1$
		else {
			StringTokenizer tokenizer = new StringTokenizer(input, ","); //$NON-NLS-1$
			while (tokenizer.hasMoreTokens())
				list.add(tokenizer.nextToken());
		}

		final String[] addrsOrExprs = list.toArray(new String[list.size()]);

		ParamHolder params;
		if (dialog.enteredExpression())
			params = new ExpressionsHolder(addrsOrExprs);
		else
			params = new AddressAndSpaceHolder(addrsOrExprs, dialog.getMemorySpace());

		final ParamHolder params_f = params;
		Job job = new Job("Add Memory Block") { //$NON-NLS-1$
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				addMemoryBlocks(context, retrieval, params_f, renderingSite);
				return Status.OK_STATUS;
			}
		};
		job.setSystem(true);
		job.schedule();

	}

	@Override
	public boolean canAddMemoryBlocks(IWorkbenchPart part, ISelection selection) throws CoreException {
		return true;
	}

	@Override
	public boolean supportsAddMemoryBlocks(IWorkbenchPart part) {
		return (IDebugUIConstants.ID_MEMORY_VIEW.equals(part.getSite().getId()));
	}

	// In order to avoid duplicating the addMemoryBlocks method--one
	// version for expressions, one for memory-space+address, we pass in a
	// an opaque parameter and let the logic within addMemoryBlocks
	// differentiate where needed via isinstanceof

	private class ParamHolder {
	}

	class AddressAndSpaceHolder extends ParamHolder {
		public AddressAndSpaceHolder(final String[] addresses, final String memorySpace) {
			this.addresses = addresses;
			this.memorySpace = memorySpace;
		}

		public String[] addresses;

		public String memorySpace;
	}

	private class ExpressionsHolder extends ParamHolder {
		public ExpressionsHolder(final String[] expressions) {
			this.expressions = expressions;
		}

		public String[] expressions;
	}

	private void addMemoryBlocks(Object context, IMemorySpaceAwareMemoryBlockRetrieval memRetrieval,
			final ParamHolder params, IMemoryRenderingSite memRendSite) {

		final String[] addrsOrExprs = (params instanceof AddressAndSpaceHolder)
				? ((AddressAndSpaceHolder) params).addresses
				: ((ExpressionsHolder) params).expressions;

		for (int i = 0; i < addrsOrExprs.length; i++) {
			String addrOrExpr = addrsOrExprs[i].trim();
			try {

				// get extended memory block with the expression or address
				// entered
				IMemoryBlockExtension memBlock;

				if (params instanceof AddressAndSpaceHolder)
					memBlock = memRetrieval.getMemoryBlock(addrOrExpr, context,
							((AddressAndSpaceHolder) params).memorySpace);
				else
					memBlock = memRetrieval.getExtendedMemoryBlock(addrOrExpr, context);

				// add block to memory block manager
				if (memBlock != null) {
					IMemoryBlock[] memArray = new IMemoryBlock[] { memBlock };

					DebugPlugin.getDefault().getMemoryBlockManager().addMemoryBlocks(memArray);

					addDefaultRenderings(memBlock, memRendSite);
				} else {
					// open error if it failed to retrieve a memory block
					openError(Messages.AddMemBlocks_title, Messages.AddMemBlocks_noMemoryBlock, null);
				}
			} catch (DebugException e1) {
				openError(Messages.AddMemBlocks_title, Messages.AddMemBlocks_failed, e1);
			} catch (NumberFormatException e2) {
				String message = Messages.AddMemBlocks_failed + "\n" + Messages.AddMemBlocks_input_invalid; //$NON-NLS-1$
				openError(Messages.AddMemBlocks_title, message, null);
			}
		}
	}

	private void addDefaultRenderings(IMemoryBlock memoryBlock, IMemoryRenderingSite memRendSite) {

		// This method was mostly lifted from the platform's AddMemoryBlockAction

		IMemoryRenderingType primaryType = DebugUITools.getMemoryRenderingManager()
				.getPrimaryRenderingType(memoryBlock);
		IMemoryRenderingType renderingTypes[] = DebugUITools.getMemoryRenderingManager()
				.getDefaultRenderingTypes(memoryBlock);

		// create primary rendering
		try {
			if (primaryType != null) {
				createRenderingInContainer(memoryBlock, memRendSite, primaryType,
						IDebugUIConstants.ID_RENDERING_VIEW_PANE_1);
			} else if (renderingTypes.length > 0) {
				primaryType = renderingTypes[0];
				createRenderingInContainer(memoryBlock, memRendSite, renderingTypes[0],
						IDebugUIConstants.ID_RENDERING_VIEW_PANE_1);
			}
		} catch (CoreException e1) {
			CDebugUIPlugin.log(e1);
		}

		for (int i = 0; i < renderingTypes.length; i++) {
			try {
				boolean create = true;
				if (primaryType != null) {
					if (primaryType.getId().equals(renderingTypes[i].getId()))
						create = false;
				}
				if (create)
					createRenderingInContainer(memoryBlock, memRendSite, renderingTypes[i],
							IDebugUIConstants.ID_RENDERING_VIEW_PANE_2);
			} catch (CoreException e) {
				CDebugUIPlugin.log(e);
			}
		}
	}

	private void createRenderingInContainer(IMemoryBlock memoryBlock, IMemoryRenderingSite memRendSite,
			IMemoryRenderingType primaryType, String paneId) throws CoreException {

		// This method was mostly lifted from the platform's AddMemoryBlockAction

		IMemoryRendering rendering = primaryType.createRendering();
		IMemoryRenderingContainer container = memRendSite.getContainer(paneId);
		rendering.init(container, memoryBlock);
		container.addMemoryRendering(rendering);
	}

	/**
	 * Helper function to open an error dialog.
	 * @param title
	 * @param message
	 * @param e
	 */
	static public void openError(final String title, final String message, final Exception e) {
		UIJob uiJob = new UIJob("open error") { //$NON-NLS-1$

			@Override
			public IStatus runInUIThread(IProgressMonitor monitor) {
				// open error for the exception
				String detail = ""; //$NON-NLS-1$
				if (e != null)
					detail = e.getMessage();

				Shell shell = CDebugUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getShell();

				MessageDialog.openError(shell, title, message + "\n" + detail); //$NON-NLS-1$
				return Status.OK_STATUS;
			}
		};
		uiJob.setSystem(true);
		uiJob.schedule();
	}

	private static Object getContextSelectionForPart(IWorkbenchPart part) {
		IDebugContextService contextService = DebugUITools.getDebugContextManager()
				.getContextService(part.getSite().getWorkbenchWindow());

		ISelection debugContext = contextService.getActiveContext(getPartId(part));
		if (debugContext == null) {
			debugContext = contextService.getActiveContext();
		}

		if (debugContext instanceof IStructuredSelection) {
			return ((IStructuredSelection) debugContext).getFirstElement();
		}

		return null;
	}

	private static String getPartId(IWorkbenchPart part) {
		if (part instanceof IViewPart) {
			IViewSite site = (IViewSite) part.getSite();
			return site.getId() + (site.getSecondaryId() != null ? (":" + site.getSecondaryId()) : ""); //$NON-NLS-1$ //$NON-NLS-2$
		} else {
			return part.getSite().getId();
		}
	}

	/**
	 * Execute runnable on UI thread if the current thread is not an UI thread.
	 * Otherwise execute it directly.
	 *
	 * @param runnable
	 *            the runnable to execute
	 */
	private void runOnUIThread(final Runnable runnable) {
		if (Display.getCurrent() != null) {
			runnable.run();
		} else {
			UIJob job = new UIJob("Memory Browser UI Job") { //$NON-NLS-1$
				@Override
				public IStatus runInUIThread(IProgressMonitor monitor) {
					runnable.run();
					return Status.OK_STATUS;
				}
			};
			job.setSystem(true);
			job.schedule();
		}
	}
}
