/*******************************************************************************
 * Copyright (c) 2004, 2012 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.Iterator;
import java.util.List;

import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.model.IDebugElement;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaThread;

import com.sun.jdi.IncompatibleThreadStateException;

/**
 * Represent a Java thread in the threads and monitors model.
 */
public class JavaMonitorThread extends PlatformObject {

	/**
	 * The underlying thread.
	 */
	private IJavaThread fThread;

	private IThread fOriginalThread;

	/**
	 * The monitor this thread is waiting for.
	 */
	private JavaMonitor fContendedMonitor;
	/**
	 * The monitors owned by this thread.
	 */
	private JavaMonitor[] fOwnedMonitors= new JavaMonitor[0];
	/**
	 * Indicate if this thread is currently part of a deadlock.
	 */
	private boolean fIsInDeadlock;
	/**
	 * Indicate that the information for this thread need to be update, it
	 * may have changed.
	 */
	private boolean fToUpdate= true;

	/**
	 * List of JavaOwningThread and JavaWaitingThread associated with this thread.
	 */
	private List<IDebugElement> fElements= new ArrayList<>();

	/**
	 * JavaWaitingThread object used to return the JavaOwnedMonitor for this
	 * thread.
	 */
	private JavaWaitingThread fBaseWaitingThread;
	/**
	 * JavaOwningThread object uset to return the JavaWaitingMonitor for this
	 * thread.
	 */
	private JavaOwningThread fBaseOwningThread;

	public JavaMonitorThread(IJavaThread underlyingThread, IThread originalThread) {
		fThread= underlyingThread;
		fOriginalThread= originalThread;
	}

	public IJavaThread getThread() {
		return fThread;
	}

	public IThread getOriginalThread() {
		return fOriginalThread;
	}

	protected void setOriginalThread(IThread originalThread) {
		fOriginalThread= originalThread;
	}

	/**
	 * @see org.eclipse.debug.core.model.IDebugElement#getModelIdentifier()
	 */
	public String getModelIdentifier() {
		return fThread.getModelIdentifier();
	}

	/**
	 * @see org.eclipse.debug.core.model.IDebugElement#getDebugTarget()
	 */
	public IDebugTarget getDebugTarget() {
		return fThread.getDebugTarget();
	}

	/**
	 * @see org.eclipse.debug.core.model.IDebugElement#getLaunch()
	 */
	public ILaunch getLaunch() {
		return fThread.getLaunch();
	}

	/**
	 * @see org.eclipse.debug.core.model.ISuspendResume#isSuspended()
	 */
	public boolean isSuspended() {
		return fThread.isSuspended();
	}

	/**
	 * Returns the contended monitor to be used as a child
	 * of the underlying thread in the debug launch view.
	 */
	public JavaContendedMonitor getContendedMonitor() {
		if (fBaseOwningThread == null) {
			fBaseOwningThread= new JavaOwningThread(this, null);
		}
		return fBaseOwningThread.getContendedMonitor();
	}

	/**
	 * Returns the owned monitors to be used as children
	 * of the underlying thread in the debug launch view.
	 */
	public JavaOwnedMonitor[] getOwnedMonitors() {
		if (fBaseWaitingThread == null) {
			fBaseWaitingThread= new JavaWaitingThread(this, null);
		}
		return fBaseWaitingThread.getOwnedMonitors();
	}

	/**
	 * Returns the monitor this thread is waiting for.
	 */
	protected JavaMonitor getContendedMonitor0() {
		if (fToUpdate) {
			update();
		}
		return fContendedMonitor;
	}

	/**
	 * Returns the monitors owned by this thread.
	 */
	protected JavaMonitor[] getOwnedMonitors0() {
		if (fToUpdate) {
			update();
		}
		return fOwnedMonitors;
	}

