/*******************************************************************************
 * Copyright (c) 2004, 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
 *	   WindRiver - Bug 192028 [Memory View] Memory view does not
 *                 display memory blocks that do not reference IDebugTarget
 *******************************************************************************/

package org.eclipse.debug.internal.core;

import java.util.ArrayList;

import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IDebugEventSetListener;
import org.eclipse.debug.core.IMemoryBlockListener;
import org.eclipse.debug.core.IMemoryBlockManager;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IMemoryBlockExtension;
import org.eclipse.debug.core.model.IMemoryBlockRetrieval;


/**
 * Implementation of IMemoryBlockManager
 * The manager is responsible to manage all memory blocks in the workbench.
 *
 * @since 3.1
 */
public class MemoryBlockManager implements IMemoryBlockManager, IDebugEventSetListener {

	private ArrayList<IMemoryBlockListener> listeners = new ArrayList<>();
	private ArrayList<IMemoryBlock> memoryBlocks = new ArrayList<>();

	private static final int ADDED = 0;
	private static final int REMOVED = 1;
	/**
	 * Notifies a memory block listener in a safe runnable to
	 * handle exceptions.
	 */
	class MemoryBlockNotifier implements ISafeRunnable {

		private IMemoryBlockListener fListener;
		private int fType;
		private IMemoryBlock[] fMemoryBlocks;

		@Override
		public void handleException(Throwable exception) {
			DebugPlugin.log(exception);
		}

		@Override
		public void run() throws Exception {
			switch (fType) {
				case ADDED:
					fListener.memoryBlocksAdded(fMemoryBlocks);
					break;
				case REMOVED:
					fListener.memoryBlocksRemoved(fMemoryBlocks);
					break;
				default:
					break;
			}
		}

		/**
		 * Notify listeners of added/removed memory block events
		 *
		 * @param memBlocks blocks that have changed
		 * @param update type of change
		 */
		public void notify(IMemoryBlock[] memBlocks, int update) {
			if (listeners != null) {
				fType = update;
				Object[] copiedListeners= listeners.toArray(new IMemoryBlockListener[listeners.size()]);
				for (int i= 0; i < copiedListeners.length; i++) {
					fListener = (IMemoryBlockListener)copiedListeners[i];
					fMemoryBlocks = memBlocks;
                    SafeRunner.run(this);
				}
			}
			fListener = null;
			fMemoryBlocks = null;
		}
	}

	/**
	 * Returns the <code>MemoryBlockNotifier</code>
	 * @return the <code>MemoryBlockNotifier</code>
	 *
	 * TODO consider using only one of these, and sync where needed,
	 * this way we are not creating a new every single time.
	 */
	private MemoryBlockNotifier getMemoryBlockNotifier() {
		return new MemoryBlockNotifier();
	}

	@Override
	public void addMemoryBlocks(IMemoryBlock[] mem) {
		if (memoryBlocks == null) {
			return;
		}
		if (mem == null) {
			DebugPlugin.logMessage("Null argument passed into IMemoryBlockManager.addMemoryBlock", null); //$NON-NLS-1$
			return;
		}

		if(mem.length > 0) {
			ArrayList<IMemoryBlock> newMemoryBlocks = new ArrayList<>();
			for (int i=0; i<mem.length; i++) {
				// do not allow duplicates
				if (!memoryBlocks.contains(mem[i])) {
					newMemoryBlocks.add(mem[i]);
					memoryBlocks.add(mem[i]);
					// add listener for the first memory block added
					if (memoryBlocks.size() == 1) {
						DebugPlugin.getDefault().addDebugEventListener(this);
					}
				}
			}
			notifyListeners(newMemoryBlocks.toArray(new IMemoryBlock[newMemoryBlocks.size()]), ADDED);
		}
	}

	@Override
	public void removeMemoryBlocks(IMemoryBlock[] memBlocks) {
		if (memoryBlocks == null) {
			return;
		}
		if (memBlocks == null){
			DebugPlugin.logMessage("Null argument passed into IMemoryBlockManager.removeMemoryBlock", null); //$NON-NLS-1$
			return;
		}

		if(memBlocks.length > 0) {
			for (int i=0; i<memBlocks.length; i++) {
				memoryBlocks.remove(memBlocks[i]);
				// remove listener after the last memory block has been removed
				if (memoryBlocks.size() == 0) {
					DebugPlugin.getDefault().removeDebugEventListener(this);
				}
				if (memBlocks[i] instanceof IMemoryBlockExtension) {
					try {
						((IMemoryBlockExtension)memBlocks[i]).dispose();
					} catch (DebugException e) {
						DebugPlugin.log(e);
					}
				}
			}
			notifyListeners(memBlocks, REMOVED);
		}
	}

