/*******************************************************************************
 * Copyright (c) 2009, 2012 QNX Software Systems 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:
 *     QNX Software Systems - initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.internal.ui.workingsets;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Button;
import org.eclipse.ui.IWorkingSet;

/**
 * The view controller for the working set configurations pane in the dialog. It takes care of coordinating
 * the user gestures in the pane with the working set configuration model, and vice-versa. It also implements
 * the handling of the action buttons.
 *
 * @author Christian W. Damus (cdamus)
 *
 * @since 6.0
 *
 */
class WorkingSetConfigsController implements SelectionListener, ISelectionChangedListener {

	private TreeViewer tree;
	private Button addButton;
	private Button removeButton;
	private Button renameButton;
	private Button activateButton;
	private Button buildButton;

	private IWorkingSetProxy.ISnapshot initialWorkingSet;
	private IWorkingSetProxy.ISnapshot currentWorkingSet;
	private IWorkingSetConfiguration.ISnapshot currentConfig;

	private final WorkspaceSnapshot workspace;

	private ProjectConfigsController projectsController;

	/**
	 * Initializes me we an initial working set to select.
	 */
	WorkingSetConfigsController(WorkspaceSnapshot workspace, IWorkingSetProxy.ISnapshot initialWorkingSet) {
		this.workspace = workspace;

		if (initialWorkingSet != null) {
			this.initialWorkingSet = initialWorkingSet;
		} else {
			IWorkingSet[] recent = WorkingSetConfigurationManager.WS_MGR.getRecentWorkingSets();
			if ((recent != null) && (recent.length > 0)) {
				this.initialWorkingSet = workspace.getWorkingSet(recent[0].getName());
			}
		}
	}

	/**
	 * Initializes me.
	 */
	WorkingSetConfigsController(WorkspaceSnapshot workspace) {
		this(workspace, null);
	}

	/**
	 * Assigns the tree viewer that I control.
	 *
	 * @param tree
	 *            my tree viewer
	 */
	void setTreeViewer(final TreeViewer tree) {
		if (this.tree != null) {
			this.tree.removeSelectionChangedListener(this);
		}

		this.tree = tree;

		if (this.tree != null) {
			this.tree.addSelectionChangedListener(this);

			this.tree.setInput(workspace.getWorkingSets());

			if (!workspace.getWorkingSets().isEmpty()) {
				tree.getTree().getDisplay().asyncExec(() -> {
					Object initialSelection;

					ITreeContentProvider content = (ITreeContentProvider) tree.getContentProvider();

					if ((initialWorkingSet != null)
							&& Arrays.asList(content.getElements(tree.getInput())).contains(initialWorkingSet)) {
						initialSelection = initialWorkingSet;
					} else {
						// we have a most-recently-used working set. Just
						// take the first in tree order
						initialSelection = tree.getTree().getItem(0).getData();
					}

					Object[] children = content.getChildren(initialSelection);
					IStructuredSelection sel;

					if ((children == null) || (children.length == 0)) {
						// Shouldn't happen: there should at least be the
						// read-only config.
						// Can only select the initial working set
						sel = new StructuredSelection(initialSelection);
					} else {
						Object[] toSort = new Object[children.length];
						System.arraycopy(children, 0, toSort, 0, children.length);
						tree.getComparator().sort(tree, toSort);
						sel = new StructuredSelection(toSort[0]);
					}

					// make the selection
					tree.setSelection(sel, true);
				});
			}
		}
	}

	private Button updateButton(Button oldButton, Button newButton) {
		if (oldButton != null) {
			oldButton.removeSelectionListener(this);
		}

		if (newButton != null) {
			newButton.addSelectionListener(this);
		}

		return newButton;
	}

	/**
	 * Assigns me my "Add..." button.
	 *
	 * @param addButton
	 *            my add button
	 */
	void setAddButton(Button addButton) {
		this.addButton = updateButton(this.addButton, addButton);
	}

	/**
	 * Assigns me my "Remove" button.
	 *
	 * @param removeButton
	 *            my remove button
	 */
	void setRemoveButton(Button removeButton) {
		this.removeButton = updateButton(this.removeButton, removeButton);
	}

