/*******************************************************************************
 * Copyright (c) 2000, 2018 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.ui.externaltools.internal.ui;


import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.externaltools.internal.IExternalToolConstants;
import org.eclipse.core.externaltools.internal.model.BuilderCoreUtils;
import org.eclipse.core.externaltools.internal.model.ExternalToolBuilder;
import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceDescription;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationListener;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
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.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.ui.dialogs.PropertyPage;
import org.eclipse.ui.externaltools.internal.launchConfigurations.ExternalToolsMainTab;
import org.eclipse.ui.externaltools.internal.launchConfigurations.ExternalToolsUtil;
import org.eclipse.ui.externaltools.internal.launchConfigurations.IgnoreWhiteSpaceComparator;
import org.eclipse.ui.externaltools.internal.model.BuilderUtils;
import org.eclipse.ui.externaltools.internal.model.ExternalToolsPlugin;
import org.eclipse.ui.externaltools.internal.model.IExternalToolsHelpContextIds;
import org.eclipse.ui.externaltools.internal.model.IPreferenceConstants;
import org.eclipse.ui.progress.IProgressService;

/**
 * Property page to add external tools builders.
 */
public final class BuilderPropertyPage extends PropertyPage implements ICheckStateListener {

	//locally mark a command's enabled state so it can be processed correctly on performOK
	private static final String COMMAND_ENABLED= "CommandEnabled"; //$NON-NLS-1$

	private Button upButton, downButton, newButton, importButton, editButton, removeButton;

	private boolean userHasMadeChanges= false;

	private List<ILaunchConfiguration> configsToBeDeleted = null;
	private List<ICommand> commandsToBeDeleted = null;

	private CheckboxTableViewer viewer= null;

	private boolean fWarned = false;
	/**
	 * Flag to know if we can perform an edit of the selected element(s)
	 */
	private boolean fCanEdit = false;

	private ILabelProvider labelProvider= new BuilderLabelProvider();

	/**
	 * Error configs are objects representing entries pointing to
	 * invalid launch configurations
	 */
	public class ErrorConfig {
		private ICommand command;
		public ErrorConfig(ICommand command) {
			this.command= command;
		}
		public ICommand getCommand() {
			return command;
		}
	}

	/**
	 * Collection of configurations created while the page is open.
	 * Stored here so they can be deleted if the page is cancelled.
	 */
	private List<ILaunchConfiguration> newConfigList = new ArrayList<>();

	private SelectionListener buttonListener= new SelectionAdapter() {
		@Override
		public void widgetSelected(SelectionEvent e) {
			handleButtonPressed((Button) e.widget);
		}
	};

	/**
	 * Launch configuration listener which is responsible for updating items in
	 * the tree when the user renames configurations in the dialog.
	 *
	 * This is necessary because when we tell the configuration dialog to open
	 * on a launch config and the user renames that config, the old config (the
	 * one in the tree) is made obsolete and a new config is created. This
	 * listener hears when new configurations are created this way and replaces
	 * the old configuration with the new.
	 */
	private ILaunchConfigurationListener configurationListener= new ILaunchConfigurationListener() {
		/**
		 * A launch configuration has been added. If this config has been
		 * movedFrom a configuration in the tree, replace the old config with
		 * the new.
		 */
		@Override
		public void launchConfigurationAdded(final ILaunchConfiguration configuration) {
			ILaunchManager manager= DebugPlugin.getDefault().getLaunchManager();
			final ILaunchConfiguration oldConfig= manager.getMovedFrom(configuration);
			if (oldConfig == null) {
				return;
			}
			//Replace the movedFrom config in the list of newly created configs
			if (newConfigList.remove(oldConfig)) {
				newConfigList.add(configuration);
			}

			Display.getDefault().asyncExec(() -> {
				TableItem[] items = viewer.getTable().getItems();
				for (TableItem item : items) {
					Object data = item.getData();
					if (data == oldConfig) {
						// Found the movedFrom config in the tree. Replace it
						// with the new config
						item.setData(configuration);
						viewer.update(configuration, null);
						break;
					}
				}
			});
		}
		@Override
		public void launchConfigurationChanged(ILaunchConfiguration configuration) {
		}
		@Override
		public void launchConfigurationRemoved(ILaunchConfiguration configuration) {
		}
	};

	/**
	 * Creates an initialized property page
	 */
	public BuilderPropertyPage() {
		super();
		noDefaultAndApplyButton();
	}

