/*******************************************************************************
 *  Copyright (c) 2008, 2011 IBM Corporation and others.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 * 
 *  Contributors:
 *     IBM Corporation - initial API and implementation
 *     Sonatype, Inc. - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.dialogs;

import java.net.URI;
import java.util.ArrayList;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.equinox.internal.p2.ui.*;
import org.eclipse.equinox.internal.p2.ui.model.*;
import org.eclipse.equinox.internal.p2.ui.query.IUViewQueryContext;
import org.eclipse.equinox.internal.p2.ui.viewers.*;
import org.eclipse.equinox.internal.provisional.p2.repository.RepositoryEvent;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.repository.IRepositoryManager;
import org.eclipse.equinox.p2.ui.LoadMetadataRepositoryJob;
import org.eclipse.equinox.p2.ui.ProvisioningUI;
import org.eclipse.jface.viewers.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;

/**
 * An AvailableIUGroup is a reusable UI component that displays the
 * IU's available for installation.  By default, content from all available
 * repositories is shown.
 * 
 * @since 3.4
 */
public class AvailableIUGroup extends StructuredIUGroup {

	/**
	 * Show contents from all repositories
	 */
	public static final int AVAILABLE_ALL = 1;

	/**
	 * Don't show any repository content
	 */
	public static final int AVAILABLE_NONE = 2;

	/**
	 * Show local repository content
	 */
	public static final int AVAILABLE_LOCAL = 3;

	/**
	 * Show content for a specific repository
	 */
	public static final int AVAILABLE_SPECIFIED = 4;

	IUViewQueryContext queryContext;
	int filterConstant = AVAILABLE_ALL;
	URI repositoryFilter;
	QueryableMetadataRepositoryManager queryableManager;
	// We restrict the type of the filter used because PatternFilter does
	// unnecessary accesses of children that cause problems with the deferred
	// tree.
	AvailableIUPatternFilter filter;
	private boolean useBold = false;
	private IUDetailsLabelProvider labelProvider;
	private int repoFlags;
	Display display;
	DelayedFilterCheckboxTree filteredTree;
	Job lastRequestedLoadJob;

	/**
	 * Create a group that represents the available IU's from all available
	 * repositories.  The default policy controls the visibility flags for
	 * repositories and IU's.
	 * 
	 * @param parent the parent composite for the group
	 */
	public AvailableIUGroup(ProvisioningUI ui, final Composite parent) {
		this(ui, parent, parent.getFont(), null, getDefaultColumnConfig(), AVAILABLE_ALL);
	}

	private static IUColumnConfig[] getDefaultColumnConfig() {
		// increase primary column width because we might be nesting names under categories and require more space than a flat list
		IUColumnConfig nameColumn = new IUColumnConfig(ProvUIMessages.ProvUI_NameColumnTitle, IUColumnConfig.COLUMN_NAME, ILayoutConstants.DEFAULT_PRIMARY_COLUMN_WIDTH + 15);
		IUColumnConfig versionColumn = new IUColumnConfig(ProvUIMessages.ProvUI_VersionColumnTitle, IUColumnConfig.COLUMN_VERSION, ILayoutConstants.DEFAULT_COLUMN_WIDTH);
		return new IUColumnConfig[] {nameColumn, versionColumn};
	}

	/**
	 * Create a group that represents the available IU's.
	 * 
	 * @param ui the policy to use for deciding what should be shown
	 * @param parent the parent composite for the group
	 * @param font The font to use for calculating pixel sizes.  This font is
	 * not managed by the receiver.
	 * @param queryContext the IUViewQueryContext that determines additional
	 * information about what is shown, such as the visible repositories
	 * @param columnConfig the description of the columns that should be shown.  If <code>null</code>, a default
	 * will be used.
	 * @param filterConstant a constant specifying which repositories are used when showing content
	 */
	public AvailableIUGroup(ProvisioningUI ui, final Composite parent, Font font, IUViewQueryContext queryContext, IUColumnConfig[] columnConfig, int filterConstant) {
		super(ui, parent, font, columnConfig);
		this.display = parent.getDisplay();
		if (queryContext == null)
			this.queryContext = ProvUI.getQueryContext(getPolicy());
		else
			this.queryContext = queryContext;
		repoFlags = ui.getRepositoryTracker().getMetadataRepositoryFlags();
		this.queryableManager = new QueryableMetadataRepositoryManager(ui, false);
		this.filterConstant = filterConstant;
		this.filter = new AvailableIUPatternFilter(getColumnConfig());
		this.filter.setIncludeLeadingWildcard(true);
		createGroupComposite(parent);
	}