	/**
	 * Update the information for this thread.
	 * @return <code>true</code> if the contended monitor or
	 * the owned monitors changed.
	 */
	private boolean update() {
		boolean changed= false;
		synchronized(this) {
			if (!fToUpdate) {
				return false;
			}
			try {
				// update the contended monitor
				IJavaObject contendedMonitor= fThread.getContendedMonitor();
				if (contendedMonitor == null) {
					changed= fContendedMonitor != null;
					fContendedMonitor= null;
				} else {
					changed= fContendedMonitor == null || !contendedMonitor.equals(fContendedMonitor.getMonitor());
					fContendedMonitor= ThreadMonitorManager.getDefault().getJavaMonitor(contendedMonitor);
				}
				// update the owned monitors
				IJavaObject[] ownedMonitors= fThread.getOwnedMonitors();
				if (ownedMonitors == null || ownedMonitors.length == 0) {
					// no owned monitor, not much to do
					changed= fOwnedMonitors != null && fOwnedMonitors.length != 0;
					fOwnedMonitors= new JavaMonitor[0];
				} else {
					JavaMonitor[] tmp= new JavaMonitor[ownedMonitors.length];
					ThreadMonitorManager threadMonitorManager= ThreadMonitorManager.getDefault();
					if (changed || fOwnedMonitors.length != ownedMonitors.length) {
						// if we know it changed, we can just create the new list.
						for (int i= 0; i < ownedMonitors.length; i++) {
							tmp[i]= threadMonitorManager.getJavaMonitor(ownedMonitors[i]);
						}
						changed= true;
					} else {
						// we need to check in the new list contains the same monitors as the
						// previous list
						int sameMonitor= 0;
						for (int i= 0; i < ownedMonitors.length; i++) {
							for (int j= 0; j < fOwnedMonitors.length; j++) {
								if (ownedMonitors[i].equals(fOwnedMonitors[j].getMonitor())) {
									sameMonitor++;
									break;
								}
							}
							tmp[i]= threadMonitorManager.getJavaMonitor(ownedMonitors[i]);
						}
						changed= sameMonitor != ownedMonitors.length;
					}
					fOwnedMonitors= tmp;
				}
			} catch (DebugException e) {
			    Throwable cause= e.getStatus().getException();
			    if (!(cause instanceof IncompatibleThreadStateException)) {
			        // IncompatibleThreadStateException are expected from Sun VMs
			        // if the thread is not suspended.
			        // For all other exceptions, null the values.
					fContendedMonitor= null;
					changed= fOwnedMonitors != null && fOwnedMonitors.length != 0;
					fOwnedMonitors= new JavaMonitor[0];
			    }
			} finally {
				fToUpdate= false;
			}
		}
		if (changed) {
			fireChangeEvent(DebugEvent.CONTENT);
		}
		return changed;
	}

	/**
	 * send a change event for theJavaWaitingThread and JavaOwningThread
	 * associated with this thread
	 */
	private void fireChangeEvent(int detail) {
		Object[] elements= fElements.toArray();
		List<Object> changedElement= new ArrayList<>();
		if (fOriginalThread != null) {
			changedElement.add(fOriginalThread);
		}
		for (int i= 0; i < elements.length; i++) {
			Object element= elements[i];
			// the two 'base' elements are not part of the hierarchy, they are
			// used to get the children of the Thread.
			if (element != fBaseOwningThread && element != fBaseWaitingThread) {
			    changedElement.add(element);
			}
		}
		DebugEvent[] changeEvents= new DebugEvent[changedElement.size()];
		int i= 0;
		for (Iterator<Object> iter= changedElement.iterator(); iter.hasNext();) {
		    changeEvents[i++]= new DebugEvent(iter.next(), DebugEvent.CHANGE, detail);
        }
		DebugPlugin.getDefault().fireDebugEventSet(changeEvents);
	}

	public synchronized void setToUpdate() {
		if (!fToUpdate) {
			fToUpdate= true;
			if (fContendedMonitor != null) {
				fContendedMonitor.setToUpdate();
			}
			if (fOwnedMonitors != null) {
				for (int i= 0; i < fOwnedMonitors.length; i++) {
					fOwnedMonitors[i].setToUpdate();
				}
			}
		}
	}

	protected void addElement(JavaOwningThread thread) {
		fElements.add(thread);
	}

	protected void addElement(JavaWaitingThread thread) {
		fElements.add(thread);
	}

	public void refresh() {
		if (fToUpdate && !update()) {
			if (fContendedMonitor != null) {
				fContendedMonitor.refresh();
			}
			for (int i= 0; i < fOwnedMonitors.length; i++) {
				fOwnedMonitors[i].refresh();
			}
		}
	}

	/**
	 * Indicate if this thread is currently part of a deadlock
	 */
	public boolean isInDeadlock() {
		return fIsInDeadlock;
	}
	/**
	 * Set this thread as being part of a deadlock.
	 */
	public void setInDeadlock(boolean isInDeadlock) {
		boolean oldValue= fIsInDeadlock;
		fIsInDeadlock = isInDeadlock;
		if (oldValue != isInDeadlock) {
			fireChangeEvent(DebugEvent.STATE);
		}
	}
}