	@Override
	public void addListener(IMemoryBlockListener listener) {

		if(listeners == null) {
			return;
		}
		if(listener == null) {
			DebugPlugin.logMessage("Null argument passed into IMemoryBlockManager.addListener", null); //$NON-NLS-1$
			return;
		}
		if (!listeners.contains(listener)) {
			listeners.add(listener);
		}
	}

	@Override
	public void removeListener(IMemoryBlockListener listener) {

		if(listeners == null) {
			return;
		}
		if(listener == null) {
			DebugPlugin.logMessage("Null argument passed into IMemoryBlockManager.removeListener", null); //$NON-NLS-1$
			return;
		}
		if (listeners.contains(listener)) {
			listeners.remove(listener);
		}
	}

	@Override
	public IMemoryBlock[] getMemoryBlocks() {
		return memoryBlocks.toArray(new IMemoryBlock[memoryBlocks.size()]);
	}

	@Override
	public IMemoryBlock[] getMemoryBlocks(IDebugTarget debugTarget) {
		ArrayList<IMemoryBlock> memoryBlocksList = new ArrayList<>();
		for (IMemoryBlock block : memoryBlocks) {
			if (block.getDebugTarget() == debugTarget) {
				memoryBlocksList.add(block);
			}
		}
		return memoryBlocksList.toArray(new IMemoryBlock[memoryBlocksList.size()]);
	}

	@Override
	public IMemoryBlock[] getMemoryBlocks(IMemoryBlockRetrieval retrieve) {
		ArrayList<IMemoryBlock> memoryBlocksList = new ArrayList<>();
		for (IMemoryBlock block : memoryBlocks) {
			if (block instanceof IMemoryBlockExtension) {
				if (((IMemoryBlockExtension) block).getMemoryBlockRetrieval() == retrieve) {
					memoryBlocksList.add(block);
				}
			}
			else {
				IMemoryBlockRetrieval mbRetrieval = block.getAdapter(IMemoryBlockRetrieval.class);
				// standard memory block always uses the debug target as the memory block retrieval
				if (mbRetrieval == null) {
					mbRetrieval = block.getDebugTarget();
				}
				if (mbRetrieval == retrieve) {
					memoryBlocksList.add(block);
				}
			}
		}
		return memoryBlocksList.toArray(new IMemoryBlock[memoryBlocksList.size()]);
	}

	/**
	 * Notifies the listeners about the given memory blocks and the event to be sent
	 * @param memBlocks the array of memory blocks
	 * @param event the event to notify to the blocks
	 */
	private void notifyListeners(IMemoryBlock[] memBlocks, int event) {
		getMemoryBlockNotifier().notify(memBlocks, event);
	}

	@Override
	public void handleDebugEvents(DebugEvent[] events) {
		for (int i=0; i < events.length; i++) {
			handleDebugEvent(events[i]);
		}
	}

	/**
	 * Handles a debug event
	 * @param event the {@link DebugEvent}
	 */
	public void handleDebugEvent(DebugEvent event) {
		Object obj = event.getSource();
		IDebugTarget dt = null;

		if (event.getKind() == DebugEvent.TERMINATE) {
			// a terminate event could happen from an IThread or IDebugTarget
			// only handle a debug event from the debug target
			if (obj instanceof IDebugTarget) {
				dt = ((IDebugTarget)obj);

				// getMemoryBlocks will return an empty array if it is null
				IMemoryBlock[] deletedMemoryBlocks = getMemoryBlocks(dt);
				removeMemoryBlocks(deletedMemoryBlocks);
			}
		}
	}

	/**
	 * Clean up when the plugin is shut down
	 */
	public void shutdown() {
		if (listeners != null) {
			listeners.clear();
			listeners = null;
		}

		if (memoryBlocks != null) {
			memoryBlocks.clear();
			memoryBlocks = null;
		}
	}

}
