/*******************************************************************************
 * Copyright (c) 2004, 2015 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.jdt.internal.debug.ui.monitors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IDebugEventSetListener;
import org.eclipse.debug.core.model.IDebugElement;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.ui.IJavaDebugUIConstants;
import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPreferenceInitializer;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

/**
 * Manager for the thread and monitor model.
 */
public class ThreadMonitorManager implements IDebugEventSetListener, IPropertyChangeListener {

	private static ThreadMonitorManager fDefaultManager;

	/**
	 * HashMap IJavaThread -> JavaMonitorThread
	 */
	private HashMap<IDebugElement, Object> fJavaMonitorThreads;
	/**
	 * HashMap IJavaObject -> JavaMonitor
	 */
	private HashMap<IDebugElement, Object> fJavaMonitors;

	private boolean fIsEnabled;

	/**
	 * Returns the default ThreadMonitorManager object.
	 */
	public static ThreadMonitorManager getDefault() {
		if (fDefaultManager == null) {
			fDefaultManager= new ThreadMonitorManager();
		}
		return fDefaultManager;
	}

	private ThreadMonitorManager() {
		fJavaMonitorThreads= new HashMap<>();
		fJavaMonitors= new HashMap<>();
		IPreferenceStore preferenceStore = JDIDebugUIPlugin.getDefault().getPreferenceStore();
		preferenceStore.addPropertyChangeListener(this);
		fIsEnabled= preferenceStore.getBoolean(IJavaDebugUIConstants.PREF_SHOW_MONITOR_THREAD_INFO);
		if (fIsEnabled) {
			DebugPlugin.getDefault().addDebugEventListener(this);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.IDebugEventSetListener#handleDebugEvents(org.eclipse.debug.core.DebugEvent[])
	 */
	@Override
	public void handleDebugEvents(DebugEvent[] events) {
		for (int i= 0; i < events.length; i++) {
			DebugEvent debugEvent= events[i];
			Object eventSource= debugEvent.getSource();
			int eventKind= debugEvent.getKind();
			IJavaThread javaThread = null;
			if (eventSource instanceof IAdaptable) {
				IAdaptable adaptable = (IAdaptable)eventSource;
				javaThread = adaptable.getAdapter(IJavaThread.class);
				if (javaThread != null) {
					switch (eventKind) {
						case DebugEvent.SUSPEND:
						case DebugEvent.RESUME:
							// refresh on suspend/resume
							if (debugEvent.getDetail() != DebugEvent.EVALUATION_IMPLICIT) {
								handleSuspendResume();
							}
							break;
						case DebugEvent.TERMINATE:
							// clean the thread map when a thread terminates
							handleThreadTerminate(javaThread);
							break;
					}
				} else {
					IJavaDebugTarget target = adaptable.getAdapter(IJavaDebugTarget.class);
					if (target != null) {
						switch (eventKind) {
							case DebugEvent.SUSPEND:
							case DebugEvent.RESUME:
								// refresh on suspend/resume
								if (debugEvent.getDetail() != DebugEvent.EVALUATION_IMPLICIT) {
									handleSuspendResume();
								}
								break;
							case DebugEvent.TERMINATE:
								// clean the maps when a target terminates
								handleDebugTargetTerminate(target);
								break;
						}
					}
				}
			}
		}
	}

	private void handleSuspendResume() {
		JavaMonitorThread[] threads = getJavaMonitorThreads();
		for (int i = 0; i < threads.length; i++) {
			threads[i].setToUpdate();
		}
		DebugPlugin.getDefault().asyncExec(new RefreshAndDetectDeadlock());
	}

	private void handleThreadTerminate(IJavaThread thread) {
		// remove this thread
		synchronized(fJavaMonitorThreads) {
			fJavaMonitorThreads.remove(thread);
		}
	}

	private void handleDebugTargetTerminate(IJavaDebugTarget debugTarget) {
		// remove the threads and monitors for this debug target.
		clean(fJavaMonitors, debugTarget);
		clean(fJavaMonitorThreads, debugTarget);
	}

	private void clean(Map<IDebugElement, Object> map, IJavaDebugTarget debugTarget) {
		IDebugElement debugElements[] = null;
		synchronized(map) {
			debugElements = new IDebugElement[map.size()];
			debugElements =	map.keySet().toArray(debugElements);
		}
		for(int i = 0; i < debugElements.length; ++i) {
			if (debugElements[i].getDebugTarget().equals(debugTarget)) {
				synchronized(map) {
					map.remove(debugElements[i]);
				}
			}
		}
	}

	/**
	 * Returns the unique JavaMonitorThread object for the given thread.
	 */
	protected JavaMonitorThread getJavaMonitorThread(IJavaThread thread, IThread originalThread) {
		synchronized (fJavaMonitorThreads) {
			JavaMonitorThread javaMonitorThread= (JavaMonitorThread) fJavaMonitorThreads.get(thread);
			if (javaMonitorThread == null) {
				javaMonitorThread= new JavaMonitorThread(thread, originalThread);
				fJavaMonitorThreads.put(thread, javaMonitorThread);
				DebugPlugin.getDefault().asyncExec(new DetectDeadlock());
			} else if (originalThread != null) {
				javaMonitorThread.setOriginalThread(originalThread);
			}
			return javaMonitorThread;
		}
	}

	/**
	 * Returns the unique JavaMonitor object for the given monitor.
	 */
	protected JavaMonitor getJavaMonitor(IJavaObject monitor) {
		synchronized (fJavaMonitors) {
			JavaMonitor javaMonitor= (JavaMonitor) fJavaMonitors.get(monitor);
			if (javaMonitor == null) {
				javaMonitor= new JavaMonitor(monitor);
				fJavaMonitors.put(monitor, javaMonitor);
			}
			return javaMonitor;
		}
	}

	/**
	 * Removes a monitor from the monitor map.
	 */
	protected void removeJavaMonitor(JavaMonitor monitor) {
		synchronized(fJavaMonitors) {
			fJavaMonitors.remove(monitor.getMonitor());
		}
	}

	/**
	 * Returns the monitor the given thread is waiting for.
	 */
	public JavaContendedMonitor getContendedMonitor(IThread thread) {
        IJavaThread javaThread = thread.getAdapter(IJavaThread.class);
		if (javaThread == null || !fIsEnabled || !((IJavaDebugTarget)javaThread.getDebugTarget()).supportsMonitorInformation()) {
			return null;
		}
		return getJavaMonitorThread(javaThread, thread).getContendedMonitor();
	}

	/**
	 * Returns the monitors the given thread owns.
	 */
	public JavaOwnedMonitor[] getOwnedMonitors(IThread thread) {
        IJavaThread javaThread = thread.getAdapter(IJavaThread.class);
    	if (javaThread == null || !fIsEnabled || !((IJavaDebugTarget)javaThread.getDebugTarget()).supportsMonitorInformation()) {
    		return new JavaOwnedMonitor[0];
    	}
    	return getJavaMonitorThread(javaThread, thread).getOwnedMonitors();
	}

	/**
	 *  Runnable to be run asynchronously, to refresh the model and
	 *  look for deadlocks.
	 */
	class RefreshAndDetectDeadlock extends DetectDeadlock {
		@Override
		public void run() {
			JavaMonitorThread[] threads= getJavaMonitorThreads();
			for (int i = 0; i < threads.length; i++) {
				threads[i].refresh();
			}
			super.run();
		}
	}

	class DetectDeadlock implements Runnable {
		@Override
		public void run() {
			JavaMonitorThread[] threads= getJavaMonitorThreads();
			JavaMonitor[] monitors= getJavaMonitors();
			List<Object> inDeadlock= new ArrayList<>();
			for (int i = 0; i < threads.length; i++) {
				JavaMonitorThread thread= threads[i];
				List<JavaMonitorThread> threadStack= new ArrayList<>();
				List<JavaMonitor> monitorStack= new ArrayList<>();
				while (thread != null) {
					boolean isInDeadlock= false;
					if (inDeadlock.contains(thread) || threadStack.contains(thread)) {
						isInDeadlock= true;
					} else {
						JavaMonitor monitor = thread.getContendedMonitor0();
						if (monitor == null) {
							thread= null;
						} else if (inDeadlock.contains(monitor)) {
							isInDeadlock= true;
						} else {
							threadStack.add(thread);
							monitorStack.add(monitor);
							thread= monitor.getOwningThread0();
						}
					}
					if (isInDeadlock) {
						// is in a deadlock, set the elements of the back trace as 'in a deadlock'
						for (Iterator<JavaMonitorThread> iter = threadStack.iterator(); iter.hasNext();) {
							inDeadlock.add(iter.next());
						}
						for (Iterator<JavaMonitor> iter = monitorStack.iterator(); iter.hasNext();) {
							inDeadlock.add(iter.next());
						}
						thread= null;
					}
				}
			}
			for (int i = 0; i < threads.length; i++) {
				JavaMonitorThread thread= threads[i];
				thread.setInDeadlock(inDeadlock.contains(thread));
			}
			for (int i = 0; i < monitors.length; i++) {
				JavaMonitor monitor= monitors[i];
				monitor.setInDeadlock(inDeadlock.contains(monitor));
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
	 */
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		if (event.getProperty().equals(IJavaDebugUIConstants.PREF_SHOW_MONITOR_THREAD_INFO)) {
			fIsEnabled= JDIDebugUIPreferenceInitializer.getBoolean(event);
			if (fIsEnabled) {
				DebugPlugin.getDefault().addDebugEventListener(this);
			} else {
				DebugPlugin.getDefault().removeDebugEventListener(this);
			}
		}
	}

	/**
	 * Returns <code>true</code> if SHOW_MONITOR_THREAD_INFO is on and the given thread is
	 * in a deadlock, <code>false</code> otherwise.
	 */
	public boolean isInDeadlock(IThread thread) {
        IJavaThread javaThread = thread.getAdapter(IJavaThread.class);
		if (!fIsEnabled || !((IJavaDebugTarget)javaThread.getDebugTarget()).supportsMonitorInformation()) {
			return false;
		}
		return getJavaMonitorThread(javaThread, thread).isInDeadlock();
	}

	private JavaMonitor[] getJavaMonitors() {
		synchronized(fJavaMonitors) {
			JavaMonitor[] monitors = new JavaMonitor[fJavaMonitors.size()];
			return fJavaMonitors.values().toArray(monitors);
		}
	}

	private JavaMonitorThread[] getJavaMonitorThreads() {
		synchronized(fJavaMonitorThreads) {
			JavaMonitorThread[] threads = new JavaMonitorThread[fJavaMonitorThreads.size()];
			return fJavaMonitorThreads.values().toArray(threads);
		}
	}
}
