/*******************************************************************************
 * Copyright (c) 2000, 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.propertypages;

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

import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IDebugElement;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;

/**
 *
 */
public class ThreadFilterEditor {

	private JavaBreakpointAdvancedPage fPage;
	private CheckboxTreeViewer fThreadViewer;
	private ThreadFilterContentProvider fContentProvider;
	private CheckHandler fCheckHandler;
	private static String MAIN= "main"; //$NON-NLS-1$

	public ThreadFilterEditor(Composite parent, JavaBreakpointAdvancedPage page) {
		fPage= page;
		fContentProvider= new ThreadFilterContentProvider();
		fCheckHandler= new CheckHandler();
		createThreadViewer(parent);
	}

	private void createThreadViewer(Composite parent) {
		Label label= new Label(parent, SWT.NONE);
		label.setText(PropertyPageMessages.ThreadFilterEditor_1);
		label.setFont(parent.getFont());
		label.setLayoutData(new GridData());

		GridData data= new GridData(GridData.FILL_BOTH);
		data.heightHint= 100;
		fThreadViewer= new CheckboxTreeViewer(parent, SWT.BORDER);
		fThreadViewer.addCheckStateListener(fCheckHandler);
		fThreadViewer.getTree().setLayoutData(data);
		fThreadViewer.getTree().setFont(parent.getFont());
		fThreadViewer.setContentProvider(fContentProvider);
		fThreadViewer.setLabelProvider(DebugUITools.newDebugModelPresentation());
		fThreadViewer.setInput(DebugPlugin.getDefault().getLaunchManager());
		setInitialCheckedState();
	}

	protected void doStore() {
		IDebugTarget[] targets= getDebugTargets();
		IJavaDebugTarget target;
		IThread[] threads;
		IJavaThread thread;
		for (int i= 0, numTargets= targets.length; i < numTargets; i++) {
			target = targets[i].getAdapter(IJavaDebugTarget.class);
			if (target != null) {
				try {
					if (fThreadViewer.getChecked(target)) {
						threads= target.getThreads();
						for (int j=0, numThreads= threads.length; j < numThreads; j++) {
							thread= (IJavaThread)threads[j];
							if (fThreadViewer.getChecked(thread)) {
								// thread selected for filtering
								fPage.getBreakpoint().setThreadFilter(thread);
								break; // Can only set one filtered thread.
							}
						}
					} else {
						fPage.getBreakpoint().removeThreadFilter(target);
					}
				} catch (CoreException e) {
					JDIDebugUIPlugin.log(e);
				}
			}
		}
	}

	/**
	 * Sets the initial checked state of the tree viewer.
	 * The initial state should reflect the current state
	 * of the breakpoint. If the breakpoint has a thread
	 * filter in a given thread, that thread should be
	 * checked.
	 */
	protected void setInitialCheckedState() {
		try {
			IDebugTarget[] targets= getDebugTargets();
			for (int i= 0, numTargets= targets.length; i < numTargets; i++) {
				IJavaDebugTarget target = targets[i].getAdapter(IJavaDebugTarget.class);
				if (target != null) {
					IJavaThread filteredThread= fPage.getBreakpoint().getThreadFilter(target);
					if (filteredThread != null) {
						fCheckHandler.checkThread(filteredThread, true);
					}
				}
			}
		} catch (CoreException e) {
			JDIDebugUIPlugin.log(e);
		}
	}

	/**
	 * Returns the debug targets that appear in the tree
	 */
	protected IDebugTarget[] getDebugTargets() {
		Object input= fThreadViewer.getInput();
		if (!(input instanceof ILaunchManager)) {
			return new IJavaDebugTarget[0];
		}
		ILaunchManager launchManager= (ILaunchManager)input;
		return launchManager.getDebugTargets();
	}

	class CheckHandler implements ICheckStateListener {
		@Override
		public void checkStateChanged(CheckStateChangedEvent event) {
			Object element= event.getElement();
			if (element instanceof IDebugTarget) {
				checkTarget((IDebugTarget)element, event.getChecked());
			} else if (element instanceof IThread) {
				checkThread((IThread)element, event.getChecked());
			}
			verifyCheckedState();
		}

		/**
		 * Check or uncheck a debug target in the tree viewer.
		 * When a debug target is checked, attempt to
		 * check one of the target's threads by default.
		 * When a debug target is unchecked, uncheck all
		 * its threads.
		 */
		protected void checkTarget(IDebugTarget target, boolean checked) {
			fThreadViewer.setChecked(target, checked);
			if (checked) {
				fThreadViewer.expandToLevel(target, AbstractTreeViewer.ALL_LEVELS);
				IThread[] threads;
				try {
					threads= target.getThreads();
				} catch (DebugException exception) {
					JDIDebugUIPlugin.log(exception);
					return;
				}
				IThread thread;
				boolean checkedThread= false;
				// Try to check the "main" thread by default
				for (int i= 0, numThreads= threads.length; i < numThreads; i++) {
					thread= threads[i];
					String name= null;
					try {
						name= thread.getName();
					} catch (DebugException exception) {
						JDIDebugUIPlugin.log(exception);
					}
					if (MAIN.equals(name)) {
						checkedThread= fThreadViewer.setChecked(thread, true);
					}
				}
				// If the main thread couldn't be checked, check the first
				// available thread
				if (!checkedThread) {
					for (int i= 0, numThreads= threads.length; i < numThreads; i++) {
						if (fThreadViewer.setChecked(threads[i], true)) {
							break;
						}
					}
				}
			} else { // Unchecked
				IThread[] threads;
				try {
					threads= target.getThreads();
				} catch (DebugException exception) {
					JDIDebugUIPlugin.log(exception);
					return;
				}
				for (int i= 0, numThreads= threads.length; i < numThreads; i++) {
					fThreadViewer.setChecked(threads[i], false);
				}
			}
		}

