/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.workingsets;

import java.util.Arrays;
import java.util.HashSet;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TreeItem;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.wizard.WizardPage;

import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.IWorkingSetPage;


/**
 * A tree viewer on the left is used to show the workspace content, a table viewer on the
 * right is used to show the working set content. Buttons to move content from right
 * to left and vice versa are available between the two viewers. A text field allows to
 * set/change the working sets name.
 *
 * @since 3.1
 */
public abstract class AbstractWorkingSetWizardPage extends WizardPage implements IWorkingSetPage {

	private final class AddedElementsFilter extends ViewerFilter {

		@Override
		public boolean select(Viewer viewer, Object parentElement, Object element) {
			return !fSelectedElements.contains(element);
		}

	}

	private Text fWorkingSetName;
	private TreeViewer fTree;
	private TableViewer fTable;
	private ITreeContentProvider fTreeContentProvider;

	private boolean fFirstCheck;
	private final HashSet<Object> fSelectedElements;
	private IWorkingSet fWorkingSet;

	protected AbstractWorkingSetWizardPage(String pageName, String title, ImageDescriptor titleImage) {
		super(pageName, title, titleImage);

		fSelectedElements= new HashSet<>();
		fFirstCheck= true;
	}

	/**
	 * Returns the page id as specified in the extension point.
	 * @return the page id
	 */
	protected abstract String getPageId();

	/**
	 * Configure the tree viewer used on the left side of the dialog.
	 *
	 * Implementors must set:
	 * <ul>
	 * 	<li>The content provider</li>
	 *  <li>The label provider</li>
	 *  <li>The viewers input</li>
	 * </ul>
	 * They may also set:
	 * <ul>
	 *   <li>The viewer comparator</li>
	 *   <li>Any viewer filter</li>
	 *   <li>The selection</li>
	 * </ul>
	 *
	 * @param tree the tree to configure
	 */
	protected abstract void configureTree(TreeViewer tree);

	/**
	 * Configure the table viewer used on the right side of the dialog.
	 *
	 * Implementors must set:
	 * <ul>
	 *  <li>The label provider</li>
	 * </ul>
	 * They may also set:
	 * <ul>
	 *   <li>The viewer comparator</li>
	 * </ul>
	 * They must not set:
	 * <ul>
	 * 	<li>The viewers content provider</li>
	 * 	<li>The viewers input</li>
	 *  <li>Any viewer filter</li>
	 * </ul>
	 *
	 * @param table the table to configure
	 */
	protected abstract void configureTable(TableViewer table);

	/**
	 * Returns the elements which are shown in the table initially. Return an empty
	 * array if the table should be empty. The given working set is the working set
	 * which will be configured by this dialog, or <b>null</b> if it does not yet
	 * exist.
	 *
	 * @param workingSet the working set to configure or <b>null</b> if not yet exist
	 * @return the elements to show in the table
	 */
	protected abstract Object[] getInitialWorkingSetElements(IWorkingSet workingSet);