	/**
	 * Add the project's build to the table viewer.
	 */
	private void addBuildersToTable() {
		IProject project = getInputProject();
		if (project == null) {
			return;
		}
		//add build spec entries to the table
		ICommand[] commands= null;
		try {
			commands = project.getDescription().getBuildSpec();
		} catch (CoreException e) {
			handleException(e);
			return;
		}

		boolean projectNeedsMigration= false;
		for (ICommand command : commands) {
			String[] version= new String[] {IExternalToolConstants.EMPTY_STRING};
			ILaunchConfiguration config = BuilderUtils.configFromBuildCommandArgs(project, command.getArguments(), version);
			if (BuilderCoreUtils.VERSION_2_1.equals(version[0])) {
				// Storing the .project file of a project with 2.1 configs, will
				// edit the file in a way that isn't backwards compatible.
				projectNeedsMigration= true;
			}
			Object element= null;
			if (config != null) {
				if (!config.isWorkingCopy() && !config.exists()) {
					Shell shell= getShell();
					if (shell == null) {
						return;
					}
					IStatus status = new Status(IStatus.ERROR, ExternalToolsPlugin.PLUGIN_ID, 0, NLS.bind(ExternalToolsUIMessages.BuilderPropertyPage_Exists, new String[]{config.getName()}), null);
					ErrorDialog.openError(getShell(), ExternalToolsUIMessages.BuilderPropertyPage_errorTitle,
							NLS.bind(ExternalToolsUIMessages.BuilderPropertyPage_External_Tool_Builder__0__Not_Added_2, new String[]{config.getName()}),
							status);
					userHasMadeChanges= true;
				} else {
					element= config;
				}
			} else {
				String builderID = command.getBuilderName();
				if (builderID.equals(ExternalToolBuilder.ID) && command.getArguments().get(BuilderCoreUtils.LAUNCH_CONFIG_HANDLE) != null) {
					// An invalid external tool entry.
					element = new ErrorConfig(command);
				} else {
					element = command;
				}
			}
			if (element != null) {
				viewer.add(element);
				viewer.setChecked(element, isEnabled(element));
			}
		}
		if (projectNeedsMigration) {
			IPreferenceStore store= ExternalToolsPlugin.getDefault().getPreferenceStore();
			boolean prompt= store.getBoolean(IPreferenceConstants.PROMPT_FOR_PROJECT_MIGRATION);
			boolean proceed= true;
			if (prompt) {
				Shell shell= getShell();
				if (shell == null) {
					return;
				}
				MessageDialogWithToggle dialog= MessageDialogWithToggle.openYesNoQuestion(shell, ExternalToolsUIMessages.BuilderPropertyPage_0, ExternalToolsUIMessages.BuilderPropertyPage_1, ExternalToolsUIMessages.BuilderPropertyPage_2, false, null, null);
				proceed= dialog.getReturnCode() == IDialogConstants.YES_ID;
				store.setValue(IPreferenceConstants.PROMPT_FOR_PROJECT_MIGRATION, !dialog.getToggleState());
			}
			if (!proceed) {
				// Open the page read-only
				viewer.getTable().setEnabled(false);
				downButton.setEnabled(false);
				editButton.setEnabled(false);
				importButton.setEnabled(false);
				newButton.setEnabled(false);
				removeButton.setEnabled(false);
			}
		}
	}