		/**
		 * Check or uncheck a thread.
		 * When a thread is checked, make sure its debug
		 * target is also checked.
		 * When a thread is unchecked, uncheck its debug
		 * target.
		 */
		protected void checkThread(IThread thread, boolean checked) {
			fThreadViewer.setChecked(thread, checked);
			IDebugTarget target= (thread).getDebugTarget();
			if (checked) {
				// When a thread is checked, make sure the target
				// is checked and all other threads are
				// unchecked (simulate radio button behavior)
				if (!fThreadViewer.getChecked(target)) {
					fThreadViewer.setChecked(target, true);
				}
				IThread[] threads;
				try {
					threads= target.getThreads();
				} catch (DebugException exception) {
					JDIDebugUIPlugin.log(exception);
					return;
				}
				for (int i= 0, numThreads= threads.length; i < numThreads; i++) {
					if (threads[i] != thread) {
						// Uncheck all threads other than the selected thread
						fThreadViewer.setChecked(threads[i], false);
					}
				}
			} else {
				// When a thread is unchecked, uncheck the target
				fThreadViewer.setChecked(target, false);
			}
		}

		/**
		 * Verify the state of the tree viewer.
		 * If the user selects a debug target, they must select
		 * a thread.
		 */
		protected void verifyCheckedState() {
			IDebugTarget[] targets= getDebugTargets();
			IDebugTarget target;
			IThread[] threads;
			boolean checkedThread;
			for (int i= 0, numTargets= targets.length; i < numTargets; i++) {
				target= targets[i];
				if (!fThreadViewer.getChecked(target)) {
					continue;
				}
				try {
					threads= target.getThreads();
				} catch (DebugException exception) {
					JDIDebugUIPlugin.log(exception);
					continue;
				}
				checkedThread= false;
				for (int j= 0, numThreads= threads.length; j < numThreads; j++) {
					if (fThreadViewer.getChecked(threads[j])) {
						checkedThread= true;
						break;
					}
				}
				if (checkedThread) {
					fPage.setErrorMessage(null);
				} else {
					fPage.setErrorMessage(PropertyPageMessages.ThreadFilterEditor_2);
				}
			}
		}

	}

	class ThreadFilterContentProvider implements ITreeContentProvider {
		/**
		 * @see ITreeContentProvider#getChildren(Object)
		 */
		@Override
		public Object[] getChildren(Object parent) {
			if (parent instanceof IDebugTarget) {
				IJavaDebugTarget target = ((IDebugTarget)parent).getAdapter(IJavaDebugTarget.class);
				if (target != null) {
					try {
						return ((IJavaDebugTarget)parent).getThreads();
					} catch (DebugException e) {
						JDIDebugUIPlugin.log(e);
					}
				}
			}
			if (parent instanceof ILaunchManager) {
				List<IJavaDebugTarget> children= new ArrayList<>();
				ILaunch[] launches= ((ILaunchManager) parent).getLaunches();
				IDebugTarget[] targets;
				IJavaDebugTarget target;
				for (int i= 0, numLaunches= launches.length; i < numLaunches; i++) {
					targets= launches[i].getDebugTargets();
					for (int j= 0, numTargets= targets.length; j < numTargets; j++) {
						target= targets[j].getAdapter(IJavaDebugTarget.class);
						if (target != null && !target.isDisconnected() && !target.isTerminated()) {
							children.add(target);
						}
					}
				}
				return children.toArray();
			}
			return new Object[0];
		}

		/**
		 * @see ITreeContentProvider#getParent(Object)
		 */
		@Override
		public Object getParent(Object element) {
			if (element instanceof IThread) {
				return ((IThread)element).getDebugTarget();
			}
			if (element instanceof IDebugTarget) {
				return ((IDebugElement)element).getLaunch();
			}
			if (element instanceof ILaunch) {
				return DebugPlugin.getDefault().getLaunchManager();
			}
			return null;
		}

		/**
		 * @see ITreeContentProvider#hasChildren(Object)
		 */
		@Override
		public boolean hasChildren(Object element) {
			if (element instanceof IStackFrame) {
				return false;
			}
			if (element instanceof IDebugElement) {
				return getChildren(element).length > 0;
			}
			if (element instanceof ILaunch) {
				return true;
			}
			if (element instanceof ILaunchManager) {
				return ((ILaunchManager) element).getLaunches().length > 0;
			}
			return false;
		}

		/**
		 * @see IStructuredContentProvider#getElements(Object)
		 */
		@Override
		public Object[] getElements(Object inputElement) {
			return getChildren(inputElement);
		}

		/**
		 * @see IContentProvider#dispose()
		 */
		@Override
		public void dispose() {
		}

		/**
		 * @see IContentProvider#inputChanged(Viewer, Object, Object)
		 */
		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}
	}
}