	protected StructuredViewer createViewer(Composite parent) {
		// Table of available IU's
		filteredTree = new DelayedFilterCheckboxTree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER, filter, getPreFilterJobProvider());
		final TreeViewer availableIUViewer = filteredTree.getViewer();

		availableIUViewer.getTree().setFont(parent.getFont());
		filteredTree.getFilterControl().setFont(parent.getFont());

		// If the user expanded or collapsed anything while we were loading a repo
		// in the background, we would not want to disrupt their work by making
		// a newly loaded visible and expanding it.  Setting the load job to null 
		// will take care of this.
		availableIUViewer.getTree().addTreeListener(new TreeListener() {
			public void treeCollapsed(TreeEvent e) {
				lastRequestedLoadJob = null;
			}

			public void treeExpanded(TreeEvent e) {
				lastRequestedLoadJob = null;
			}
		});

		labelProvider = new IUDetailsLabelProvider(filteredTree, getColumnConfig(), getShell());
		labelProvider.setUseBoldFontForFilteredItems(useBold);
		labelProvider.setToolTipProperty(IInstallableUnit.PROP_DESCRIPTION);

		// Filters and sorters before establishing content, so we don't refresh unnecessarily.
		IUComparator comparator = new IUComparator(IUComparator.IU_NAME);
		comparator.useColumnConfig(getColumnConfig());
		availableIUViewer.setComparator(comparator);
		availableIUViewer.setComparer(new ProvElementComparer());

		// Now the content provider.
		DeferredQueryContentProvider contentProvider = new DeferredQueryContentProvider();
		availableIUViewer.setContentProvider(contentProvider);

		// Now the presentation, columns before label provider.
		setTreeColumns(availableIUViewer.getTree());
		availableIUViewer.setLabelProvider(labelProvider);

		// Notify the filtered tree so that it can hook listeners on the
		// content provider.  This is needed so that filtering is only allowed
		// after content has been retrieved.
		filteredTree.contentProviderSet(contentProvider);

		final StructuredViewerProvisioningListener listener = new StructuredViewerProvisioningListener(getClass().getName(), availableIUViewer, ProvUIProvisioningListener.PROV_EVENT_METADATA_REPOSITORY, getProvisioningUI().getOperationRunner()) {
			protected void repositoryAdded(final RepositoryEvent event) {
				makeRepositoryVisible(event.getRepositoryLocation());
			}

			protected void refreshViewer() {
				final TreeViewer treeViewer = filteredTree.getViewer();
				final Tree tree = treeViewer.getTree();
				IWorkbench workbench = PlatformUI.getWorkbench();
				if (workbench.isClosing())
					return;
				if (tree != null && !tree.isDisposed()) {
					updateAvailableViewState();
				}

			}
		};
		ProvUI.getProvisioningEventBus(getProvisioningUI().getSession()).addListener(listener);