	/*
	 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
	 */
	@Override
	public void createControl(Composite parent) {
		initializeDialogUnits(parent);

		Composite composite= new Composite(parent, SWT.NONE);
		composite.setLayout(new GridLayout());
		composite.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL));
		setControl(composite);

		Label label= new Label(composite, SWT.WRAP);
		label.setText(WorkingSetMessages.JavaWorkingSetPage_workingSet_name);
		GridData gd= new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL | GridData.VERTICAL_ALIGN_CENTER);
		label.setLayoutData(gd);

		fWorkingSetName= new Text(composite, SWT.SINGLE | SWT.BORDER);
		fWorkingSetName.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL));
		fWorkingSetName.addModifyListener(
			e -> validateInput()
		);

		Composite leftCenterRightComposite= new Composite(composite, SWT.NONE);
		GridData gridData= new GridData(SWT.FILL, SWT.FILL, true, true);
		gridData.heightHint= convertHeightInCharsToPixels(20);
		leftCenterRightComposite.setLayoutData(gridData);
		GridLayout gridLayout= new GridLayout(3, false);
		gridLayout.marginHeight= 0;
		gridLayout.marginWidth= 0;
		leftCenterRightComposite.setLayout(gridLayout);

		Composite leftComposite= new Composite(leftCenterRightComposite, SWT.NONE);
		gridData= new GridData(SWT.FILL, SWT.FILL, true, true);
		gridData.widthHint= convertWidthInCharsToPixels(40);
		leftComposite.setLayoutData(gridData);
		gridLayout= new GridLayout(1, false);
		gridLayout.marginHeight= 0;
		gridLayout.marginWidth= 0;
		leftComposite.setLayout(gridLayout);

		Composite centerComposite = new Composite(leftCenterRightComposite, SWT.NONE);
		gridLayout= new GridLayout(1, false);
		gridLayout.marginHeight= 0;
		gridLayout.marginWidth= 0;
		centerComposite.setLayout(gridLayout);
		centerComposite.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, false, false));

		Composite rightComposite= new Composite(leftCenterRightComposite, SWT.NONE);
		gridData= new GridData(SWT.FILL, SWT.FILL, true, true);
		gridData.widthHint= convertWidthInCharsToPixels(40);
		rightComposite.setLayoutData(gridData);
		gridLayout= new GridLayout(1, false);
		gridLayout.marginHeight= 0;
		gridLayout.marginWidth= 0;
		rightComposite.setLayout(gridLayout);

		createTree(leftComposite);
		createTable(rightComposite);

		if (fWorkingSet != null)
			fWorkingSetName.setText(fWorkingSet.getName());

		initializeSelectedElements();
		validateInput();

		fTable.setInput(fSelectedElements);
		fTable.refresh(true);
		fTree.refresh(true);

		createButtonBar(centerComposite);

		fWorkingSetName.setFocus();
		fWorkingSetName.setSelection(0, fWorkingSetName.getText().length());

		Dialog.applyDialogFont(composite);
	}

	private void createTree(Composite parent) {

		Label label= new Label(parent, SWT.NONE);
		label.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
		label.setText(WorkingSetMessages.JavaWorkingSetPage_workspace_content);

		fTree= new TreeViewer(parent, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI);
		fTree.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		fTree.addFilter(new AddedElementsFilter());
		fTree.setUseHashlookup(true);

		configureTree(fTree);

		fTreeContentProvider= (ITreeContentProvider) fTree.getContentProvider();
	}

	private void createButtonBar(Composite parent) {
		Label spacer= new Label(parent, SWT.NONE);
		spacer.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));

		final Button addButton= new Button(parent, SWT.PUSH);
		addButton.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
		addButton.setText(WorkingSetMessages.JavaWorkingSetPage_add_button);
		addButton.setEnabled(!fTree.getSelection().isEmpty());

		final Button addAllButton= new Button(parent, SWT.PUSH);
		addAllButton.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
		addAllButton.setText(WorkingSetMessages.JavaWorkingSetPage_addAll_button);
		addAllButton.setEnabled(fTree.getTree().getItems().length > 0);

		final Button removeButton= new Button(parent, SWT.PUSH);
		removeButton.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
		removeButton.setText(WorkingSetMessages.JavaWorkingSetPage_remove_button);
		removeButton.setEnabled(!fTable.getSelection().isEmpty());

		final Button removeAllButton= new Button(parent, SWT.PUSH);
		removeAllButton.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, false, false));
		removeAllButton.setText(WorkingSetMessages.JavaWorkingSetPage_removeAll_button);
		removeAllButton.setEnabled(!fSelectedElements.isEmpty());

		fTree.addSelectionChangedListener(event -> addButton.setEnabled(!event.getSelection().isEmpty()));

		addButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				addTreeSelection();

				removeAllButton.setEnabled(true);
				addAllButton.setEnabled(fTree.getTree().getItems().length > 0);
			}
		});

		fTree.addDoubleClickListener(event -> {
			addTreeSelection();

			removeAllButton.setEnabled(true);
			addAllButton.setEnabled(fTree.getTree().getItems().length > 0);
		});

		fTable.addSelectionChangedListener(event -> removeButton.setEnabled(!event.getSelection().isEmpty()));

		removeButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				removeTableSelection();

				addAllButton.setEnabled(true);
				removeAllButton.setEnabled(!fSelectedElements.isEmpty());
			}
		});

		fTable.addDoubleClickListener(event -> {
			removeTableSelection();

			addAllButton.setEnabled(true);
			removeAllButton.setEnabled(!fSelectedElements.isEmpty());
		});

		addAllButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				for (TreeItem item : fTree.getTree().getItems()) {
					fSelectedElements.add(item.getData());
				}
				fTable.refresh();
				fTree.refresh();

				addAllButton.setEnabled(false);
				removeAllButton.setEnabled(true);
			}
		});

		removeAllButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				fSelectedElements.clear();

				fTable.refresh();
				fTree.refresh();

				removeAllButton.setEnabled(false);
				addAllButton.setEnabled(true);
			}
		});

	}

	/**
	 * Moves selected elements in the tree into the table
	 */
	private void addTreeSelection() {
		IStructuredSelection selection= (IStructuredSelection) fTree.getSelection();
		fSelectedElements.addAll(selection.toList());
		Object[] selectedElements= selection.toArray();
		fTable.add(selectedElements);
		fTree.remove(selectedElements);
		fTable.setSelection(selection);
		fTable.getControl().setFocus();
		validateInput();
	}

	/**
	 * Moves the selected elements in the table into the tree
	 */
	private void removeTableSelection() {
		IStructuredSelection selection= (IStructuredSelection) fTable.getSelection();
		fSelectedElements.removeAll(selection.toList());
		Object[] selectedElements= selection.toArray();
		fTable.remove(selectedElements);
		try {
			fTree.getTree().setRedraw(false);
			for (Object selectedElement : selectedElements) {
				fTree.refresh(fTreeContentProvider.getParent(selectedElement), true);
			}
		} finally {
			fTree.getTree().setRedraw(true);
		}
		fTree.setSelection(selection);
		fTree.getControl().setFocus();
		validateInput();
	}

	private void createTable(Composite parent) {
		Label label= new Label(parent, SWT.WRAP);
		label.setText(WorkingSetMessages.JavaWorkingSetPage_workingSet_content);
		label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));

		fTable= new TableViewer(parent, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI);

		GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
		fTable.getControl().setLayoutData(gd);

		fTable.setUseHashlookup(true);

		configureTable(fTable);

		fTable.setContentProvider(ArrayContentProvider.getInstance());
		fTable.setInput(fSelectedElements);
	}

	/*
	 * Implements method from IWorkingSetPage
	 */
	@Override
	public IWorkingSet getSelection() {
		return fWorkingSet;
	}

	/*
	 * Implements method from IWorkingSetPage
	 */
	@Override
	public void setSelection(IWorkingSet workingSet) {
		Assert.isNotNull(workingSet, "Working set must not be null"); //$NON-NLS-1$
		fWorkingSet= workingSet;
		if (getContainer() != null && getShell() != null && fWorkingSetName != null) {
			fFirstCheck= false;
			fWorkingSetName.setText(fWorkingSet.getName());
			initializeSelectedElements();
			validateInput();
		}
	}

	/*
	 * Implements method from IWorkingSetPage
	 */
	@Override
	public void finish() {
		String workingSetName= fWorkingSetName.getText();
		HashSet<Object> elements= fSelectedElements;

		if (fWorkingSet == null) {
			IWorkingSetManager workingSetManager= PlatformUI.getWorkbench().getWorkingSetManager();
			fWorkingSet= workingSetManager.createWorkingSet(workingSetName, elements.toArray(new IAdaptable[elements.size()]));
			fWorkingSet.setId(getPageId());
		} else {
			// Add inaccessible resources
			HashSet<IProject> closedProjectsToRetain= new HashSet<>(elements.size());
			HashSet<IProject> closedProjectsToRemove= new HashSet<>(elements.size());
			for (IAdaptable oldItem : fWorkingSet.getElements()) {
				IResource oldResource= null;
				if (oldItem instanceof IResource) {
					oldResource= (IResource) oldItem;
				} else {
					oldResource= oldItem.getAdapter(IResource.class);
				}
				if (oldResource != null && oldResource.isAccessible() == false) {
					IProject project= oldResource.getProject();
					if (oldResource.equals(project)) {
						closedProjectsToRetain.add(project);
					} else if (elements.contains(project)) {
						elements.add(oldItem);
						closedProjectsToRemove.add(project);
					}
				}
			}
			closedProjectsToRemove.removeAll(closedProjectsToRetain);
			elements.removeAll(closedProjectsToRemove);

			fWorkingSet.setName(workingSetName);
			fWorkingSet.setElements(elements.toArray(new IAdaptable[elements.size()]));
		}
	}

	private void validateInput() {
		String errorMessage= null;
		String infoMessage= null;
		String newText= fWorkingSetName.getText();

		if (newText.equals(newText.trim()) == false)
			errorMessage = WorkingSetMessages.JavaWorkingSetPage_warning_nameWhitespace;
		if (newText.isEmpty()) {
			if (fFirstCheck) {
				setPageComplete(false);
				fFirstCheck= false;
				return;
			} else
				errorMessage= WorkingSetMessages.JavaWorkingSetPage_warning_nameMustNotBeEmpty;
		}

		fFirstCheck= false;

		if (errorMessage == null && (fWorkingSet == null || newText.equals(fWorkingSet.getName()) == false)) {
			for (IWorkingSet workingSet : PlatformUI.getWorkbench().getWorkingSetManager().getWorkingSets()) {
				if (newText.equals(workingSet.getName())) {
					errorMessage= WorkingSetMessages.JavaWorkingSetPage_warning_workingSetExists;
				}
			}
		}

		if (!hasSelectedElement())
			infoMessage= WorkingSetMessages.JavaWorkingSetPage_warning_resourceMustBeChecked;

		setMessage(infoMessage, INFORMATION);
		setErrorMessage(errorMessage);
		setPageComplete(errorMessage == null);
	}

	private boolean hasSelectedElement() {
		return !fSelectedElements.isEmpty();
	}

	private void initializeSelectedElements() {
		fSelectedElements.addAll(Arrays.asList(getInitialWorkingSetElements(fWorkingSet)));
	}

}
