/*******************************************************************************
 * Copyright (c) 2005, 2022 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
 *     Ian Bull <irbull@cs.uvic.ca> - bug 204404 and bug 207064
 *     EclipseSource Corporation - ongoing enhancements
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 487943
 *     Karsten Thoms <karsten.thoms@itemis.de> - Bug 522332
 *******************************************************************************/
package org.eclipse.pde.internal.ui.launcher;

import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.ModelEntry;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.build.IPDEBuildConstants;
import org.eclipse.pde.internal.core.DependencyManager;
import org.eclipse.pde.internal.core.DependencyManager.Options;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.launching.launcher.BundleLauncherHelper;
import org.eclipse.pde.internal.launching.launcher.LaunchValidationOperation;
import org.eclipse.pde.internal.ui.PDELabelProvider;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEPluginImages;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.SWTFactory;
import org.eclipse.pde.internal.ui.elements.NamedElement;
import org.eclipse.pde.internal.ui.shared.CachedCheckboxTreeViewer;
import org.eclipse.pde.internal.ui.shared.FilteredCheckboxTree;
import org.eclipse.pde.internal.ui.util.PersistablePluginObject;
import org.eclipse.pde.internal.ui.util.SWTUtil;
import org.eclipse.pde.internal.ui.util.SourcePluginFilter;
import org.eclipse.pde.internal.ui.wizards.ListUtil;
import org.eclipse.pde.launching.IPDELauncherConstants;
import org.eclipse.pde.ui.launcher.AbstractLauncherTab;
import org.eclipse.pde.ui.launcher.BundlesTab;
import org.eclipse.pde.ui.launcher.PluginsTab;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.TreeEditor;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.IWorkingSetSelectionDialog;
import org.eclipse.ui.dialogs.PatternFilter;
import org.osgi.resource.Resource;

public abstract class AbstractPluginBlock {

	protected AbstractLauncherTab fTab;

	private FilteredCheckboxTree fPluginFilteredTree;
	protected CachedCheckboxTreeViewer fPluginTreeViewer;
	private NamedElement fWorkspacePlugins;
	private NamedElement fExternalPlugins;
	private IPluginModelBase[] fExternalModels;
	private IPluginModelBase[] fWorkspaceModels;
	protected int fNumExternalChecked;
	protected int fNumWorkspaceChecked;

	private Button fAutoIncludeRequirementsButton;
	private boolean fAutoIncludeRequirementsButtonChanged;
	private Button fIncludeOptionalButton;
	protected Button fAddWorkspaceButton;
	private Button fAutoValidate;

	private Button fSelectAllButton;
	private Button fDeselectButton;
	private Button fWorkingSetButton;
	private Button fAddRequiredButton;
	private Button fDefaultsButton;
	private Button fFilterButton;

	/**
	 * The configuration this block is currently displaying or <code>null</code>
	 * if none set.
	 */
	protected ILaunchConfiguration fLaunchConfig;
	private final Listener fListener = new Listener();

	private Label fCounter;

	private Button fValidateButton;

	private HashMap<Object, String> levelColumnCache = new HashMap<>();
	private HashMap<Object, String> autoColumnCache = new HashMap<>();
	private TreeEditor levelColumnEditor = null;
	private TreeEditor autoColumnEditor = null;
	private boolean fIsDisposed = false;

	private PluginStatusDialog fDialog;

	class PluginModelNameBuffer {
		private final Set<String> nameSet;

		PluginModelNameBuffer() {
			super();
			nameSet = new HashSet<>();
		}

		void add(IPluginModelBase model) {
			nameSet.add(getPluginName(model));
		}

		private String getPluginName(IPluginModelBase model) {
			String startLevel = null;
			String autoStart = null;
			if (fPluginTreeViewer.isCheckedLeafElement(model)) {
				startLevel = levelColumnCache.get(model) != null ? levelColumnCache.get(model) : null;
				autoStart = autoColumnCache.get(model) != null ? autoColumnCache.get(model) : null;
			}
			return BundleLauncherHelper.formatBundleEntry(model, startLevel, autoStart);
		}

		public Set<String> getNameSet() {
			return nameSet;
		}

		@Override
		public String toString() {
			return String.join(",", nameSet); //$NON-NLS-1$
		}
	}

	/**
	 * Label provider for the tree.
	 */
	class OSGiLabelProvider extends PDELabelProvider {

		@Override
		public Image getColumnImage(Object obj, int index) {
			return index == 0 ? super.getColumnImage(obj, index) : null;
		}

		@Override
		public String getColumnText(Object obj, int index) {
			boolean isChecked = fPluginTreeViewer.getChecked(obj);
			switch (index) {
				case 0 :
					return super.getColumnText(obj, index);
				case 1 :
					if (isChecked && levelColumnCache != null && levelColumnCache.containsKey(obj)) {
						return levelColumnCache.get(obj);
					}
					return ""; //$NON-NLS-1$
				case 2 :
					if (isChecked && autoColumnCache != null && autoColumnCache.containsKey(obj)) {
						return autoColumnCache.get(obj);
					}
					return ""; //$NON-NLS-1$
				default :
					return ""; //$NON-NLS-1$
			}
		}
	}

	class Listener extends SelectionAdapter {