	/**
	 * Assigns me my "Rename..." button.
	 *
	 * @param renameButton
	 *            my rename button
	 */
	void setRenameButton(Button renameButton) {
		this.renameButton = updateButton(this.renameButton, renameButton);
	}

	/**
	 * Assigns me my "Activate" button.
	 *
	 * @param activateButton
	 *            my activate button
	 */
	void setActivateButton(Button activateButton) {
		this.activateButton = updateButton(this.activateButton, activateButton);
	}

	/**
	 * Assigns me my "Build" button.
	 *
	 * @param buildButton
	 *            my build button
	 */
	void setBuildButton(Button buildButton) {
		this.buildButton = updateButton(this.buildButton, buildButton);
	}

	/**
	 * Connects me to the controller for the project configurations pane, into which I inject the currently
	 * selected working set configuration.
	 *
	 * @param controller
	 *            my project configurations controller
	 */
	void setProjectConfigsController(ProjectConfigsController controller) {
		this.projectsController = controller;

		if (controller != null) {
			controller.setWorkingSetConfiguration(currentConfig);
		}
	}

	@Override
	public void widgetDefaultSelected(SelectionEvent e) {
		// not interesting
	}

	/**
	 * Handles button presses in the working set configurations pane.
	 */
	@Override
	public void widgetSelected(SelectionEvent e) {
		// handle button press
		if (e.widget == addButton) {
			addConfig();
		} else if (e.widget == removeButton) {
			removeConfig();
		} else if (e.widget == renameButton) {
			renameConfig();
		} else if (e.widget == activateButton) {
			activateConfig();
		} else if (e.widget == buildButton) {
			buildConfig();
		}
	}

	/**
	 * Handles selection of working sets and their configurations. Among potentially other actions, this
	 * injects the working-set configuration selection into the project configurations controller and updates
	 * the enablement of the buttons.
	 */
	@Override
	public void selectionChanged(SelectionChangedEvent event) {
		currentConfig = null;
		currentWorkingSet = null;

		if (event.getSelection() instanceof IStructuredSelection) {
			IStructuredSelection sel = (IStructuredSelection) event.getSelection();

			if (!sel.isEmpty()) {
				Object first = sel.getFirstElement();

				if (first instanceof IWorkingSetConfiguration) {
					currentConfig = (IWorkingSetConfiguration.ISnapshot) first;
					currentWorkingSet = currentConfig.getWorkingSet();
				} else if (first instanceof IWorkingSetProxy) {
					currentWorkingSet = (IWorkingSetProxy.ISnapshot) first;
				}
			}
		}

		if (projectsController != null) {
			// tell the project controller
			projectsController.setWorkingSetConfiguration(currentConfig);
		}

		updateButtons();
	}

	/**
	 * Handler for the "Add..." button.
	 */
	private void addConfig() {
		InputDialog dlg = new InputDialog(tree.getTree().getShell(),
				WorkingSetMessages.WSConfigsController_addDlg_title, WorkingSetMessages.WSConfigsController_addDlg_msg,
				WorkingSetMessages.WSConfigsController_addDlg_defaultName, newText -> {
					if (currentWorkingSet.getConfiguration(newText) != null) {
						return WorkingSetMessages.WSConfigsController_addDlg_nameExists;
					}
					if (newText.length() == 0) {
						return WorkingSetMessages.WSConfigsController_addDlg_emptyName;
					}
					return null;
				});

		if (dlg.open() == IDialogConstants.OK_ID) {
			IWorkingSetConfiguration.ISnapshot newConfig = currentWorkingSet.createConfiguration(dlg.getValue());
			tree.refresh(currentWorkingSet);
			tree.setSelection(new StructuredSelection(newConfig), true);
			currentConfig = newConfig;
			currentWorkingSet = currentConfig.getWorkingSet();

			// this is a "recently used" working set
			IWorkingSet ws = currentWorkingSet.resolve();
			if (ws != null) {
				WorkingSetConfigurationManager.WS_MGR.addRecentWorkingSet(ws);
			}
		}
	}

	/**
	 * Handler for the "Remove" button.
	 */
	private void removeConfig() {
		currentWorkingSet.removeConfiguration(currentConfig);
		tree.refresh(currentWorkingSet);
	}

