/*******************************************************************************
 * Copyright (c) 2010, 2012 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.jsdt.debug.internal.core.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.IDebugEventSetListener;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchListener;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.jsdt.debug.core.breakpoints.IJavaScriptBreakpoint;
import org.eclipse.wst.jsdt.debug.core.jsdi.ScriptReference;
import org.eclipse.wst.jsdt.debug.core.jsdi.ThreadReference;
import org.eclipse.wst.jsdt.debug.core.jsdi.VirtualMachine;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.DebuggerStatementEvent;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.Event;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.EventSet;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.ScriptLoadEvent;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.ThreadEnterEvent;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.ThreadExitEvent;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.VMDeathEvent;
import org.eclipse.wst.jsdt.debug.core.jsdi.event.VMDisconnectEvent;
import org.eclipse.wst.jsdt.debug.core.jsdi.request.DebuggerStatementRequest;
import org.eclipse.wst.jsdt.debug.core.jsdi.request.ScriptLoadRequest;
import org.eclipse.wst.jsdt.debug.core.jsdi.request.ThreadEnterRequest;
import org.eclipse.wst.jsdt.debug.core.jsdi.request.ThreadExitRequest;
import org.eclipse.wst.jsdt.debug.core.jsdi.request.VMDeathRequest;
import org.eclipse.wst.jsdt.debug.core.model.IJavaScriptDebugTarget;
import org.eclipse.wst.jsdt.debug.core.model.IScript;
import org.eclipse.wst.jsdt.debug.core.model.IScriptGroup;
import org.eclipse.wst.jsdt.debug.core.model.JavaScriptDebugModel;
import org.eclipse.wst.jsdt.debug.internal.core.JavaScriptDebugPlugin;
import org.eclipse.wst.jsdt.debug.internal.core.JavaScriptPreferencesManager;
import org.eclipse.wst.jsdt.debug.internal.core.breakpoints.JavaScriptBreakpoint;

/**
 * JavaScript debug target
 * 
 * @since 1.0
 */
public class JavaScriptDebugTarget extends JavaScriptDebugElement implements IJavaScriptDebugTarget, IDebugEventSetListener, ILaunchListener, IJavaScriptEventListener {

	static final String DEFAULT_NAME = ModelMessages.JSDIDebugTarget_jsdi_debug_target;
	
	/**
	 * The cached collection of scripts, sorted by name
	 */
	private ArrayList iScriptCache = null;
	
	private final IProcess process;
	private final VirtualMachine vm;
	private final ILaunch launch;
	private final boolean supportsTerminate;
	private final boolean supportsDisconnect;
	private final EventDispatcher eventDispatcher;
	private String name;
	private Object lock = new Object();

	private ArrayList threads = new ArrayList();
	private ArrayList breakpoints = new ArrayList();
	
	private IScriptGroup scriptgroup = null;

	private boolean disconnected = false;
	private boolean terminating = false;
	private boolean terminated = false;
	private boolean suspended = false;

	private ThreadEnterRequest threadEnterRequest;
	private ThreadExitRequest threadExitRequest;
	private ScriptLoadRequest scriptLoadrequest;
	private VMDeathRequest deathRequest;

	private DebuggerStatementRequest debuggerStatementRequest;
	
	/**
	 * Constructor
	 * 
	 * @param vm
	 * @param process
	 * @param launch
	 * @param supportsTerminate
	 * @param supportsDisconnect
	 */
	public JavaScriptDebugTarget(VirtualMachine vm, IProcess process, ILaunch launch, boolean supportsTerminate, boolean supportsDisconnect) {
		super(null);
		this.vm = vm;
		this.process = process;
		this.launch = launch;
		this.supportsTerminate = supportsTerminate;
		this.supportsDisconnect = supportsDisconnect;
		this.eventDispatcher = new EventDispatcher(this);
		this.scriptgroup = new ScriptGroup(this);
		initialize();
	}