		private void filterAffectingControl(SelectionEvent e) {
			boolean resetFilterButton = false;
			Object source = e.getSource();

			// If the filter is on, turn it off, apply the action, and turn it back on.
			// This has to happen this way because there is no real model behind
			// the view.  The only model is the actual plug-in model, and the state
			// does not get set on that model until an apply is performed.
			if (fFilterButton.getSelection()) {
				fFilterButton.setSelection(false);
				handleFilterButton();
				resetFilterButton = true;
			}
			if (source == fSelectAllButton) {
				toggleGroups(true);
			} else if (source == fDeselectButton) {
				toggleGroups(false);
			} else if (source == fWorkingSetButton) {
				handleWorkingSets();
			} else if (source == fAddRequiredButton) {
				addRequiredPlugins();
			} else if (source == fDefaultsButton) {
				handleRestoreDefaults();
			}

			if (resetFilterButton) {
				fFilterButton.setSelection(true);
				handleFilterButton();
			}
		}

		private void handleWorkingSets() {
			IWorkingSetManager workingSetManager = PlatformUI.getWorkbench().getWorkingSetManager();
			IWorkingSetSelectionDialog dialog = workingSetManager.createWorkingSetSelectionDialog(getShell(), true);
			if (dialog.open() == Window.OK) {
				String[] ids = getPluginIDs(dialog.getSelection());
				ArrayList<IPluginModelBase> newCheckedModels = new ArrayList<>();
				ArrayList<Object> allCheckedModels = new ArrayList<>();
				for (String id : ids) {
					IPluginModelBase model = PluginRegistry.findModel(id);
					if (model != null) {
						if (!fPluginTreeViewer.getChecked(model)) {
							newCheckedModels.add(model);
						}
					}
				}
				Object[] checkedElements = fPluginTreeViewer.getCheckedElements();
				allCheckedModels.addAll(Arrays.asList(checkedElements));// previous
				allCheckedModels.addAll(newCheckedModels);// newly selected
				fPluginTreeViewer.setCheckedElements(allCheckedModels.toArray());
				// reset text on newly selected models
				for (IPluginModelBase iPluginModelBase : newCheckedModels) {
					resetText(iPluginModelBase);
				}
				countSelectedModels();
			}
		}

		private String[] getPluginIDs(IWorkingSet[] workingSets) {
			HashSet<String> set = new HashSet<>();
			for (IWorkingSet workingSet : workingSets) {
				IAdaptable[] elements = workingSet.getElements();
				for (IAdaptable element2 : elements) {
					Object element = element2;
					if (element instanceof PersistablePluginObject) {
						set.add(((PersistablePluginObject) element).getPluginID());
					} else {
						if (element instanceof IJavaProject) {
							element = ((IJavaProject) element).getProject();
						}
						if (element instanceof IProject) {
							IPluginModelBase model = PluginRegistry.findModel((IProject) element);
							if (model != null) {
								set.add(model.getPluginBase().getId());
							}
						}
					}
				}
			}
			return set.toArray(new String[set.size()]);
		}

		@Override
		public void widgetSelected(SelectionEvent e) {
			Object source = e.getSource();

			if (source == fFilterButton) {
				handleFilterButton();
			} else if (source == fSelectAllButton || source == fDeselectButton || source == fWorkingSetButton || source == fAddRequiredButton || source == fDefaultsButton) {
				// These are all the controls that may affect the filtering.  For example, the filter
				// is enabled only to show selected bundles, and the user invokes "select all", we need
				// to update the filter.
				filterAffectingControl(e);
			} else if (source == fValidateButton) {
				handleValidate();
			}
			if (!fIsDisposed) {
				fTab.updateLaunchConfigurationDialog();
			}
		}
	}

	class PluginContentProvider implements ITreeContentProvider {
		@Override
		public boolean hasChildren(Object parent) {
			return !(parent instanceof IPluginModelBase);
		}

		@Override
		public Object[] getChildren(Object parent) {
			if (parent == fExternalPlugins) {
				return getExternalModels();
			}
			if (parent == fWorkspacePlugins) {
				return getWorkspaceModels();
			}
			return new Object[0];
		}

		@Override
		public Object getParent(Object child) {
			if (child instanceof IPluginModelBase) {
				IResource resource = ((IPluginModelBase) child).getUnderlyingResource();
				return resource == null ? fExternalPlugins : fWorkspacePlugins;
			}
			return null;
		}

		@Override
		public Object[] getElements(Object input) {
			ArrayList<NamedElement> list = new ArrayList<>();
			if (getWorkspaceModels().length > 0) {
				list.add(fWorkspacePlugins);
			}
			if (getExternalModels().length > 0) {
				list.add(fExternalPlugins);
			}
			return list.toArray();
		}
	}

	public AbstractPluginBlock(AbstractLauncherTab tab) {
		fTab = tab;
		PDEPlugin.getDefault().getLabelProvider().connect(this);
	}

	/**
	 * Returns an array of external plugins that are currently enabled.
	 * @return array of external enabled plugins, possibly empty
	 */
	protected IPluginModelBase[] getExternalModels() {
		if (fExternalModels == null) {
			fExternalModels = collectModelsToDisplay(PluginRegistry.getExternalModels());
		}
		return fExternalModels;
	}