		availableIUViewer.getControl().addDisposeListener(new DisposeListener() {
			public void widgetDisposed(DisposeEvent e) {
				ProvUI.getProvisioningEventBus(getProvisioningUI().getSession()).removeListener(listener);
			}
		});
		updateAvailableViewState();
		return availableIUViewer;
	}

	private void setTreeColumns(Tree tree) {
		tree.setHeaderVisible(true);

		IUColumnConfig[] cols = getColumnConfig();
		for (int i = 0; i < cols.length; i++) {
			TreeColumn tc = new TreeColumn(tree, SWT.NONE, i);
			tc.setResizable(true);
			tc.setText(cols[i].getColumnTitle());
			tc.setWidth(cols[i].getWidthInPixels(tree));
		}
	}

	Object getNewInput() {
		if (repositoryFilter != null) {
			return new MetadataRepositoryElement(queryContext, getProvisioningUI(), repositoryFilter, true);
		} else if (filterConstant == AVAILABLE_NONE) {
			// Dummy object that explains empty site list
			return new ProvElement(null) {
				public Object[] getChildren(Object o) {
					String description;
					String name;
					int severity;
					if (!getPolicy().getRepositoriesVisible()) {
						// shouldn't get here ideally.  No sites and no way to add any.
						severity = IStatus.ERROR;
						name = ProvUIMessages.AvailableIUGroup_NoSitesConfiguredExplanation;
						description = ProvUIMessages.AvailableIUGroup_NoSitesConfiguredDescription;
					} else {
						severity = IStatus.INFO;
						name = ProvUIMessages.AvailableIUGroup_NoSitesExplanation;
						description = ProvUIMessages.ColocatedRepositoryManipulator_NoContentExplanation;
					}
					return new Object[] {new EmptyElementExplanation(null, severity, name, description)};
				}

				public String getLabel(Object o) {
					// Label not needed for input
					return null;
				}
			};
		} else {
			queryableManager.setRespositoryFlags(repoFlags);
			return new MetadataRepositories(queryContext, getProvisioningUI(), queryableManager);
		}
	}

	/**
	 * Set a boolean indicating whether a bold font should be used when
	 * showing filtered items.  This method does not refresh the tree or 
	 * labels, so that must be done explicitly by the caller.
	 * @param useBoldFont
	 */
	public void setUseBoldFontForFilteredItems(boolean useBoldFont) {
		if (labelProvider != null)
			labelProvider.setUseBoldFontForFilteredItems(useBoldFont);
	}

	/**
	 * Return the composite that contains the controls in this group.
	 * @return the composite
	 */
	public Composite getComposite() {
		return super.getComposite();
	}

	/**
	 * Get the viewer used to represent the available IU's
	 * @return the viewer
	 */
	public StructuredViewer getStructuredViewer() {
		return super.getStructuredViewer();
	}

	/**
	 * Get the selected IU's
	 * @return the array of selected IU's
	 */
	// overridden for visibility in the public package
	public java.util.List<IInstallableUnit> getSelectedIUs() {
		return super.getSelectedIUs();
	}

	// overridden to weed out non-IU elements, such as repositories or empty explanations
	public Object[] getSelectedIUElements() {
		Object[] elements = ((IStructuredSelection) viewer.getSelection()).toArray();
		ArrayList<Object> list = new ArrayList<Object>(elements.length);
		for (int i = 0; i < elements.length; i++)
			if (ElementUtils.getIU(elements[i]) != null)
				list.add(elements[i]);
		return list.toArray();
	}

	public CheckboxTreeViewer getCheckboxTreeViewer() {
		return filteredTree.getCheckboxTreeViewer();
	}

	/**
	 * Get the selected IU's
	 * @return the array of checked IU's
	 */
	public IInstallableUnit[] getCheckedLeafIUs() {
		Object[] selections = filteredTree.getCheckboxTreeViewer().getCheckedElements();
		if (selections.length == 0)
			return new IInstallableUnit[0];
		ArrayList<IInstallableUnit> leaves = new ArrayList<IInstallableUnit>(selections.length);
		for (int i = 0; i < selections.length; i++) {
			if (!getCheckboxTreeViewer().getGrayed(selections[i])) {
				IInstallableUnit iu = ProvUI.getAdapter(selections[i], IInstallableUnit.class);
				if (iu != null && !ProvUI.isCategory(iu) && !leaves.contains(iu))
					leaves.add(iu);
			}
		}
		return leaves.toArray(new IInstallableUnit[leaves.size()]);
	}

	public Tree getTree() {
		if (viewer == null)
			return null;
		return ((TreeViewer) viewer).getTree();
	}

	/*
	 * Make the repository with the specified location visible in the viewer.
	 */
	void makeRepositoryVisible(final URI location) {
		// If we are viewing by anything other than site, there is no specific way
		// to make a repo visible. 
		if (!(queryContext.getViewType() == IUViewQueryContext.AVAILABLE_VIEW_BY_REPO)) {
			if (Display.getCurrent() == null)
				display.asyncExec(new Runnable() {
					public void run() {
						updateAvailableViewState();
					}
				});
			else
				updateAvailableViewState();
			return;
		}
		// First reset the input so that the new repo shows up
		Runnable runnable = new Runnable() {
			public void run() {
				final TreeViewer treeViewer = filteredTree.getViewer();
				final Tree tree = treeViewer.getTree();
				IWorkbench workbench = PlatformUI.getWorkbench();
				if (workbench.isClosing())
					return;
				if (tree != null && !tree.isDisposed()) {
					updateAvailableViewState();
				}
			}
		};
		if (Display.getCurrent() == null)
			display.asyncExec(runnable);
		else
			runnable.run();
		// We don't know if loading will be a fast or slow operation.
		// We do it in a job to be safe, and when it's done, we update
		// the UI.
		Job job = new Job(NLS.bind(ProvUIMessages.AvailableIUGroup_LoadingRepository, URIUtil.toUnencodedString(location))) {
			protected IStatus run(IProgressMonitor monitor) {
				try {
					getProvisioningUI().loadMetadataRepository(location, true, monitor);
					return Status.OK_STATUS;
				} catch (ProvisionException e) {
					return e.getStatus();
				} catch (OperationCanceledException e) {
					return Status.CANCEL_STATUS;
				}
			}
		};
		job.setPriority(Job.LONG);
		job.setSystem(true);
		job.setUser(false);
		job.addJobChangeListener(new JobChangeAdapter() {
			public void done(final IJobChangeEvent event) {
				if (event.getResult().isOK())
					display.asyncExec(new Runnable() {
						@SuppressWarnings("rawtypes")
						public void run() {
							final TreeViewer treeViewer = filteredTree.getViewer();
							IWorkbench workbench = PlatformUI.getWorkbench();
							if (workbench.isClosing())
								return;
							// Expand only if there have been no other jobs started for other repos.
							if (event.getJob() == lastRequestedLoadJob) {
								final Tree tree = treeViewer.getTree();
								if (tree != null && !tree.isDisposed()) {
									TreeItem[] items = tree.getItems();
									for (int i = 0; i < items.length; i++) {
										if (items[i].getData() instanceof IRepositoryElement) {
											URI url = ((IRepositoryElement) items[i].getData()).getLocation();
											if (url.equals(location)) {
												treeViewer.expandToLevel(items[i].getData(), AbstractTreeViewer.ALL_LEVELS);
												tree.select(items[i]);
												return;
											}
										}
									}
								}
							}
						}
					});
			}
		});
		lastRequestedLoadJob = job;
		job.schedule();
	}

	public void updateAvailableViewState() {
		if (getTree() == null || getTree().isDisposed())
			return;
		final Composite parent = getComposite().getParent();
		setUseBoldFontForFilteredItems(queryContext.getViewType() != IUViewQueryContext.AVAILABLE_VIEW_FLAT);

		BusyIndicator.showWhile(display, new Runnable() {
			public void run() {
				parent.setRedraw(false);
				getCheckboxTreeViewer().setInput(getNewInput());
				parent.layout(true);
				parent.setRedraw(true);
			}
		});
	}

	public Control getDefaultFocusControl() {
		if (filteredTree != null)
			return filteredTree.getFilterControl();
		return null;
	}

	protected GridData getViewerGridData() {
		GridData data = super.getViewerGridData();
		data.heightHint = convertHeightInCharsToPixels(ILayoutConstants.DEFAULT_TABLE_HEIGHT);
		return data;
	}

	/**
	 * Set the checked elements to the specified selections.  This method
	 * does not force visibility/expansion of the checked elements.  If they are not
	 * visible, they will not be checked.
	 * @param selections
	 */
	public void setChecked(Object[] selections) {
		filteredTree.getCheckboxTreeViewer().setCheckedElements(selections);
		// TODO HACK ALERT!
		// Since We don't have API for setAllChecked(boolean), clients have to use this method.
		// We need to signal DelayedFilterCheckboxTree when everything needs to be deselected since
		// we aren't firing an event for each item.
		Object element = selections.length == 0 ? DelayedFilterCheckboxTree.ALL_ITEMS_HACK : selections[0];
		filteredTree.getCheckboxTreeViewer().fireCheckStateChanged(element, selections.length > 0);
	}

	public void setRepositoryFilter(int filterFlag, URI repoLocation) {
		// If there has been no change, don't do anything.  We will be
		// clearing out selection caches in this method and should not do
		// so if there's really no change.
		if (filterConstant == filterFlag) {
			if (filterConstant != AVAILABLE_SPECIFIED)
				return;
			if (repoLocation != null && repoLocation.equals(repositoryFilter))
				return;
		}
		filterConstant = filterFlag;

		switch (filterFlag) {
			case AVAILABLE_ALL :
			case AVAILABLE_NONE :
				repositoryFilter = null;
				repoFlags &= ~IRepositoryManager.REPOSITORIES_LOCAL;
				break;
			case AVAILABLE_LOCAL :
				repositoryFilter = null;
				repoFlags |= IRepositoryManager.REPOSITORIES_LOCAL;
				break;
			default :
				repositoryFilter = repoLocation;
				break;
		}
		updateAvailableViewState();
		filteredTree.clearCheckStateCache();
	}

	private IPreFilterJobProvider getPreFilterJobProvider() {
		return new IPreFilterJobProvider() {

			public Job getPreFilterJob() {
				switch (filterConstant) {
					case AVAILABLE_ALL :
						Job preFilterJob = new LoadMetadataRepositoryJob(getProvisioningUI());
						preFilterJob.setProperty(LoadMetadataRepositoryJob.SUPPRESS_REPOSITORY_EVENTS, Boolean.toString(true));
						return preFilterJob;
					case AVAILABLE_NONE :
					case AVAILABLE_LOCAL :
						return null;
					default :
						if (repositoryFilter == null)
							return null;
						Job job = new Job("Repository Load Job") { //$NON-NLS-1$
							@Override
							protected IStatus run(IProgressMonitor monitor) {
								try {
									getProvisioningUI().loadMetadataRepository(repositoryFilter, false, monitor);
									return Status.OK_STATUS;
								} catch (ProvisionException e) {
									return e.getStatus();
								}
							}

						};
						job.setPriority(Job.SHORT);
						return job;
				}
			}

		};
	}
}