	/**
	 * Initialize any threads and breakpoints existing at the time this target
	 * has been created
	 */
	public synchronized void initialize() {
		// perform initializations
		initializeThreads();
		initializeBreakpoints();

		//register listening for script load request
		scriptLoadrequest = getEventRequestManager().createScriptLoadRequest();
		scriptLoadrequest.setEnabled(true);
		addJSDIEventListener(this, scriptLoadrequest);
		
		deathRequest = getEventRequestManager().createVMDeathRequest();
		deathRequest.setEnabled(true);
		addJSDIEventListener(this, deathRequest);
		
		getLaunch().addDebugTarget(this);

		DebugPlugin plugin = DebugPlugin.getDefault();
		plugin.addDebugEventListener(this);
		plugin.getLaunchManager().addLaunchListener(this);
		fireCreationEvent();
		// begin handling/dispatching events after the creation event is handled
		// by all listeners
		plugin.asyncExec(new Runnable() {
			public void run() {
				Thread t = new Thread(eventDispatcher, "JavaScriptDebugModel.EventDispatcher"); //$NON-NLS-1$
				t.setDaemon(true);
				t.start();
			}
		});
	}

	/**
	 * Shuts down the target
	 */
	public synchronized void shutdown() {
		try {
			if (supportsTerminate) {
				terminate();
			} else if (supportsDisconnect) {
				disconnect();
			}
		} catch (DebugException e) {
			JavaScriptDebugPlugin.log(e);
			//in case we failed
			cleanup();
			fireTerminateEvent();
		}
	}