	/**
	 * Returns an array of plugins from the workspace.  Non-OSGi plugins (no valid bundle
	 * manifest) will be filtered out.
	 * @return array of workspace OSGi plugins, possibly empty
	 */
	protected IPluginModelBase[] getWorkspaceModels() {
		if (fWorkspaceModels == null) {
			fWorkspaceModels = collectModelsToDisplay(PluginRegistry.getWorkspaceModels());
		}
		return fWorkspaceModels;
	}

	private IPluginModelBase[] collectModelsToDisplay(IPluginModelBase[] models) {
		SourcePluginFilter sourcePluginFilter = new SourcePluginFilter();
		return Arrays.stream(models)
				.filter(model -> model.getBundleDescription() != null)
				.filter(sourcePluginFilter)
				.toArray(IPluginModelBase[]::new);
	}

	protected void updateCounter() {
		if (fCounter != null) {
			int checked = fNumExternalChecked + fNumWorkspaceChecked;
			int total = getWorkspaceModels().length + getExternalModels().length;
			fCounter.setText(NLS.bind(PDEUIMessages.AbstractPluginBlock_counter, Integer.valueOf(checked), Integer.valueOf(total)));
		}
	}

	public void createControl(Composite parent, int span, int indent) {
		createPluginViewer(parent, span - 1, indent);
		createButtonContainer(parent);

		if (fTab instanceof PluginsTab) {
			fAutoIncludeRequirementsButton = createButton(parent, span, indent, PDEUIMessages.AdvancedLauncherTab_autoIncludeRequirements_plugins );
		} else if (fTab instanceof BundlesTab) {
			fAutoIncludeRequirementsButton = createButton(parent, span, indent, PDEUIMessages.AdvancedLauncherTab_autoIncludeRequirements_bundles);
		}
		fAutoIncludeRequirementsButton.addSelectionListener(
				SelectionListener.widgetSelectedAdapter(e -> this.fAutoIncludeRequirementsButtonChanged = true));

		if (fTab instanceof PluginsTab) {
			fIncludeOptionalButton = createButton(parent, span, indent,PDEUIMessages.AdvancedLauncherTab_includeOptional_plugins);
		}else if (fTab instanceof BundlesTab) {
			fIncludeOptionalButton = createButton(parent, span, indent, PDEUIMessages.AdvancedLauncherTab_includeOptional_bundles);
		}else{
			fIncludeOptionalButton = createButton(parent, span, indent, NLS.bind(PDEUIMessages.AdvancedLauncherTab_includeOptional, fTab.getName().toLowerCase(Locale.ENGLISH)));
		}
		if (fTab instanceof PluginsTab) {
			fAddWorkspaceButton = createButton(parent, span, indent, PDEUIMessages.AdvancedLauncherTab_addNew_plugins);
		}else if (fTab instanceof BundlesTab) {
			fAddWorkspaceButton = createButton(parent, span, indent,PDEUIMessages.AdvancedLauncherTab_addNew_bundles);
		}else{
			fAddWorkspaceButton = createButton(parent, span, indent, NLS.bind(PDEUIMessages.AdvancedLauncherTab_addNew, fTab.getName().toLowerCase(Locale.ENGLISH)));
		}

		GridData gd = new GridData(GridData.FILL_HORIZONTAL);
		gd.horizontalSpan = span;
		Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
		label.setLayoutData(gd);

		if (fTab instanceof PluginsTab) {
			fAutoValidate = createButton(parent, span - 1, indent, PDEUIMessages.PluginsTabToolBar_auto_validate_plugins);
		} else if (fTab instanceof BundlesTab) {
			fAutoValidate = createButton(parent, span - 1, indent, PDEUIMessages.PluginsTabToolBar_auto_validate_bundles);
		} else{
			fAutoValidate = createButton(parent, span - 1, indent,
					NLS.bind(PDEUIMessages.PluginsTabToolBar_auto_validate,
							fTab.getName().replace("&", "").toLowerCase(Locale.ENGLISH))); //$NON-NLS-1$ //$NON-NLS-2$
		}

		fValidateButton = new Button(parent, SWT.PUSH);
		fValidateButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END));

		if (fTab instanceof PluginsTab) {
			fValidateButton.setText(PDEUIMessages.PluginsTabToolBar_validate_plugins);
		} else if (fTab instanceof BundlesTab) {
			fValidateButton.setText(PDEUIMessages.PluginsTabToolBar_validate_bundles);
		} else {
			fValidateButton
					.setText(NLS.bind(PDEUIMessages.PluginsTabToolBar_validate, fTab.getName().replace("&", ""))); //$NON-NLS-1$ //$NON-NLS-2$
		}

		SWTUtil.setButtonDimensionHint(fValidateButton);
		fValidateButton.addSelectionListener(fListener);
	}

	private Button createButton(Composite parent, int span, int indent, String text) {
		Button button = new Button(parent, SWT.CHECK);
		button.setText(text);

		GridData gd = new GridData();
		gd.horizontalSpan = span;
		gd.horizontalIndent = indent;
		button.setLayoutData(gd);
		button.addSelectionListener(fListener);
		button.setFont(parent.getFont());
		return button;
	}

	protected ILabelProvider getLabelProvider() {
		return new OSGiLabelProvider();
	}

	protected void createPluginViewer(Composite composite, int span, int indent) {
		PatternFilter patternFilter = new PatternFilter();
		patternFilter.setIncludeLeadingWildcard(true);
		fPluginFilteredTree = new FilteredCheckboxTree(composite, null, getTreeViewerStyle(), patternFilter);
		fPluginTreeViewer = fPluginFilteredTree.getCheckboxTreeViewer();

		fPluginTreeViewer.addCheckStateListener(event -> {
			// Since a check on the root of a CheckBoxTreeViewer selects all its children
			// (hidden or not), we need to ensure that all items are shown
			// if this happens.  Since it not clear what the best behaviour is here
			// this just "un-selects" the filter button.

			if (!event.getChecked())
			 {
				return; // just return if the check state goes to false
			}
			// It is not clear if this is the best approach, but it
			// is hard to tell without user feedback.
			TreeItem[] items = fPluginTreeViewer.getTree().getItems();
			for (TreeItem item : items) {
				if (event.getElement() == item.getData()) {
					// If the even happens on the root of the tree
					fFilterButton.setSelection(false);
					handleFilterButton();
					return;
				}
			}
		});
		fPluginTreeViewer.setContentProvider(new PluginContentProvider());
		fPluginTreeViewer.setLabelProvider(getLabelProvider());
		fPluginTreeViewer.setAutoExpandLevel(2);
		fPluginTreeViewer.addCheckStateListener(event -> {
			Object element = event.getElement();
			if (element instanceof IPluginModelBase) {
				handleCheckStateChanged(event);
			} else if (element instanceof NamedElement namedElement) {
				TreeItem item = (TreeItem) fPluginTreeViewer.testFindItem(namedElement);
				if (item != null) {
					if (item.getText().equals("Workspace")) { //$NON-NLS-1$
						fNumWorkspaceChecked = event.getChecked() ? getWorkspaceModels().length : 0;
					} else {
						fNumExternalChecked = event.getChecked() ? getExternalModels().length : 0;
					}
					updateCounter();
				}
			} else {
				countSelectedModels();
			}
			fTab.updateLaunchConfigurationDialog();
		});
		fPluginTreeViewer.setComparator(new ListUtil.PluginComparator() {
			@Override
			public int category(Object obj) {
				if (obj == fWorkspacePlugins) {
					return -1;
				}
				return 0;
			}
		});

		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.horizontalSpan = span;
		gd.horizontalIndent = indent;
		fPluginFilteredTree.setLayoutData(gd);

		Image siteImage = PDEPlugin.getDefault().getLabelProvider().get(PDEPluginImages.DESC_SITE_OBJ);

		fWorkspacePlugins = new NamedElement(PDEUIMessages.AdvancedLauncherTab_workspacePlugins, siteImage);
		fExternalPlugins = new NamedElement(PDEUIMessages.PluginsTab_target, siteImage);

		fPluginTreeViewer.addFilter(new Filter());

		Tree tree = fPluginTreeViewer.getTree();

		TreeColumn column1 = new TreeColumn(tree, SWT.LEFT);
		column1.setText(fTab.getName());
		column1.setWidth(400);

		TreeColumn column2 = new TreeColumn(tree, SWT.CENTER);
		column2.setText(PDEUIMessages.EquinoxPluginBlock_levelColumn);
		column2.setWidth(80);

		TreeColumn column3 = new TreeColumn(tree, SWT.CENTER);
		column3.setText(PDEUIMessages.EquinoxPluginBlock_autoColumn);
		column3.setWidth(80);
		tree.setHeaderVisible(true);

		tree.setFont(composite.getFont());
		fPluginFilteredTree.getFilterControl().setFont(composite.getFont());

		createEditors();
	}

	private void createEditors() {
		final Tree tree = fPluginTreeViewer.getTree();

		levelColumnEditor = new TreeEditor(tree);
		levelColumnEditor.horizontalAlignment = SWT.CENTER;
		levelColumnEditor.minimumWidth = 60;
		if (Util.isMac()) {
			levelColumnEditor.minimumHeight = 27;
		}

		autoColumnEditor = new TreeEditor(tree);
		autoColumnEditor.horizontalAlignment = SWT.CENTER;
		autoColumnEditor.grabHorizontal = true;
		autoColumnEditor.minimumWidth = 60;

		tree.addSelectionListener(widgetSelectedAdapter(e -> {
			// Clean up any previous editor control
			Control oldEditor = levelColumnEditor.getEditor();
			if (oldEditor != null && !oldEditor.isDisposed()) {
				oldEditor.dispose();
			}

			oldEditor = autoColumnEditor.getEditor();
			if (oldEditor != null && !oldEditor.isDisposed()) {
				oldEditor.dispose();
			}

			// Identify the selected row
			final TreeItem item = (TreeItem) e.item;
			if (item != null && !isEditable(item)) {
				return;
			}

			if (item != null && !isFragment(item)) { // only display editing controls if we're not a fragment
				final Spinner spinner = new Spinner(tree, SWT.BORDER);
				spinner.setMinimum(0);
				String level = item.getText(1);
				int defaultLevel = level.length() == 0 || "default".equals(level) ? 0 : Integer.parseInt(level); //$NON-NLS-1$
				spinner.setSelection(defaultLevel);
				spinner.addModifyListener(e1 -> {
					if (item.getChecked()) {
						int selection = spinner.getSelection();
						item.setText(1, selection == 0 ? "default" //$NON-NLS-1$
								: Integer.toString(selection));
						levelColumnCache.put(item.getData(), item.getText(1));
						fTab.updateLaunchConfigurationDialog();
					}
				});
				// Resize column with on GTK to ensure the Spinner widget will
				// fit. See bug 552519.
				if (Util.isGtk()) {
					Point spinnerSize = spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT);
					levelColumnEditor.minimumWidth = spinnerSize.x;
					TreeColumn levelColumn = tree.getColumn(1);
					levelColumn.setWidth(spinnerSize.x);
				}
				levelColumnEditor.setEditor(spinner, item, 1);

				final CCombo combo = new CCombo(tree, SWT.BORDER | SWT.READ_ONLY);
				combo.setItems(new String[] {"default", Boolean.toString(true), Boolean.toString(false)}); //$NON-NLS-1$
				combo.setText(item.getText(2));
				combo.pack();
				combo.addSelectionListener(widgetSelectedAdapter(event -> {
					if (item.getChecked()) {
						item.setText(2, combo.getText());
						autoColumnCache.put(item.getData(), item.getText(2));
						fTab.updateLaunchConfigurationDialog();
					}
				}));
				autoColumnEditor.setEditor(combo, item, 2);
			}
		}));
	}

	private boolean isEditable(TreeItem item) {
		Object obj = item.getData();
		if (obj instanceof IPluginModelBase model) {
			String systemBundleId = PDECore.getDefault().getModelManager().getSystemBundleId();
			if (!(systemBundleId.equals(model.getPluginBase().getId()))) {
				return fPluginTreeViewer.getChecked(model);
			}
		}
		return false;
	}

	private boolean isFragment(TreeItem item) {
		Object obj = item.getData();
		if (obj instanceof IPluginModelBase) {
			return ((IPluginModelBase) obj).isFragmentModel();
		}
		return false;
	}

	/**
	 * The view filter for the tree view.  Currently this filter only
	 * filters unchecked items if the fFilterButton is selected.
	 *
	 * @author Ian Bull
	 */
	class Filter extends ViewerFilter {
		@Override
		public boolean select(Viewer viewer, Object parentElement, Object element) {
			if (fFilterButton.getSelection()) {
				// The root elements of the view should never be filtered.
				if (parentElement == PDEPlugin.getDefault()) {
					return true;
				}
				// The cached state should be used for determining if the
				// element is check marked.
				return fPluginTreeViewer.isCheckedLeafElement(element);
			}
			return true;
		}
	}

	private void createButtonContainer(Composite parent) {
		Composite composite = SWTFactory.createComposite(parent, 1, 1, GridData.FILL_VERTICAL);
		GridLayout layout = new GridLayout();
		layout.marginHeight = layout.marginWidth = 0;
		composite.setLayout(layout);
		composite.setLayoutData(new GridData(GridData.FILL_VERTICAL));

		fSelectAllButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_selectAll, SWT.PUSH);
		fDeselectButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_deselectAll, SWT.PUSH);
		fWorkingSetButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_workingSet, SWT.PUSH);
		if (fTab instanceof PluginsTab) {
			fAddRequiredButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_subset_plugins, SWT.PUSH);
		} else if (fTab instanceof BundlesTab) {
			fAddRequiredButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_subset_bundles, SWT.PUSH);
		} else {
			fAddRequiredButton = createButton(composite, NLS.bind(PDEUIMessages.AdvancedLauncherTab_subset, fTab.getName()), SWT.PUSH);
		}

		fDefaultsButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_defaults, SWT.PUSH);
		fFilterButton = createButton(composite, PDEUIMessages.AdvancedLauncherTab_selectedBundles, SWT.CHECK);
		GridData filterButtonGridData = new GridData(GridData.FILL_BOTH | GridData.VERTICAL_ALIGN_END);
		fFilterButton.setLayoutData(filterButtonGridData);

		fCounter = SWTFactory.createLabel(composite, "", 1); //$NON-NLS-1$
		fCounter.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_END));
		updateCounter();
	}

	protected int getTreeViewerStyle() {
		return SWT.BORDER | SWT.FULL_SELECTION;
	}

	private Button createButton(Composite composite, String text, int style) {
		Button button = new Button(composite, style);
		button.setText(text);
		button.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
		SWTUtil.setButtonDimensionHint(button);
		button.addSelectionListener(fListener);
		return button;
	}

	protected void handleCheckStateChanged(CheckStateChangedEvent event) {
		countSelectedModels();
		resetText((IPluginModelBase) event.getElement());
	}

	protected void setText(IPluginModelBase model, String value) {
		Widget widget = fPluginTreeViewer.testFindItem(model);
		if (widget instanceof TreeItem item) {
			int index = value == null ? -1 : value.indexOf(':');
			String levelValue = index == -1 ? "" : value.substring(0, index); //$NON-NLS-1$
			String autoValue = null;
			String modelName = model.getBundleDescription().getSymbolicName();
			item.setText(1, levelValue);
			if (model.isFragmentModel()) {
				autoValue = "false"; //$NON-NLS-1$
				item.setText(2, autoValue);
				// FIXME is this the right place for this logic?
			} else if (IPDEBuildConstants.BUNDLE_CORE_RUNTIME.equals(modelName) || IPDEBuildConstants.BUNDLE_DS.equals(modelName)) {
				autoValue = "true"; //$NON-NLS-1$
				item.setText(2, autoValue);
			} else {
				autoValue = index == -1 ? "" : value.substring(index + 1); //$NON-NLS-1$
				item.setText(2, autoValue);
			}
			levelColumnCache.put(model, levelValue);
			autoColumnCache.put(model, autoValue);
		}
	}

	protected void resetText(IPluginModelBase model) {
		String levelText = ""; //$NON-NLS-1$
		String autoText = ""; //$NON-NLS-1$

		Widget widget = fPluginTreeViewer.testFindItem(model);
		if (fPluginTreeViewer.getChecked(model)) {
			levelText = levelColumnCache.get(model);
			levelText = levelText == null || levelText.length() == 0 ? "default" : levelText; //$NON-NLS-1$
			autoText = autoColumnCache.get(model);
			autoText = autoText == null || autoText.length() == 0 ? "default" : autoText; //$NON-NLS-1$

			// Replace run levels and auto start values for certain important system bundles
			String systemValue = BundleLauncherHelper.resolveSystemRunLevelText(model.getBundleDescription());
			levelText = systemValue != null ? systemValue : levelText;

			systemValue = BundleLauncherHelper.resolveSystemAutoText(model.getBundleDescription());
			autoText = systemValue != null ? systemValue : autoText;

			// Recache the values in case they changed.  I believe the code to only recache
			// if they actually changed takes more time than just setting the value.
			levelColumnCache.put(model, levelText);
			autoColumnCache.put(model, autoText);
		}

		// Set values in UI (although I'm not sure why we don't use the label provider here)
		if (widget instanceof TreeItem) {
			((TreeItem) widget).setText(1, levelText);
		}

		if (widget instanceof TreeItem) {
			((TreeItem) widget).setText(2, autoText);
		}
	}

	protected void handleGroupStateChanged(Object group, boolean checked) {
		if (!fPluginFilteredTree.getPatternFilter().select(fPluginTreeViewer, null, group)) {
			return;
		}

		fPluginTreeViewer.setChecked(group, checked);

		if (group instanceof NamedElement namedElement) {
			TreeItem item = (TreeItem) fPluginTreeViewer.testFindItem(namedElement);
			if (item != null) {
				TreeItem[] children = item.getItems();
				if (children == null) {
					return;
				}
				for (TreeItem childItem : children) {
					Object child = childItem.getData();
					if (child instanceof IPluginModelBase) {
						resetText((IPluginModelBase) child);
					}
				}
			}
		}
	}

	protected void toggleGroups(boolean select) {
		handleGroupStateChanged(fWorkspacePlugins, select);
		handleGroupStateChanged(fExternalPlugins, select);

		countSelectedModels();
	}

	protected void handleFilterButton() {
		fPluginTreeViewer.refresh();
		fPluginTreeViewer.expandAll();
	}


	protected void setChecked(IPluginModelBase model, boolean checked) {
		fPluginTreeViewer.setChecked(model, checked);
		resetText(model);
	}

	/**
	 * Initializes the contents of this block from the given config.  The table's input
	 * will only be initialized if the boolean parameter is set to true.
	 *
	 * @param config launch configuration to init from or <code>null</code>
	 * @param enableTable whether to set the input on the table
	 */
	public void initializeFrom(ILaunchConfiguration config, boolean enableTable) throws CoreException {
		fLaunchConfig = config;
		initializeButtonsFrom(config);
		levelColumnCache = new HashMap<>();
		autoColumnCache = new HashMap<>();
		fPluginFilteredTree.getPatternFilter().setPattern(null);
		if (!enableTable) {
			fPluginTreeViewer.setInput(null);
			refreshTreeView(fPluginTreeViewer);
		} else if (fPluginTreeViewer.getInput() == null) {
			fPluginTreeViewer.setUseHashlookup(true);
			fPluginTreeViewer.setInput(PDEPlugin.getDefault());
			fPluginTreeViewer.reveal(fWorkspacePlugins);
		}
	}

	protected void initializeButtonsFrom(ILaunchConfiguration config) throws CoreException {
		boolean autoIncludeRequired = config.getAttribute(IPDELauncherConstants.AUTOMATIC_INCLUDE_REQUIREMENTS, false);
		fAutoIncludeRequirementsButton.setSelection(autoIncludeRequired);
		fIncludeOptionalButton.setSelection(config.getAttribute(IPDELauncherConstants.INCLUDE_OPTIONAL, true));
		fAddWorkspaceButton.setSelection(config.getAttribute(IPDELauncherConstants.AUTOMATIC_ADD, true));
		fAutoValidate.setSelection(config.getAttribute(IPDELauncherConstants.AUTOMATIC_VALIDATE, true));
		fFilterButton.setSelection(config.getAttribute(IPDELauncherConstants.SHOW_SELECTED_ONLY, false));
	}

	/**
	 * Looks at the currently checked plugins and finds any plug-ins required by them.  The required plug-ins are
	 * then also checked in the tree
	 */
	protected void addRequiredPlugins() {
		Object[] checked = fPluginTreeViewer.getCheckedLeafElements();
		List<IPluginModelBase> toCheck = Arrays.stream(checked).filter(IPluginModelBase.class::isInstance)
				.map(IPluginModelBase.class::cast).collect(Collectors.toList());

		DependencyManager.Options[] options = fIncludeOptionalButton.getSelection()
				? new Options[] { Options.INCLUDE_NON_TEST_FRAGMENTS, Options.INCLUDE_OPTIONAL_DEPENDENCIES }
				: new Options[] { Options.INCLUDE_NON_TEST_FRAGMENTS };
		Set<BundleDescription> additionalBundles = DependencyManager.getDependencies(toCheck, options);

		additionalBundles.stream().map(Resource.class::cast).map(PluginRegistry::findModel).filter(Objects::nonNull)
				.forEach(toCheck::add);

		checked = toCheck.toArray();
		setCheckedElements(checked);

		countSelectedModels();
	}

	protected IPluginModelBase findPlugin(String id) {
		ModelEntry entry = PluginRegistry.findEntry(id);
		if (entry != null) {
			IPluginModelBase model = entry.getModel();
			if (fPluginTreeViewer.getChecked(model)) {
				return model;
			}

			IPluginModelBase[] models = entry.getWorkspaceModels();
			for (IPluginModelBase pluginModel : models) {
				if (fPluginTreeViewer.getChecked(pluginModel)) {
					return pluginModel;
				}
			}

			models = entry.getExternalModels();
			for (IPluginModelBase pluginModel : models) {
				if (fPluginTreeViewer.getChecked(pluginModel)) {
					return pluginModel;
				}
			}
			return null;
		}
		return null;
	}

	protected void setCheckedElements(Object[] checked) {
		fPluginTreeViewer.setCheckedElements(checked);
		updateGroup(fWorkspacePlugins);
		updateGroup(fExternalPlugins);
	}

	private void updateGroup(Object group) {
		Widget item = fPluginTreeViewer.testFindItem(group);
		if (item instanceof TreeItem) {
			TreeItem[] items = ((TreeItem) item).getItems();
			for (TreeItem childItem : items) {
				if (childItem.getChecked() == (childItem.getText(1).length() == 0)) {
					Object model = childItem.getData();
					if (model instanceof IPluginModelBase) {
						resetText((IPluginModelBase) model);
					}
				}
			}
		}
	}

	private void countSelectedModels() {
		fNumWorkspaceChecked = countChecked(getWorkspaceModels());
		fNumExternalChecked = countChecked(getExternalModels());
	}

	private int countChecked(Object[] elements) {
		return (int) Arrays.stream(elements).filter(fPluginTreeViewer::isCheckedLeafElement).count();
	}

	public void performApply(ILaunchConfigurationWorkingCopy config) {
		if (fAutoIncludeRequirementsButtonChanged) {
			boolean includeRequirements = fAutoIncludeRequirementsButton.getSelection();
			config.setAttribute(IPDELauncherConstants.AUTOMATIC_INCLUDE_REQUIREMENTS, includeRequirements);
			fAutoIncludeRequirementsButtonChanged = false;
		}
		config.setAttribute(IPDELauncherConstants.INCLUDE_OPTIONAL, fIncludeOptionalButton.getSelection());
		config.setAttribute(IPDELauncherConstants.AUTOMATIC_ADD, fAddWorkspaceButton.getSelection());
		config.setAttribute(IPDELauncherConstants.AUTOMATIC_VALIDATE, fAutoValidate.getSelection());
		config.setAttribute(IPDELauncherConstants.SHOW_SELECTED_ONLY, fFilterButton.getSelection());
		savePluginState(config);
		updateCounter();
	}

	protected abstract void savePluginState(ILaunchConfigurationWorkingCopy config);

	public void setDefaults(ILaunchConfigurationWorkingCopy config) {
		config.setAttribute(IPDELauncherConstants.INCLUDE_OPTIONAL, true);
		config.setAttribute(IPDELauncherConstants.AUTOMATIC_ADD, true);
		config.setAttribute(IPDELauncherConstants.AUTOMATIC_VALIDATE, true);
		config.setAttribute(IPDELauncherConstants.SHOW_SELECTED_ONLY, false);
	}

	public void enableViewer(boolean enable) {
		fPluginFilteredTree.setEnabled(enable);
		fAddRequiredButton.setEnabled(enable);
		fDefaultsButton.setEnabled(enable);
		fWorkingSetButton.setEnabled(enable);
		fSelectAllButton.setEnabled(enable);
		fDeselectButton.setEnabled(enable);
		fAutoIncludeRequirementsButton.setEnabled(enable);
		fIncludeOptionalButton.setEnabled(enable);
		fAddWorkspaceButton.setEnabled(enable);
		fCounter.setEnabled(enable);
		fFilterButton.setEnabled(enable);
	}

	public void dispose() {
		PDEPlugin.getDefault().getLabelProvider().disconnect(this);
		fIsDisposed = true;
	}

	protected boolean isEnabled() {
		return fPluginTreeViewer.getTree().isEnabled();
	}

	protected void handleRestoreDefaults() {
		TreeSet<String> wtable = new TreeSet<>();
		ArrayList<IPluginModelBase> checkedModels = new ArrayList<>();
		for (int i = 0; i < getWorkspaceModels().length; i++) {
			IPluginModelBase model = getWorkspaceModels()[i];
			checkedModels.add(model);
			String id = model.getPluginBase().getId();
			if (id != null) {
				wtable.add(model.getPluginBase().getId());
			}
		}

		IPluginModelBase[] externalModels = getExternalModels();
		for (IPluginModelBase model : externalModels) {
			boolean masked = wtable.contains(model.getPluginBase().getId());
			if (!masked && model.isEnabled()) {
				checkedModels.add(model);
			}
		}
		fPluginTreeViewer.setCheckedElements(checkedModels.toArray());
		countSelectedModels();

		Object[] selected = fPluginTreeViewer.getCheckedElements();
		for (Object selectedElement : selected) {
			if (selectedElement instanceof IPluginModelBase) {
				resetText((IPluginModelBase) selectedElement);
			}
		}
	}

	protected Shell getShell() {
		// use Shell of launcher window.  If launcher window is disposed (not sure how it could happen), use workbenchwindow.  Bug 168198
		try {
			Control c = fTab.getControl();
			if (!c.isDisposed()) {
				return c.getShell();
			}
		} catch (SWTException e) {
		}
		return PDEPlugin.getActiveWorkbenchShell();
	}

	public void handleValidate() {
		try {
			LaunchValidationOperation fOperation = createValidationOperation();
			fOperation.run(new NullProgressMonitor());

			if (fDialog == null) {
				if (fOperation.hasErrors()) {
					fDialog = new PluginStatusDialog(getShell(), SWT.MODELESS | SWT.CLOSE | SWT.BORDER | SWT.TITLE | SWT.RESIZE);
					fDialog.setInput(fOperation);
					fDialog.open();
					fDialog = null;
				} else if (fOperation.isEmpty()) {
					if (fTab instanceof PluginsTab) {
						MessageDialog.openInformation(getShell(), PDEUIMessages.PluginStatusDialog_pluginValidation,PDEUIMessages.AbstractLauncherToolbar_noSelection_plugins);
					}else if (fTab instanceof BundlesTab) {
						MessageDialog.openInformation(getShell(), PDEUIMessages.PluginStatusDialog_pluginValidation,PDEUIMessages.AbstractLauncherToolbar_noSelection_bundles);
					}else{
						MessageDialog.openInformation(getShell(), PDEUIMessages.PluginStatusDialog_pluginValidation, NLS.bind(PDEUIMessages.AbstractLauncherToolbar_noSelection, fTab.getName().toLowerCase(Locale.ENGLISH)));
					}

				} else {
					MessageDialog.openInformation(getShell(), PDEUIMessages.PluginStatusDialog_pluginValidation, PDEUIMessages.AbstractLauncherToolbar_noProblems);
				}
			} else {
				if (fOperation.getInput().size() > 0) {
					fDialog.refresh(fOperation.getInput());
				} else {
					Map<String, IStatus> input = new HashMap<>(1);
					input.put(PDEUIMessages.AbstractLauncherToolbar_noProblems, Status.OK_STATUS);
					fDialog.refresh(input);
				}
			}
		} catch (CoreException e) {
			PDEPlugin.log(e);
		}
	}

	protected void setVisible(boolean visible) {
		if (!visible) {
			if (fDialog != null) {
				fDialog.close();
				fDialog = null;
			}
		}
	}

	protected abstract LaunchValidationOperation createValidationOperation() throws CoreException;

	/**
	 * Disposing the editor in tree viewer
	 *
	 */
	protected void refreshTreeView(CheckboxTreeViewer treeView) {
		// Reset any editors on the tree viewer
		if (levelColumnEditor != null && levelColumnEditor.getEditor() != null && !levelColumnEditor.getEditor().isDisposed()) {
			levelColumnEditor.getEditor().dispose();
		}

		if (autoColumnEditor != null && autoColumnEditor.getEditor() != null && !autoColumnEditor.getEditor().isDisposed()) {
			autoColumnEditor.getEditor().dispose();
		}
	}

	protected void resetGroup(NamedElement group) {
		Widget widget = fPluginTreeViewer.testFindItem(group);
		if (widget instanceof TreeItem) {
			TreeItem[] items = ((TreeItem) widget).getItems();
			for (int i = 0; i < items.length; i++) {
				if (!items[i].getChecked()) {
					Object model = items[i].getData();
					if (model instanceof IPluginModelBase) {
						resetText((IPluginModelBase) model);
					}
				}
			}
		}
	}

	protected final void initializePluginsState(Map<IPluginModelBase, String> selectedPlugins) {
		fPluginTreeViewer.setCheckedElements(selectedPlugins.keySet().toArray());
		for (Entry<IPluginModelBase, String> entry : selectedPlugins.entrySet()) {
			IPluginModelBase model = entry.getKey();
			setText(model, entry.getValue());
		}

		countSelectedModels();
		resetGroup(fWorkspacePlugins);
		resetGroup(fExternalPlugins);

		if (fFilterButton.getSelection()) {
			// Once the page is initialized, apply any filtering.
			handleFilterButton();
		}
		updateCounter();
	}

}