	/**
	 * Handler for the "Rename..." button.
	 */
	private void renameConfig() {
		InputDialog dlg = new InputDialog(tree.getTree().getShell(),
				WorkingSetMessages.WSConfigsController_renameDlg_title,
				WorkingSetMessages.WSConfigsController_renameDlg_msg, currentConfig.getName(), newText -> {
					if (newText.equals(currentConfig.getName())) {
						return ""; //$NON-NLS-1$
					}
					if (currentWorkingSet.getConfiguration(newText) != null) {
						return WorkingSetMessages.WSConfigsController_addDlg_nameExists;
					}
					if (newText.length() == 0) {
						return WorkingSetMessages.WSConfigsController_addDlg_emptyName;
					}
					return null;
				});

		if (dlg.open() == IDialogConstants.OK_ID) {
			currentConfig.setName(dlg.getValue());
			tree.refresh(currentWorkingSet);
		}
	}

	/**
	 * Handler for the "Activate" button.
	 */
	private void activateConfig() {
		currentConfig.activate();
		projectsController.update();
		updateForActivation();
	}

	/**
	 * Updates the display to reflect potential changes in project activation and the resulting changes in
	 * working-set config activation, if any.
	 */
	private void updateForActivation() {
		// update all working-set configs that intersect this config
		Collection<IWorkingSetProxy.ISnapshot> unaffectedWorkingSets = new java.util.HashSet<>(
				workspace.getWorkingSets());

		for (IProject project : currentConfig.getWorkingSet().resolveProjects()) {
			for (Iterator<IWorkingSetProxy.ISnapshot> iter = unaffectedWorkingSets.iterator(); iter.hasNext();) {
				IWorkingSetProxy.ISnapshot next = iter.next();

				if (next.resolveProjects().contains(project)) {
					iter.remove();

					if (next.updateActiveConfigurations()) {
						// major change. Refresh it altogether
						tree.refresh(next);
					} else {
						// lighter-weight updates of its configs
						for (IWorkingSetConfiguration config : next.getConfigurations()) {
							tree.update(config, null);
						}
					}
				}
			}
		}

		updateButtons();
	}

	/**
	 * Handler for the "Build" button.
	 */
	private void buildConfig() {
		final IStatus[] problem = new IStatus[1];

		try {
			ProgressMonitorDialog dlg = new ProgressMonitorDialog(tree.getControl().getShell());
			dlg.run(true, true, monitor -> {
				IStatus status = currentConfig.build(monitor);
				if (status.matches(IStatus.WARNING | IStatus.ERROR)) {
					problem[0] = status;
				}
			});
		} catch (Exception e) {
			CUIPlugin.log(WorkingSetMessages.WSConfigsController_buildFailedLog, e);
		}

		// it is possible that some project configurations had to applied in
		// order to effect a build. Refresh to handle that case
		updateForActivation();

		if (problem[0] != null) {
			// show the problem
			ErrorDialog.openError(tree.getControl().getShell(),
					WorkingSetMessages.WSConfigsController_buildFailedDlgTitle,
					WorkingSetMessages.WSConfigsController_buildFailedDlgMsg, problem[0]);
		}
	}

	/**
	 * Updates the enablement state of the action buttons according to the current selection.
	 */
	private void updateButtons() {
		if (addButton != null) {
			addButton.setEnabled(currentWorkingSet != null);
		}
		if (removeButton != null) {
			removeButton.setEnabled((currentConfig != null) && !currentConfig.isReadOnly());
		}
		if (renameButton != null) {
			renameButton.setEnabled((currentConfig != null) && !currentConfig.isReadOnly());
		}
		if (activateButton != null) {
			activateButton.setEnabled((currentConfig != null) && !currentConfig.isActive());
		}
		if (buildButton != null) {
			buildButton.setEnabled(currentConfig != null);
		}
	}

	/**
	 * Notification that the selection of configuration(s) in some project in the current working set
	 * configuration has changed. I accordingly update the visuals of the working-set configuration to
	 * indicate whether it is active or not.
	 *
	 * @param project
	 *            the project configuration whose active configuration selections have changed
	 */
	void projectSelectionsChanged(IWorkingSetProjectConfiguration project) {
		tree.update(currentConfig, null);
		updateButtons(); // depends on whether the ws config is active
	}
}