	/**
	 * Creates and returns a button with the given label, id, and enablement.
	 */
	private Button createButton(Composite parent, String label) {
		Button button = new Button(parent, SWT.PUSH);
		button.setFont(parent.getFont());
		button.setText(label);
		button.setEnabled(false);
		button.addSelectionListener(buttonListener);
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		data.grabExcessHorizontalSpace = true;
		button.setLayoutData(data);
		int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
		data.widthHint = Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x);
		return button;
	}

	@Override
	protected Control createContents(Composite parent) {

		PlatformUI.getWorkbench().getHelpSystem().setHelp(parent, IExternalToolsHelpContextIds.EXTERNAL_TOOLS_BUILDER_PROPERTY_PAGE);

		Font font = parent.getFont();

		Composite topLevel = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		topLevel.setLayout(layout);
		topLevel.setLayoutData(new GridData(GridData.FILL_BOTH));

		Label description = new Label(topLevel, SWT.WRAP);
		description.setText(ExternalToolsUIMessages.BuilderPropertyPage_description);
		description.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		description.setFont(font);

		Composite tableAndButtons = new Composite(topLevel, SWT.NONE);
		tableAndButtons.setLayoutData(new GridData(GridData.FILL_BOTH));
		layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		layout.numColumns = 2;
		tableAndButtons.setLayout(layout);

		// table of builders and tools
		viewer= CheckboxTableViewer.newCheckList(tableAndButtons, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.BORDER);
		viewer.setLabelProvider(labelProvider);
		viewer.addCheckStateListener(this);
		Table builderTable= viewer.getTable();
		builderTable.setLayoutData(new GridData(GridData.FILL_BOTH));
		builderTable.setFont(font);
		builderTable.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				handleTableSelectionChanged();
			}
		});

		builderTable.addListener(SWT.MouseDoubleClick, event -> {
			// we must not allow editing of elements that cannot be edited via
			// the selection changed logic
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=386820
			if (fCanEdit) {
				handleEditButtonPressed();
			}
		});

		//button area
		Composite buttonArea = new Composite(tableAndButtons, SWT.NONE);
		layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		buttonArea.setLayout(layout);
		buttonArea.setFont(font);
		buttonArea.setLayoutData(new GridData(GridData.FILL_VERTICAL));
		newButton = createButton(buttonArea, ExternalToolsUIMessages.BuilderPropertyPage_newButton);
		importButton = createButton(buttonArea, ExternalToolsUIMessages.BuilderPropertyPage__Import____3);
		editButton = createButton(buttonArea, ExternalToolsUIMessages.BuilderPropertyPage_editButton);
		removeButton = createButton(buttonArea, ExternalToolsUIMessages.BuilderPropertyPage_removeButton);
		new Label(buttonArea, SWT.LEFT);
		upButton = createButton(buttonArea, ExternalToolsUIMessages.BuilderPropertyPage_upButton);
		downButton = createButton(buttonArea, ExternalToolsUIMessages.BuilderPropertyPage_downButton);

		newButton.setEnabled(true);
		importButton.setEnabled(true);

		//populate widget contents
		addBuildersToTable();

		return topLevel;
	}

	/**
	 * Turns auto-building on or off in the workspace.
	 */
	private void setAutobuild(boolean newState) throws CoreException {
		IWorkspace workspace= ResourcesPlugin.getWorkspace();
		IWorkspaceDescription wsDescription= workspace.getDescription();
		boolean oldState= wsDescription.isAutoBuilding();
		if (oldState != newState) {
			wsDescription.setAutoBuilding(newState);
			workspace.setDescription(wsDescription);
		}
	}

	/**
	 * Returns the project that is the input for this property page,
	 * or <code>null</code>.
	 */
	private IProject getInputProject() {
		IAdaptable element = getElement();
		if (element instanceof IProject) {
			return (IProject) element;
		}
		Object resource = element.getAdapter(IResource.class);
		if (resource instanceof IProject) {
			return (IProject) resource;
		}
		return null;
	}

	/**
	 * One of the buttons has been pressed, act accordingly.
	 */
	private void handleButtonPressed(Button button) {
		if (button == newButton) {
			handleNewButtonPressed();
		} else if (button == importButton) {
			handleImportButtonPressed();
		} else if (button == editButton) {
			handleEditButtonPressed();
		} else if (button == removeButton) {
			handleRemoveButtonPressed();
		} else if (button == upButton) {
			moveSelectionUp();
		} else if (button == downButton) {
			moveSelectionDown();
		}
		if (getControl().isDisposed()) {
			return;
		}
		handleTableSelectionChanged();
		viewer.getTable().setFocus();
	}

	@Override
	public void checkStateChanged(CheckStateChangedEvent event) {
		Object element= event.getElement();
		boolean checked = event.getChecked();
		if (element instanceof ILaunchConfiguration) {
			enableLaunchConfiguration((ILaunchConfiguration) element, checked);
		} else if (element instanceof ICommand) {
			Shell shell= getShell();
			if (shell == null) {
				return;
			}
			if (checked) {
				enableCommand((ICommand)element, checked);
				return;
			} else if (!fWarned) {
				if(MessageDialog.openConfirm(shell, ExternalToolsUIMessages.BuilderPropertyPage_6, ExternalToolsUIMessages.BuilderPropertyPage_7)) {
					fWarned = true;
				}
			}
			if(fWarned) {
				enableCommand((ICommand)element, checked);
			}
			else {
				viewer.removeCheckStateListener(this);
				viewer.setChecked(element, true);
				viewer.addCheckStateListener(this);
			}

		}
	}

	private void enableLaunchConfiguration(ILaunchConfiguration configuration, boolean enable) {
		ILaunchConfigurationWorkingCopy workingCopy= null;
		try {
			if (configuration instanceof ILaunchConfigurationWorkingCopy) {
				workingCopy = (ILaunchConfigurationWorkingCopy) configuration;
			} else {
				// Replace the config with a working copy
				TableItem[] items= viewer.getTable().getItems();
				for (TableItem item : items) {
					if (item.getData() == configuration) {
						workingCopy = configuration.getWorkingCopy();
						item.setData(workingCopy);
					}
				}
			}
			if (workingCopy != null) {
				workingCopy.setAttribute(IExternalToolConstants.ATTR_BUILDER_ENABLED, enable);
			}
		} catch (CoreException e) {
			return;
		}
		userHasMadeChanges= true;
	}

	private void enableCommand(ICommand command, boolean enable) {
		Map<String, String> args = command.getArguments();
		if (args == null) {
			args = new HashMap<>(1);
		}
		args.put(COMMAND_ENABLED, Boolean.toString(enable));
		command.setArguments(args);
		userHasMadeChanges= true;
	}

	/**
	 * The user has pressed the import button. Prompt them to select a
	 * configuration to import from the workspace.
	 */
	private void handleImportButtonPressed() {
		ILaunchManager manager= DebugPlugin.getDefault().getLaunchManager();
		List<ILaunchConfigurationType> toolTypes = getConfigurationTypes(IExternalToolConstants.ID_EXTERNAL_TOOLS_LAUNCH_CATEGORY);
		List<ILaunchConfiguration> configurations = new ArrayList<>();
		for (ILaunchConfigurationType type : toolTypes) {
			try {
				ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type);
				for (ILaunchConfiguration launchConfiguration : configs) {
					if (!DebugUITools.isPrivate(launchConfiguration)) {
						configurations.add(launchConfiguration);
					}
				}
			} catch (CoreException e) {
			}
		}
		Shell shell= getShell();
		if (shell == null) {
			return;
		}
		ElementListSelectionDialog dialog= new ElementListSelectionDialog(shell, new BuilderLabelProvider());
		dialog.setTitle(ExternalToolsUIMessages.BuilderPropertyPage_4);
		dialog.setMessage(ExternalToolsUIMessages.BuilderPropertyPage_5);
		dialog.setElements(configurations.toArray());
		if (dialog.open() == Window.CANCEL) {
			return;
		}
		Object results[]= dialog.getResult();
		if (results.length == 0) { //OK pressed with nothing selected
			return;
		}
		ILaunchConfiguration config= (ILaunchConfiguration) results[0];
		ILaunchConfiguration newConfig= null;
		boolean wasAutobuilding= ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
		try {
			setAutobuild(false);
			newConfig= BuilderUtils.duplicateConfiguration(getInputProject(), config);
		} catch (CoreException e) {
			handleException(e);
		} finally {
			try {
				setAutobuild(wasAutobuilding);
			} catch (CoreException e) {
				handleException(e);
			}
		}
		if (newConfig != null) {
			userHasMadeChanges= true;
			viewer.add(newConfig);
			viewer.setChecked(newConfig, isEnabled(newConfig));
			newConfigList.add(newConfig);
		}
	}

	/**
	 * The user has pressed the remove button. Delete the selected builder.
	 */
	private void handleRemoveButtonPressed() {
		IStructuredSelection selection = viewer.getStructuredSelection();
		if (selection != null) {
			int numSelected= selection.size();
			userHasMadeChanges= true;
			Iterator<?> iterator = selection.iterator();
			while (iterator.hasNext()) {
				Object item= iterator.next();
				if (item instanceof ILaunchConfiguration) {
					if (configsToBeDeleted == null) {
						configsToBeDeleted = new ArrayList<>(numSelected);
					}
					configsToBeDeleted.add((ILaunchConfiguration) item);
				} else if (item instanceof ICommand) {
					if (commandsToBeDeleted == null) {
						commandsToBeDeleted = new ArrayList<>(numSelected);
					}
					commandsToBeDeleted.add((ICommand) item);
				}
				viewer.remove(item);
			}
		}
	}

	/**
	 * The user has pressed the new button. Create a new configuration and open
	 * the launch configuration edit dialog on the new config.
	 */
	private void handleNewButtonPressed() {
		ILaunchConfigurationType type = promptForConfigurationType();
		if (type == null) {
			return;
		}
		boolean wasAutobuilding= ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
		try {
			ILaunchConfigurationWorkingCopy workingCopy = null;
			String name= DebugPlugin.getDefault().getLaunchManager().generateLaunchConfigurationName(ExternalToolsUIMessages.BuilderPropertyPage_New_Builder_7);
			workingCopy = type.newInstance(BuilderUtils.getBuilderFolder(getInputProject(), true), name);

			StringBuilder buffer= new StringBuilder(IExternalToolConstants.BUILD_TYPE_FULL);
			buffer.append(',');
			buffer.append(IExternalToolConstants.BUILD_TYPE_INCREMENTAL);
			buffer.append(',');
			workingCopy.setAttribute(IExternalToolConstants.ATTR_RUN_BUILD_KINDS, buffer.toString());
			workingCopy.setAttribute(ExternalToolsMainTab.FIRST_EDIT, true);
			ILaunchConfiguration config = null;
			setAutobuild(false);
			config = workingCopy.doSave();
			//needs to be added here in case the user hits apply in the edit dialog
			//then we can correctly update the list with the new config.
			newConfigList.add(config);
			int code= editConfiguration(config);
			if (code == Window.CANCEL) {
				// If the user cancelled, delete the newly created config
				newConfigList.remove(config);
				config.delete();
			} else {
				userHasMadeChanges= true;
				//retrieve the last "new" config
				//may have been changed by the user pressing apply in the edit dialog
				config= newConfigList.get(newConfigList.size() - 1);
				viewer.add(config);
				viewer.setChecked(config, isEnabled(config));
			}
		} catch (CoreException e) {
			handleException(e);
		} finally {
			try {
				setAutobuild(wasAutobuilding);
			} catch (CoreException e) {
				handleException(e);
			}
		}
	}

	/**
	 * Prompts the user to edit the given launch configuration. Returns the
	 * return code from opening the launch configuration dialog.
	 */
	private int editConfiguration(ILaunchConfiguration config) {
		ILaunchManager manager= DebugPlugin.getDefault().getLaunchManager();
		manager.addLaunchConfigurationListener(configurationListener);
		Shell shell= getShell();
		if (shell == null) {
			return Window.CANCEL;
		}
		int code= DebugUITools.openLaunchConfigurationPropertiesDialog(shell, config, org.eclipse.ui.externaltools.internal.model.IExternalToolConstants.ID_EXTERNAL_TOOLS_BUILDER_LAUNCH_GROUP);
		manager.removeLaunchConfigurationListener(configurationListener);
		return code;
	}

	/**
	 * Prompts the user to choose a launch configuration type to create and
	 * returns the type the user selected or <code>null</code> if the user
	 * cancelled.
	 *
	 * @return the configuration type selected by the user or <code>null</code>
	 * if the user cancelled.
	 */
	private ILaunchConfigurationType promptForConfigurationType() {
		List<ILaunchConfigurationType> externalToolTypes = getConfigurationTypes(IExternalToolConstants.ID_EXTERNAL_TOOLS_BUILDER_LAUNCH_CATEGORY);
		Shell shell= getShell();
		if (shell == null) {
			return null;
		}
		ElementListSelectionDialog dialog = new ElementListSelectionDialog(shell, new BuilderLabelProvider());
		dialog.setElements(externalToolTypes.toArray());
		dialog.setMultipleSelection(false);
		dialog.setTitle(ExternalToolsUIMessages.BuilderPropertyPage_Choose_configuration_type_8);
		dialog.setMessage(ExternalToolsUIMessages.BuilderPropertyPage_Choose_an_external_tool_type_to_create_9);
		dialog.open();
		Object result[] = dialog.getResult();
		if (result == null || result.length == 0) {
			return null;
		}
		return (ILaunchConfigurationType) result[0];
	}

	/**
	 * Returns the launch configuration types of the given category
	 */
	private List<ILaunchConfigurationType> getConfigurationTypes(String category) {
		ILaunchConfigurationType types[] = DebugPlugin.getDefault().getLaunchManager().getLaunchConfigurationTypes();
		List<ILaunchConfigurationType> externalToolTypes = new ArrayList<>();
		for (ILaunchConfigurationType configurationType : types) {
			if (category.equals(configurationType.getCategory())) {
				externalToolTypes.add(configurationType);
			}
		}
		return externalToolTypes;
	}

	/**
	 * The user has pressed the edit button or double-clicked. Open the launch configuration edit
	 * dialog on the selection after migrating the tool if necessary.
	 */
	private void handleEditButtonPressed() {
		TableItem[] items= viewer.getTable().getSelection();
		if (items.length == 0) {
			return;
		}
		TableItem selection= items[0];
		if (selection != null) {
			Object data = selection.getData();
			if (data instanceof ILaunchConfiguration) {
				ILaunchConfiguration config= (ILaunchConfiguration) data;
				if (BuilderUtils.isUnmigratedConfig(config)) {
					if (!shouldProceedWithMigration()) {
						return;
					}
					try {
						config= BuilderUtils.migrateBuilderConfiguration(getInputProject(), (ILaunchConfigurationWorkingCopy) config);
					} catch (CoreException e) {
						handleException(e);
						return;
					}
					// Replace the working copy in the table with the migrated configuration
					selection.setData(config);
				}
				userHasMadeChanges= true;
				boolean wasAutobuilding= ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
				try {
					setAutobuild(false);
					editConfiguration(config);
				} catch (CoreException e) {
					handleException(e);
				} finally {
					try {
						setAutobuild(wasAutobuilding);
					} catch (CoreException e) {
						handleException(e);
					}
				}
			} else if (data instanceof ICommand) {
				ICommand command= (ICommand) data;
				if (command.isConfigurable()) {
					if (editCommand(command)) {
						userHasMadeChanges= true;
					}
				}
			}
		}
	}

	private boolean editCommand(ICommand data) {
		EditCommandDialog dialog= new EditCommandDialog(getShell(), data);
		return Window.OK == dialog.open();
	}

	/**
	 * Prompts the user to proceed with the migration of a project builder from
	 * the old format to the new, launch configuration-based, format and returns
	 * whether or not the user wishes to proceed with the migration.
	 *
	 * @return boolean whether or not the user wishes to proceed with migration
	 */
	private boolean shouldProceedWithMigration() {
		if (!ExternalToolsPlugin.getDefault().getPreferenceStore().getBoolean(IPreferenceConstants.PROMPT_FOR_TOOL_MIGRATION)) {
			// User has asked not to be prompted
			return true;
		}
		Shell shell= getShell();
		if (shell == null) {
			return false;
		}
		// Warn the user that editing an old config will cause storage migration.
		MessageDialogWithToggle dialog= MessageDialogWithToggle.openYesNoQuestion(getShell(),
			ExternalToolsUIMessages.BuilderPropertyPage_Migrate_project_builder_10,
			ExternalToolsUIMessages.BuilderPropertyPage_Not_Support,
			ExternalToolsUIMessages.BuilderPropertyPage_Prompt,
			false,
			ExternalToolsPlugin.getDefault().getPreferenceStore(),
			IPreferenceConstants.PROMPT_FOR_TOOL_MIGRATION);
		return dialog.getReturnCode() == IDialogConstants.YES_ID;
	}

	/**
	 * Handles unexpected internal exceptions
	 */
	private void handleException(Exception e) {
		final IStatus[] status= new IStatus[1];
		if (e instanceof CoreException) {
			status[0] = ((CoreException) e).getStatus();
		} else {
			status[0] = new Status(IStatus.ERROR, ExternalToolsPlugin.PLUGIN_ID, 0, ExternalToolsUIMessages.BuilderPropertyPage_statusMessage, e);
		}
		Display.getDefault().asyncExec(() -> {
			Shell shell = getShell();
			if (shell != null) {
				ErrorDialog.openError(shell, ExternalToolsUIMessages.BuilderPropertyPage_errorTitle, ExternalToolsUIMessages.BuilderPropertyPage_errorMessage, status[0]);
			}
		});
	}

	/**
	 * The user has selected a different builder in table.
	 * Update button enablement.
	 */
	private void handleTableSelectionChanged() {
		newButton.setEnabled(true);
		Table builderTable= viewer.getTable();
		TableItem[] items = builderTable.getSelection();
		fCanEdit = false;
		boolean enableRemove = false;
		boolean enableUp = false;
		boolean enableDown = false;
		if(items != null) {
			boolean validSelection =  items.length > 0;
			fCanEdit = validSelection;
			enableRemove = validSelection;
			enableUp = validSelection;
			enableDown = validSelection;
			if (items.length > 1) {
				fCanEdit= false;
			}
			int indices[]= builderTable.getSelectionIndices();
			int max = builderTable.getItemCount();
			if(indices.length > 0) {
				enableUp = indices[0] != 0;
				enableDown = indices[indices.length - 1] < max - 1;
			}
			for (TableItem item : items) {
				Object data= item.getData();
				if (data instanceof ILaunchConfiguration) {
					ILaunchConfiguration config= (ILaunchConfiguration)data;
					String builderName= null;
					try {
						builderName = config.getAttribute(IExternalToolConstants.ATTR_DISABLED_BUILDER, (String)null);
					} catch (CoreException e) {
					}
					if (builderName != null) {
						//do not allow "wrapped" builders to be removed or edited if they are valid
						IExtension ext= Platform.getExtensionRegistry().getExtension(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_BUILDERS, builderName);
						fCanEdit= false;
						enableRemove= ext == null;
					}
				} else {
					if (data instanceof ErrorConfig) {
						fCanEdit= false;
						continue;
					}
					ICommand command= (ICommand) data;
					fCanEdit= command.isConfigurable();
					IExtension ext= Platform.getExtensionRegistry().getExtension(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_BUILDERS, command.getBuilderName());
					enableRemove= ext == null;
					break;
				}
			}
		}
		editButton.setEnabled(fCanEdit);
		removeButton.setEnabled(enableRemove);
		upButton.setEnabled(enableUp);
		downButton.setEnabled(enableDown);
	}

	/**
	 * Returns whether the given element (command or launch config)
	 * is enabled.
	 *
	 * @param element the element
	 * @return whether the given element is enabled
	 */
	private boolean isEnabled(Object element) {
		if (element instanceof ICommand) {
			ICommand command = (ICommand) element;
			String val = command.getArguments().get(COMMAND_ENABLED);
			if(val != null) {
				//null means enabled, see #doPerformOk
				return Boolean.parseBoolean(val);
			}
		} else if (element instanceof ILaunchConfiguration) {
			try {
				return ExternalToolsUtil.isBuilderEnabled((ILaunchConfiguration) element);
			} catch (CoreException e) {
			}
		} else if (element instanceof ErrorConfig) {
			return false;
		}
		return true;
	}

	/**
	 * Moves an entry in the builder table to the given index.
	 */
	private void move(TableItem item, int index) {
		userHasMadeChanges= true;
		Object data = item.getData();
		item.dispose();
		viewer.insert(data, index);
		viewer.setChecked(data, isEnabled(data));
	}

	/**
	 * Move the current selection in the build list down.
	 */
	private void moveSelectionDown() {
		Table builderTable= viewer.getTable();
		int indices[]= builderTable.getSelectionIndices();
		if (indices.length < 1) {
			return;
		}
		int newSelection[]= new int[indices.length];
		int max= builderTable.getItemCount() - 1;
		for (int i = indices.length - 1; i >= 0; i--) {
			int index= indices[i];
			if (index < max) {
				move (builderTable.getItem(index), index + 1);
				newSelection[i]= index + 1;
			}
		}
		builderTable.setSelection(newSelection);
	}

	/**
	 * Move the current selection in the build list up.
	 */
	private void moveSelectionUp() {
		Table builderTable= viewer.getTable();
		int indices[]= builderTable.getSelectionIndices();
		int newSelection[]= new int[indices.length];
		for (int i = 0; i < indices.length; i++) {
			int index= indices[i];
			if (index > 0) {
				move (builderTable.getItem(index), index - 1);
				newSelection[i]= index - 1;
			}
		}
		builderTable.setSelection(newSelection);
	}

	@Override
	public boolean performOk() {
		if (!userHasMadeChanges) {
			return super.performOk();
		}
		userHasMadeChanges= false;
		Table builderTable= viewer.getTable();
		int numCommands = builderTable.getItemCount();
		final Object[] itemData= new Object[numCommands];
		for (int i = 0; i < numCommands; i++) {
			itemData[i]= builderTable.getItem(i).getData();
		}
		IRunnableWithProgress runnable = monitor -> {
			doPerformOk(monitor, itemData);
			if (monitor.isCanceled()) {
				throw new InterruptedException();
			}
		};

		IProgressService service= PlatformUI.getWorkbench().getProgressService();
		try {
			service.busyCursorWhile(runnable);
		} catch (InvocationTargetException e) {
			return false;
		} catch (InterruptedException e) {
			return false;
		}
		return super.performOk();
	}

	private void doPerformOk(IProgressMonitor monitor, Object[] itemData) {
		if (monitor.isCanceled()) {
			return;
		}

		IProject project = getInputProject();
		//get all the build commands
		int numCommands = itemData.length;
		monitor.beginTask(ExternalToolsUIMessages.BuilderPropertyPage_3, numCommands + 1);
		List<ICommand> possibleCommands = new ArrayList<>(numCommands);
		for (int i = 0; i < numCommands; i++) {
			Object data = itemData[i];
			if (data instanceof ICommand) {
				if (commandsToBeDeleted != null && commandsToBeDeleted.contains(data)) {
					//command specified to be removed
					data= null;
					continue;
				}
				ICommand command= (ICommand)data;
				Map<String, String> args = command.getArguments();
				String val = args.get(COMMAND_ENABLED);
				if(val != null) {
					if (!Boolean.parseBoolean(val)) {
						ILaunchConfiguration config= disableCommand(command);
						if (config != null) {
							data= BuilderUtils.commandFromLaunchConfig(project,config);
						}
					} else {
						args.remove(COMMAND_ENABLED);
						command.setArguments(args);
				}
				}
			} else if (data instanceof ILaunchConfiguration) {
				ILaunchConfiguration config= (ILaunchConfiguration) data;
				String disabledBuilderName;
				try {
					disabledBuilderName = config.getAttribute(IExternalToolConstants.ATTR_DISABLED_BUILDER, (String)null);
					if (disabledBuilderName != null && ExternalToolsUtil.isBuilderEnabled(config)) {
						possibleCommands.add(translateBackToCommand(config, project));
						continue;
					}
				} catch (CoreException e1) {
				}

				if (!BuilderUtils.isUnmigratedConfig(config) && (config instanceof ILaunchConfigurationWorkingCopy)) {
					ILaunchConfigurationWorkingCopy workingCopy= ((ILaunchConfigurationWorkingCopy) config);
					// Save any changes to the config (such as enable/disable)
					if (workingCopy.isDirty()) {
						try {
							workingCopy.doSave();
						} catch (CoreException e) {
							Shell shell= getShell();
							if (shell != null) {
								MessageDialog.openError(shell, ExternalToolsUIMessages.BuilderPropertyPage_39, NLS.bind(ExternalToolsUIMessages.BuilderPropertyPage_40, new String[] {workingCopy.getName()}));
							}
						}
					}
				}
				data= BuilderUtils.commandFromLaunchConfig(project, config);
			} else if (data instanceof ErrorConfig) {
				data= ((ErrorConfig) data).getCommand();
			}
			if (data instanceof ICommand) {
				possibleCommands.add((ICommand) data);
			}
			monitor.worked(1);
		}
		ICommand[] commands= new ICommand[possibleCommands.size()];
		possibleCommands.toArray(commands);
		if (checkCommandsForChange(commands)) {
			//set the build spec
			try {
				IProjectDescription desc = project.getDescription();
				desc.setBuildSpec(commands);
				project.setDescription(desc, IResource.FORCE, monitor);
			} catch (CoreException e) {
				handleException(e);
				performCancel();
			}
		}

		if (configsToBeDeleted != null) {
			deleteConfigurations();
		}
		monitor.done();
	}

	private void checkBuilderFolder() {
		try {
			IFolder builderFolder= BuilderUtils.getBuilderFolder(getInputProject(), false);
			if (builderFolder != null && builderFolder.exists() && builderFolder.members().length == 0) {
				// All files in the builder folder have been deleted. Clean up
				builderFolder.delete(true, false, null);
			}
		} catch (CoreException e) {
			handleException(e);
		}
	}

	/**
	 * A non-external tool builder builder was disabled.
	 * It has been re-enabled. Translate the disabled external tool builder launch configuration
	 * wrapper back into the full fledged builder command.
	 */
	private ICommand translateBackToCommand(ILaunchConfiguration config, IProject project) {
		try {
			ICommand newCommand = project.getDescription().newCommand();
			String builderName= config.getAttribute(IExternalToolConstants.ATTR_DISABLED_BUILDER, (String)null);
			Map<String, String> args = config.getAttribute(IExternalToolConstants.ATTR_TOOL_ARGUMENTS, new HashMap<>(0));

			newCommand.setBuilderName(builderName);
			newCommand.setArguments(args);
			if (configsToBeDeleted == null) {
				configsToBeDeleted = new ArrayList<>();
			}
			configsToBeDeleted.add(config);
			return newCommand;
		} catch (CoreException exception) {
			Shell shell= getShell();
			if (shell != null) {
				MessageDialog.openError(shell, ExternalToolsUIMessages.BuilderPropertyPage_13, ExternalToolsUIMessages.BuilderPropertyPage_error);
			}
			return null;
		}
	}

	/**
	 * Disables a builder by wrapping the builder command as a disabled external
	 * tool builder. The details of the command is persisted in the launch
	 * configuration.
	 */
	private ILaunchConfiguration disableCommand(ICommand command) {
		Map<String, String> arguments = command.getArguments();
		if (arguments != null) {
			arguments.remove(COMMAND_ENABLED);
		}
		List<ILaunchConfigurationType> externalToolTypes = getConfigurationTypes(IExternalToolConstants.ID_EXTERNAL_TOOLS_BUILDER_LAUNCH_CATEGORY);
		if (externalToolTypes.isEmpty()) {
			return null;
		}
		ILaunchConfigurationType type= externalToolTypes.get(0);
		if (type == null) {
			return null;
		}
		boolean wasAutobuilding= ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
		try {
			ILaunchConfigurationWorkingCopy workingCopy = null;
			String builderName = command.getBuilderName();
			String name= DebugPlugin.getDefault().getLaunchManager().generateLaunchConfigurationName(builderName);
			workingCopy = type.newInstance(BuilderUtils.getBuilderFolder(getInputProject(), true), name);

			workingCopy.setAttribute(IExternalToolConstants.ATTR_DISABLED_BUILDER, builderName);
			if (arguments != null) {
				workingCopy.setAttribute(IExternalToolConstants.ATTR_TOOL_ARGUMENTS, arguments);
			}
			workingCopy.setAttribute(IExternalToolConstants.ATTR_BUILDER_ENABLED, false);
			ILaunchConfiguration config = null;
			setAutobuild(false);
			config = workingCopy.doSave();
			return config;
		} catch (CoreException e) {
			handleException(e);
		} finally {
			try {
				setAutobuild(wasAutobuilding);
			} catch (CoreException e) {
				handleException(e);
			}
		}
		return null;
	}

	private void deleteConfigurations() {
		boolean wasAutobuilding= ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
		try {
			setAutobuild(false);
			for (ILaunchConfiguration config : configsToBeDeleted) {
				config.delete();
			}
			checkBuilderFolder();
		} catch (CoreException e) {
			handleException(e);
		} finally {
			try {
				setAutobuild(wasAutobuilding);
			} catch (CoreException e) {
				handleException(e);
			}
		}
	}

	/**
	 * Returns whether any of the commands have changed.
	 */
	private boolean checkCommandsForChange(ICommand[] newCommands) {
		try {
			ICommand[] oldCommands = getInputProject().getDescription().getBuildSpec();
			if (oldCommands.length != newCommands.length) {
				return true;
			}
			IgnoreWhiteSpaceComparator comparator= new IgnoreWhiteSpaceComparator();
			for (int i = 0; i < oldCommands.length; i++) {
				ICommand oldCommand = oldCommands[i];
				ICommand newCommand= newCommands[i];
				String oldName= oldCommand.getBuilderName();
				String newName= newCommand.getBuilderName();
				if (oldName == null && newName != null) {
					return true;
				}

				if(oldName != null && !oldName.equals(newName)) {
					return true;
				}
				Map<String, String> oldArgs = oldCommand.getArguments();
				Map<String, String> newArgs = newCommand.getArguments();
				if (oldArgs == null) {
					if(newArgs != null) {
						return true;
					}
					continue;
				}
				if(oldArgs.size() != newArgs.size()) {
					return true;
				}
				for (Map.Entry<String, String> entry : oldArgs.entrySet()) {
					String key = entry.getKey();
					if (comparator.compare(entry.getValue(), newArgs.get(key)) != 0) {
						return true;
					}
				}
				if (oldCommand.isBuilding(IncrementalProjectBuilder.AUTO_BUILD) != newCommand.isBuilding(IncrementalProjectBuilder.AUTO_BUILD)
						|| oldCommand.isBuilding(IncrementalProjectBuilder.CLEAN_BUILD) != newCommand.isBuilding(IncrementalProjectBuilder.CLEAN_BUILD)
						|| oldCommand.isBuilding(IncrementalProjectBuilder.INCREMENTAL_BUILD) != newCommand.isBuilding(IncrementalProjectBuilder.INCREMENTAL_BUILD)
						|| oldCommand.isBuilding(IncrementalProjectBuilder.FULL_BUILD) != newCommand.isBuilding(IncrementalProjectBuilder.FULL_BUILD)) {
					return true;
				}
			}
		} catch (CoreException ce) {
			return true;
		}
		return false;
	}

	@Override
	public boolean performCancel() {
		for (ILaunchConfiguration config : newConfigList) {
			try {
				config.delete();
			} catch (CoreException e) {
				handleException(e);
			}
		}
		checkBuilderFolder();

		//remove the local marking of the enabled state of the commands
		Table builderTable= viewer.getTable();
		int numCommands = builderTable.getItemCount();
		for (int i = 0; i < numCommands; i++) {
			Object data = builderTable.getItem(i).getData();
			if (data instanceof ICommand) {
				ICommand command= (ICommand)data;
				Map<String, String> args = command.getArguments();
				args.remove(COMMAND_ENABLED);
				command.setArguments(args);
			}
		}
		return super.performCancel();
	}

	@Override
	public Shell getShell() {
		if (getControl().isDisposed()) {
			return null;
		}
		return super.getShell();
	}
}