	/**
	 * Cleans up the state of the target
	 */
	void cleanup() {
		DebugPlugin plugin = DebugPlugin.getDefault();
		plugin.getLaunchManager().removeLaunchListener(this);
		plugin.removeDebugEventListener(this);
		getEventDispatcher().shutdown();
		removeAllThreads();
		removeAllBreakpoints();
		removeJSDIEventListener(this, scriptLoadrequest);
		removeJSDIEventListener(this, deathRequest);
		this.scriptgroup = null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.DebugElement#getDebugTarget()
	 */
	public IDebugTarget getDebugTarget() {
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.DebugElement#getLaunch()
	 */
	public ILaunch getLaunch() {
		return launch;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDebugTarget#getName()
	 */
	public String getName() throws DebugException {
		synchronized (lock) {
			if(name == null) {
				name = NLS.bind(ModelMessages.debug_target_name, new String[] {vm.name(), vm.version()});
			}
		}
		return name;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDebugTarget#getProcess()
	 */
	public IProcess getProcess() {
		return process;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ITerminate#canTerminate()
	 */
	public boolean canTerminate() {
		return supportsTerminate && isAvailable();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ITerminate#isTerminated()
	 */
	public boolean isTerminated() {
		return terminated;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDisconnect#canDisconnect()
	 */
	public boolean canDisconnect() {
		return supportsDisconnect && isAvailable();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDisconnect#isDisconnected()
	 */
	public boolean isDisconnected() {
		return disconnected;
	}

	/**
	 * Returns the collection of underlying {@link ScriptReference}s
	 * whose name matches the given name
	 * @param name
	 * @return the complete list of {@link ScriptReference}s matching the given name
	 * or an empty collection, never <code>null</code>
	 */
	public synchronized List underlyingScripts(String name) {
		List scripts = getVM().allScripts();
		if(!scripts.isEmpty()) {
			List byname = new ArrayList();
			ScriptReference script = null;
			for (Iterator iter = scripts.iterator(); iter.hasNext();) {
				script = (ScriptReference) iter.next();
				String sname = Script.resolveName(script.sourceURI()); 
				if (sname.equals(name)) {
					byname.add(script);
				}
			}
			return byname;
		}
		return Collections.EMPTY_LIST;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJavaScriptDebugTarget#allScriptsByName(java.lang.String)
	 */
	public synchronized List allScriptsByName(String name) {
		List scripts = allScripts();
		if(scripts.size() > 0) {
			List byname = new ArrayList();
			IScript script = null;
			for (Iterator iter = scripts.iterator(); iter.hasNext();) {
				script = (IScript) iter.next();
				if (Script.resolveName(script.sourceURI()).equals(name)) {
					byname.add(script);
				}
			}
			return byname;
		}
		return Collections.EMPTY_LIST;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJavaScriptDebugTarget#allScripts()
	 */
	public synchronized List allScripts() {
		if(iScriptCache == null) {
			List all = getVM().allScripts();
			if(all.size() > 0) { 
				iScriptCache = new ArrayList(all.size());
				for (int i = 0; i < all.size(); i++) {
					iScriptCache.add(new Script(this, (ScriptReference) all.get(i)));
				}
				Collections.sort(iScriptCache);
			}
		}
		if(iScriptCache == null) {
			return Collections.EMPTY_LIST;
		}
		return iScriptCache;
	}
	
	/**
	 * Collects all of the current threads from the {@link VirtualMachine} and
	 * adds them to the cached list
	 */
	private synchronized void initializeThreads() {
		threadEnterRequest = vm.eventRequestManager().createThreadEnterRequest();
		threadEnterRequest.setEnabled(true);
		eventDispatcher.addEventListener(this, threadEnterRequest);

		threadExitRequest = vm.eventRequestManager().createThreadExitRequest();
		threadExitRequest.setEnabled(true);
		eventDispatcher.addEventListener(this, threadExitRequest);

		List allThreads = vm.allThreads();
		ThreadReference threadReference = null;
		for (Iterator iterator = allThreads.iterator(); iterator.hasNext();) {
			threadReference = (ThreadReference) iterator.next();
			createThread(threadReference, false);
		}
	}

	/**
	 * Removes all threads from the target
	 */
	private synchronized void removeAllThreads() {
		Iterator iter = getThreadIterator();
		while (iter.hasNext()) {
			JavaScriptThread thread = (JavaScriptThread) iter.next();
			thread.terminated();
		}
		threads.clear();
		removeJSDIEventListener(this, threadEnterRequest);
		removeJSDIEventListener(this, threadExitRequest);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDebugTarget#getThreads()
	 */
	public synchronized IThread[] getThreads() throws DebugException {
		return (IThread[]) threads.toArray(new IThread[this.threads.size()]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDebugTarget#hasThreads()
	 */
	public synchronized boolean hasThreads() throws DebugException {
		return !threads.isEmpty();
	}

	/**
	 * Installs all JavaScript breakpoints that currently exist in the
	 * breakpoint manager
	 */
	synchronized void initializeBreakpoints() {
		debuggerStatementRequest = getEventRequestManager().createDebuggerStatementRequest();
		debuggerStatementRequest.setEnabled(true);
		addJSDIEventListener(this, debuggerStatementRequest);

		IBreakpointManager manager = DebugPlugin.getDefault().getBreakpointManager();
		manager.addBreakpointListener(this);
		IBreakpoint[] managerBreakpoints = manager.getBreakpoints(JavaScriptDebugModel.MODEL_ID);
		for (int i = 0; i < managerBreakpoints.length; i++) {
			breakpointAdded(managerBreakpoints[i]);
		}
		//add the managed breakpoints
		IJavaScriptBreakpoint[] breakpoints = JavaScriptPreferencesManager.getAllManagedBreakpoints();
		for (int i = 0; i < breakpoints.length; i++) {
			breakpointAdded(breakpoints[i]);
		}
	}

	/**
	 * Removes all breakpoints from this target
	 */
	private synchronized void removeAllBreakpoints() {
		Iterator iter = ((ArrayList)this.breakpoints.clone()).iterator();
		JavaScriptBreakpoint breakpoint = null;
		while (iter.hasNext()) {
			breakpoint = (JavaScriptBreakpoint) iter.next();
			breakpoint.removeFromTarget(this);
		}
		this.breakpoints.clear();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.model.IDebugTarget#supportsBreakpoint(org.eclipse.debug.core.model.IBreakpoint)
	 */
	public boolean supportsBreakpoint(IBreakpoint breakpoint) {
		return JavaScriptDebugModel.MODEL_ID.equals(breakpoint.getModelIdentifier());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ISuspendResume#canResume()
	 */
	public boolean canResume() {
		if ((isSuspended() || canResumeThreads()) && isAvailable()) {
			if (threads.size() == 0) {
				return true;
			}
			Iterator iter = getThreadIterator();
			while (iter.hasNext()) {
				IThread thread = (IThread) iter.next();
				if (thread.canResume()) {
					// if at least 1 thread can resume the target can be resumed
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * @return true if any one of the threads in the target can be resumed,
	 *         false otherwise
	 */
	private boolean canResumeThreads() {
		Iterator iter = getThreadIterator();
		IThread thread = null;
		while (iter.hasNext()) {
			thread = (IThread) iter.next();
			if (thread.canResume()) {
				return true;
			}
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ISuspendResume#canSuspend()
	 */
	public boolean canSuspend() {
		if (!isSuspended() && isAvailable()) {
			Iterator iter = getThreadIterator();
			while (iter.hasNext()) {
				IThread thread = (IThread) iter.next();
				if (thread.isSuspended()) {
					// do not allow the target to suspend if there is already a
					// suspended thread
					return false;
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * Returns an iterator over the collection of threads. The returned iterator
	 * is made on a copy of the thread list so that it is thread safe. This
	 * method should always be used instead of getThreadList().iterator()
	 * 
	 * @return an iterator over the collection of threads
	 */
	private Iterator getThreadIterator() {
		List threadList;
		synchronized (this.threads) {
			threadList = (List) this.threads.clone();
		}
		return threadList.iterator();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ISuspendResume#isSuspended()
	 */
	public boolean isSuspended() {
		return this.suspended;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ISuspendResume#resume()
	 */
	public void resume() throws DebugException {
		if (!isAvailable()) {
			// no-op if the target is not ready
			return;
		}
		// if we are resuming the target resume all of the threads before
		// resuming the target
		// this gives the threads a chance to save state, etc before the VM is
		// resumed
		Iterator iter = getThreadIterator();
		JavaScriptThread thread = null;
		while (iter.hasNext()) {
			thread = (JavaScriptThread) iter.next();
			if (thread.isSuspended()) {
				thread.targetResume();
			}
		}
		this.suspended = false;
		resumeVM(false);
		fireResumeEvent(DebugEvent.CLIENT_REQUEST);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ISuspendResume#suspend()
	 */
	public void suspend() throws DebugException {
		if (isSuspended() || !isAvailable()) {
			// no-op if the target is suspended or not ready
			return;
		}
		try {
			suspended = true;
			if (this.vm != null) {
				this.vm.suspend();
			}
			// set all owned, unsuspended threads as suspended if we suspend the
			// target
			Iterator iter = getThreadIterator();
			while (iter.hasNext()) {
				JavaScriptThread thread = (JavaScriptThread) iter.next();
				if (!thread.isSuspended()) {
					thread.markSuspended();
				}
			}
		} finally {
			fireSuspendEvent(DebugEvent.CLIENT_REQUEST);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJavaScriptDebugTarget#getScriptGroup()
	 */
	public IScriptGroup getScriptGroup() {
		return scriptgroup;
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IDisconnect#disconnect()
	 */
	public void disconnect() throws DebugException {
		if (!isAvailable()) {
			// already done
			return;
		}
		if (!this.supportsDisconnect) {
			notSupported(NLS.bind(
					ModelMessages.JSDIDebugTarget_not_support_disconnect,
					getName()), null);
		}
		try {
			// first resume the VM, do not leave it in a suspended state
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304574
			resumeVM(true);
			if(this.process != null) {
				this.process.terminate();
			}
		}  
		catch(RuntimeException rte) {}
		finally {
			cleanup();
			disposeVM(true);
			this.disconnected = true;
			fireTerminateEvent();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.ITerminate#terminate()
	 */
	public void terminate() throws DebugException {
		if (!isAvailable()) {
			// already done
			return;
		}
		if (!this.supportsTerminate) {
			notSupported(NLS.bind(
					ModelMessages.JSDIDebugTarget_not_support_terminate,
					getName()), null);
		}
		this.terminating = true;
		try {
			// first resume the VM, do not leave it in a suspended state
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304574
			resumeVM(true);
			// next terminate the underlying process
			if (this.process != null) {
				this.process.terminate();
			}
			this.terminated = true;
		} finally {
			disposeVM(true);
			cleanup();
			this.terminating = false;
			fireTerminateEvent();
		}
	}

	/**
	 * disposes the underlying {@link VirtualMachine}
	 * 
	 * @param shutdown
	 * @throws DebugException
	 */
	void disposeVM(boolean shutdown) throws DebugException {
		if(this.vm != null) {
			try {
				this.vm.dispose();
			}
			catch(RuntimeException rte) {
				if(!shutdown) {
					disconnect();
				}
			}
		}
	}
	
	/**
	 * resumes the underlying {@link VirtualMachine}
	 * 
	 * @param shutdown if the method is being called during a terminating call
	 * @throws DebugException 
	 */
	void resumeVM(boolean shutdown) throws DebugException {
		if(this.vm != null) {
			try {
				this.vm.resume();
			}
			catch(RuntimeException rte) {
				if(!shutdown) {
					disconnect();
				}
			}
		}
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.model.IMemoryBlockRetrieval#getMemoryBlock(long,
	 * long)
	 */
	public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
		notSupported(ModelMessages.JSDIDebugTarget_unsupported_operation, null);
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.model.IMemoryBlockRetrieval#supportsStorageRetrieval
	 * ()
	 */
	public boolean supportsStorageRetrieval() {
		return false;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.internal.core.model.JavaScriptDebugElement#getVM()
	 */
	public VirtualMachine getVM() {
		return this.vm;
	}

	/**
	 * @return if the target is available to be disconnected or terminated
	 */
	public boolean isAvailable() {
		return !(this.terminated || this.terminating || this.disconnected);
	}

	/**
	 * @return the event dispatcher
	 */
	EventDispatcher getEventDispatcher() {
		return this.eventDispatcher;
	}

	/**
	 * Delegate method to create a new {@link JavaScriptThread} and add it to the list
	 * of threads
	 * 
	 * @param thread the underlying {@link ThreadReference}
	 * @return a new {@link JavaScriptThread}
	 */
	private synchronized JavaScriptThread createThread(ThreadReference thread, boolean fireEvent) {
		if (isDisconnected()) {
			return null;
		}
		JavaScriptThread jsdiThread = findThread(thread);
		if (jsdiThread != null) {
			jsdiThread.fireChangeEvent(DebugEvent.CONTENT);
			return jsdiThread;
		}
		jsdiThread = new JavaScriptThread(this, thread);
		this.threads.add(jsdiThread);
		if (fireEvent) {
			jsdiThread.fireCreationEvent();
		}
		return jsdiThread;
	}

	/**
	 * Terminates the given {@link ThreadReference} if the target is not
	 * disconnected
	 * 
	 * @param thread
	 */
	private synchronized void terminateThread(ThreadReference thread) {
		if (isDisconnected()) {
			return;
		}
		JavaScriptThread jsdiThread = findThread(thread);
		if (jsdiThread == null) {
			return;
		}
		threads.remove(jsdiThread);
		jsdiThread.markTerminated();
		jsdiThread.fireTerminateEvent();
	}

	/**
	 * Finds the {@link JavaScriptThread} mapped to the given {@link ThreadReference}
	 * 
	 * @param thread
	 * @return the mapped {@link JavaScriptThread} or <code>null</code>
	 */
	public synchronized JavaScriptThread findThread(ThreadReference thread) {
		for (Iterator iterator = threads.iterator(); iterator.hasNext();) {
			JavaScriptThread jsdiThread = (JavaScriptThread) iterator.next();
			if (jsdiThread.matches(thread)) {
				return jsdiThread;
			}
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.model.DebugElement#getAdapter(java.lang.Class)
	 */
	public Object getAdapter(Class adapter) {
		if (adapter == JavaScriptDebugTarget.class) {
			return this;
		}
		return super.getAdapter(adapter);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.IBreakpointListener#breakpointAdded(org.eclipse.debug.core.model.IBreakpoint)
	 */
	public synchronized void breakpointAdded(IBreakpoint breakpoint) {
		if (!isAvailable() || !supportsBreakpoint(breakpoint)) {
			// no-op either not ready or we don't care about the given
			// breakpoint
			return;
		}
		try {
			synchronized (this.breakpoints) {
				if(!this.breakpoints.contains(breakpoint)) {
					if(!shouldSkipBreakpoint(breakpoint)) {
						//only add to the VM if it should not be skipped
						((JavaScriptBreakpoint) breakpoint).addToTarget(this);
					}
					this.breakpoints.add(breakpoint);
				}
			}
		} catch (CoreException ce) {
			JavaScriptDebugPlugin.log(ce);
		}
	}

	/**
	 * Returns if the given breakpoint should be skipped
	 * 
	 * @param breakpoint
	 * @return <code>true</code> if the breakpoint should be skipped, <code>false</code> otherwise
	 */
	boolean shouldSkipBreakpoint(IBreakpoint breakpoint) {
		try {
			DebugPlugin plugin = DebugPlugin.getDefault();
			return plugin != null && breakpoint.isRegistered() && !plugin.getBreakpointManager().isEnabled();
		}
		catch(CoreException ce) {
			//there is something wrong, skip it
			return true;
		}
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.IBreakpointListener#breakpointChanged(org.eclipse.debug.core.model.IBreakpoint, org.eclipse.core.resources.IMarkerDelta)
	 */
	public synchronized void breakpointChanged(IBreakpoint breakpoint,IMarkerDelta delta) {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.IBreakpointListener#breakpointRemoved(org.eclipse.debug.core.model.IBreakpoint, org.eclipse.core.resources.IMarkerDelta)
	 */
	public synchronized void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
		if (!isAvailable() || !supportsBreakpoint(breakpoint)) {
			// no-op either not ready or we don't care about the breakpoint
			return;
		}
		((JavaScriptBreakpoint) breakpoint).removeFromTarget(this);
		synchronized (this.breakpoints) {
			this.breakpoints.remove(breakpoint);
		}
		// remove cached breakpoints from threads
		if (this.threads != null) {
			for (Iterator iter = this.threads.iterator(); iter.hasNext();) {
				((JavaScriptThread) iter.next()).removeBreakpoint((JavaScriptBreakpoint) breakpoint);
			}
		}
	}

	/**
	 * Returns the live list of breakpoints currently set in this target
	 * 
	 * @return the live list of breakpoints
	 */
	public List getBreakpoints() {
		return this.breakpoints;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.debug.core.IDebugEventSetListener#handleDebugEvents(org.eclipse
	 * .debug.core.DebugEvent[])
	 */
	public void handleDebugEvents(DebugEvent[] events) {
		if (events.length == 1) {
			DebugEvent event = events[0];
			switch(event.getKind()) {
				case DebugEvent.TERMINATE: {
					if(event.getSource().equals(getProcess())) {
						shutdown();
					}
					break;
				}
				case DebugEvent.CHANGE: {
					if(event.getSource().equals(scriptgroup)) {
						if(iScriptCache != null) {
							iScriptCache.clear();
							iScriptCache = null;
						}
					}
					break;
				}
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.ILaunchListener#launchRemoved(org.eclipse.debug.core.ILaunch)
	 */
	public void launchRemoved(ILaunch launch) {
		if (!isAvailable()) {
			return;
		}
		if (launch.equals(getLaunch())) {
			// This target has been unregistered, but it hasn't successfully
			// terminated.
			// Update internal state to reflect that it is disconnected
			disconnected();
		}
	}

	/**
	 * delegate to clean up if the target has been disconnected and a framework
	 * method has been called
	 */
	protected void disconnected() {
		if (!isDisconnected()) {
			this.disconnected = true;
			shutdown();
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.ILaunchListener#launchAdded(org.eclipse.debug.core.ILaunch)
	 */
	public void launchAdded(ILaunch launch) {
		// ignore
	}

	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.ILaunchListener#launchChanged(org.eclipse.debug.core.ILaunch)
	 */
	public void launchChanged(ILaunch launch) {
		// ignore
	}

	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJSDIEventListener#eventSetComplete(org.eclipse.wst.jsdt.debug.core.jsdi.event.Event, org.eclipse.wst.jsdt.debug.core.model.JSDIDebugTarget, boolean, org.eclipse.wst.jsdt.debug.core.jsdi.event.EventSet)
	 */
	public void eventSetComplete(Event event, JavaScriptDebugTarget target, boolean suspend, EventSet eventSet) {
		// thread enter
		// thread exit
		// script?

		if (event instanceof DebuggerStatementEvent) {
			DebuggerStatementEvent debuggerStatementEvent = (DebuggerStatementEvent) event;
			ThreadReference threadReference = debuggerStatementEvent.thread();
			JavaScriptThread thread = findThread(threadReference);
			thread.fireSuspendEvent(DebugEvent.BREAKPOINT);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJSDIEventListener#handleEvent(org.eclipse.wst.jsdt.debug.core.jsdi.event.Event, org.eclipse.wst.jsdt.debug.core.model.JSDIDebugTarget, boolean, org.eclipse.wst.jsdt.debug.core.jsdi.event.EventSet)
	 */
	public synchronized boolean handleEvent(Event event, JavaScriptDebugTarget target, boolean suspendVote, EventSet eventSet) {
		if (event instanceof ThreadEnterEvent) {
			ThreadEnterEvent threadEnterEvent = (ThreadEnterEvent) event;
			createThread(threadEnterEvent.thread(), true);
			return false;
		}
		if (event instanceof ThreadExitEvent) {
			ThreadExitEvent threadExitEvent = (ThreadExitEvent) event;
			terminateThread(threadExitEvent.thread());
			//need to update the script node in the event any scripts have been unloaded when the thread exits
			if(scriptgroup != null) {
				fireEvent(new DebugEvent(scriptgroup, DebugEvent.CHANGE, DebugEvent.CONTENT));
			}
			return false;
		}
		if (event instanceof ScriptLoadEvent) {
			if(iScriptCache != null) {
				iScriptCache.clear();
				iScriptCache = null;
			}
			fireEvent(new DebugEvent(this.scriptgroup, DebugEvent.MODEL_SPECIFIC, EventDispatcher.EVENT_SCRIPT_LOADED));
			return true;
		}

		if (event instanceof DebuggerStatementEvent) {
			DebuggerStatementEvent debuggerStatementEvent = (DebuggerStatementEvent) event;
			ThreadReference threadReference = debuggerStatementEvent.thread();
			JavaScriptThread thread = findThread(threadReference);
			if(!thread.isSuspended()) {
				thread.markSuspended();
			}
			return false;
		}

		// handle VM events i.e. death / disconnect
		if (event instanceof VMDeathEvent) {
			shutdown();
			return true;
		}
		if (event instanceof VMDisconnectEvent) {
			try {
				if (!this.disconnected) {
					eventCleanup();
				}
			} finally {
				shutdown();
			}
			return false;
		}
		throw new IllegalArgumentException(NLS.bind(
				ModelMessages.JSDIDebugTarget_recieved_unknown_event, event
						.toString()));
	}

	/**
	 * Cleans up the target
	 */
	void eventCleanup() {
		try {
			cleanup();
		} finally {
			this.disconnected = true;
			this.terminated = true;
			fireTerminateEvent();
		}
	}
	
	/**
	 * @return if the backing {@link VirtualMachine} can update breakpoints
	 */
	public boolean canUpdateBreakpoints() {
		return this.vm.canUpdateBreakpoints();
	}
	
	/**
	 * Update the given breakpoint
	 * 
	 * @param breakpoint
	 */
	public void updateBreakpoint(IJavaScriptBreakpoint breakpoint) {
		if(this.breakpoints.contains(breakpoint)) {
			if(this.vm.canUpdateBreakpoints()) {
				this.vm.updateBreakpoint(breakpoint);
			}
		}
	}
}
