/*******************************************************************************
 * Copyright (c) 2004, 2009 David Green 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:
 *     David Green - initial API and implementation
 *     Tasktop Technologies - improvements
 *     Helen Bershadskaya - improvements for bug 242445
 *******************************************************************************/

package org.eclipse.mylyn.tasks.ui.wizards;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
import org.eclipse.mylyn.internal.tasks.ui.wizards.Messages;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.FormToolkit;

/**
 * An abstract base class for repository settings page that supports the <code>taskRepositoryPageContribution</code>
 * extension point. {@link ITaskRepositoryPage} implementations are encouraged to extend
 * {@link AbstractRepositorySettingsPage} if possible as it provides a standard UI for managing server settings.
 * 
 * @see AbstractRepositorySettingsPage
 * @author David Green
 * @author Steffen Pingel
 * @since 3.1
 */
public abstract class AbstractTaskRepositoryPage extends WizardPage implements ITaskRepositoryPage {

	private static final String CLASS = "class"; //$NON-NLS-1$

	private static final String ID = "id"; //$NON-NLS-1$

	private static final String KIND = "connectorKind"; //$NON-NLS-1$

	private static final String TASK_REPOSITORY_PAGE_CONTRIBUTION = "taskRepositoryPageContribution"; //$NON-NLS-1$

	private static final String TASK_REPOSITORY_PAGE_CONTRIBUTION_EXTENSION = "org.eclipse.mylyn.tasks.ui.taskRepositoryPageContribution"; //$NON-NLS-1$

	private static final Comparator<AbstractTaskRepositoryPageContribution> CONTRIBUTION_COMPARATOR = new ContributionComparator();

	private final TaskRepository repository;

	private final List<AbstractTaskRepositoryPageContribution> contributions;

	FormToolkit toolkit;

	private final AbstractTaskRepositoryPageContribution.Listener contributionListener = new AbstractTaskRepositoryPageContribution.Listener() {
		public void validationRequired(AbstractTaskRepositoryPageContribution contribution) {
			validatePageSettings();
		}
	};

	/**
	 * @since 3.1
	 */
	public AbstractTaskRepositoryPage(String title, String description, TaskRepository repository) {
		super(title);
		this.repository = repository;
		this.contributions = new ArrayList<AbstractTaskRepositoryPageContribution>();
		setTitle(title);
		setDescription(description);
	}

	/**
	 * Get the kind of connector supported by this page.
	 * 
	 * @return the kind of connector, never null
	 * @since 3.1
	 */
	public abstract String getConnectorKind();

	@Override
	public void dispose() {
		if (toolkit != null) {
			toolkit.dispose();
			toolkit = null;
		}
		super.dispose();
	}

	/**
	 * Creates the contents of the page. Subclasses may override this method to change where the contributions are
	 * added.
	 * 
	 * @since 2.0
	 */
	public void createControl(Composite parent) {
		initializeDialogUnits(parent);
		toolkit = new FormToolkit(TasksUiPlugin.getDefault().getFormColors(parent.getDisplay()));

		Composite compositeContainer = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout(3, false);
		compositeContainer.setLayout(layout);

//		Composite compositeContainer = new Composite(parent, SWT.NULL);
//		Layout layout = new FillLayout();
//		compositeContainer.setLayout(layout);

		createSettingControls(compositeContainer);
		createContributionControls(compositeContainer);

		Dialog.applyDialogFont(compositeContainer);
		setControl(compositeContainer);
		//getControl().getShell().pack();
	}

	/**
	 * Creates the controls of this page.
	 * 
	 * @since 3.1
	 */
	protected abstract void createSettingControls(Composite parent);

	@Override
	public boolean isPageComplete() {
		return super.isPageComplete() && conributionsIsPageComplete();
	}

	@Override
	public boolean canFlipToNextPage() {
		return super.canFlipToNextPage() && contributionsCanFlipToNextPage();
	}

	private boolean contributionsCanFlipToNextPage() {
		for (AbstractTaskRepositoryPageContribution contribution : contributions) {
			if (!contribution.canFlipToNextPage()) {
				return false;
			}
		}
		return true;
	}

	private boolean conributionsIsPageComplete() {
		for (AbstractTaskRepositoryPageContribution contribution : contributions) {
			if (!contribution.isPageComplete()) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Subclasses should only call this method if they override {@link #createContents(Composite)}.
	 * 
	 * @param parentControl
	 *            the container into which the contributions will create their UI
	 * @since 3.1
	 */
	protected void createContributionControls(final Composite parentControl) {
		contributions.clear();
		contributions.addAll(findApplicableContributors());

		if (!contributions.isEmpty()) {
			final List<AbstractTaskRepositoryPageContribution> badContributions = new ArrayList<AbstractTaskRepositoryPageContribution>();
			for (final AbstractTaskRepositoryPageContribution contribution : contributions) {
				SafeRunnable.run(new SafeRunnable() {
					public void run() throws Exception {
						contribution.init(getConnectorKind(), repository);
						contribution.addListener(contributionListener);
					}

					@Override
					public void handleException(Throwable e) {
						badContributions.add(contribution);
						StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, NLS.bind(
								"Problems occured when initializing contribution \"{0}\"", contribution.getId()), e)); //$NON-NLS-1$
					}
				});
			}
			contributions.removeAll(badContributions);

			Collections.sort(contributions, CONTRIBUTION_COMPARATOR);

			for (final AbstractTaskRepositoryPageContribution contribution : contributions) {
				final ExpandableComposite section = createSection(parentControl, contribution.getTitle());
				section.setToolTipText(contribution.getDescription());

				SafeRunnable.run(new SafeRunnable() {
					public void run() throws Exception {
						Control control = contribution.createControl(section);
						section.setClient(control);
					}

					@Override
					public void handleException(Throwable e) {
						section.dispose();
						StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, NLS.bind(
								"Problems occured when creating control for contribution \"{0}\"", //$NON-NLS-1$
								contribution.getId()), e));
					}
				});
			}
		}
	}

	/**
	 * @since 3.1
	 */
	protected ExpandableComposite createSection(final Composite parentControl, String title) {
		final ExpandableComposite section = toolkit.createExpandableComposite(parentControl,
				ExpandableComposite.TWISTIE | ExpandableComposite.CLIENT_INDENT | ExpandableComposite.COMPACT);
		section.clientVerticalSpacing = 0;
		section.setBackground(parentControl.getBackground());
		section.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT));
		section.addExpansionListener(new ExpansionAdapter() {
			@Override
			public void expansionStateChanged(ExpansionEvent e) {
				parentControl.layout(true);
				getControl().getShell().pack();
			}
		});
		section.setText(title);
		GridDataFactory.fillDefaults().indent(0, 5).grab(true, false).span(3, SWT.DEFAULT).applyTo(section);
		return section;
	}

	/**
	 * Validate the settings of this page, not including contributions. This method should not be called directly by
	 * page implementations. Always run on a UI thread.
	 * 
	 * @return the status, or null if there are no messages.
	 * @see #validatePageSettings()
	 * @since 3.1
	 */
	protected abstract IStatus validate();

	/**
	 * Overriding methods should call <code>super.applyTo(repository)</code>
	 * 
	 * @since 3.1
	 */
	public void applyTo(TaskRepository repository) {
		applyContributionSettingsTo(repository);
	}

	private void applyContributionSettingsTo(TaskRepository repository) {
		for (AbstractTaskRepositoryPageContribution contribution : contributions) {
			contribution.applyTo(repository);
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Invokes {@link #applyTo(TaskRepository)} by default. Client may override.
	 * 
	 * @since 3.6
	 */
	public void performFinish(TaskRepository repository) {
		applyTo(repository);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Invokes {@link #applyTo(TaskRepository)} by default. Client may override.
	 * 
	 * @since 3.7
	 * @return true to indicate the finish request was accepted, and false to indicate that the finish request was
	 *         refused
	 */
	public boolean preFinish(TaskRepository repository) {
		return true;
	}

	/**
	 * Returns a status if there is a message to display, otherwise null.
	 */
	private IStatus computeValidation() {
		final MultiStatus cumulativeResult = new MultiStatus(TasksUiPlugin.ID_PLUGIN, IStatus.OK,
				Messages.AbstractTaskRepositoryPage_Validation_failed, null);

		// validate the page
		IStatus result = validate();
		if (result != null) {
			cumulativeResult.add(result);
		}

		// validate contributions
		for (final AbstractTaskRepositoryPageContribution contribution : contributions) {
			SafeRunnable.run(new SafeRunnable() {
				public void run() throws Exception {
					IStatus result = contribution.validate();
					if (result != null) {
						cumulativeResult.add(result);
					}
				}

				@Override
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, NLS.bind(
							"Problems occured when validating contribution \"{0}\"", contribution.getId()), e)); //$NON-NLS-1$
				}
			});
		}

		return cumulativeResult;
	}

	/**
	 * Validate all settings in the page including contributions. This method should be called whenever a setting is
	 * changed on the page. The results of validation are applied and the buttons of the page are updated.
	 * 
	 * @see #validate(IProgressMonitor)
	 * @see #applyValidationResult(IStatus[])
	 */
	private void validatePageSettings() {
		IStatus validationStatus = computeValidation();
		applyValidationResult(validationStatus);
		getWizard().getContainer().updateButtons();
	}

	/**
	 * Apply the results of validation to the page. The implementation finds the most {@link IStatus#getSeverity()
	 * severe} status and {@link #setMessage(String, int) applies the message} to the page.
	 * 
	 * @param status
	 *            the status of the validation, or null
	 */
	private void applyValidationResult(IStatus status) {
		if (status == null || status.isOK()) {
			setMessage(null, IMessageProvider.INFORMATION);
			setErrorMessage(null);
		} else {
			// find the most severe status
			int messageType;
			switch (status.getSeverity()) {
			case IStatus.OK:
			case IStatus.INFO:
				messageType = IMessageProvider.INFORMATION;
				break;
			case IStatus.WARNING:
				messageType = IMessageProvider.WARNING;
				break;
			case IStatus.ERROR:
			default:
				messageType = IMessageProvider.ERROR;
				break;
			}
			setErrorMessage(null);
			setMessage(status.getMessage(), messageType);
		}
	}

	private List<AbstractTaskRepositoryPageContribution> findApplicableContributors() {
		List<AbstractTaskRepositoryPageContribution> contributors = new ArrayList<AbstractTaskRepositoryPageContribution>();

		IExtensionRegistry registry = Platform.getExtensionRegistry();

		IExtensionPoint editorExtensionPoint = registry.getExtensionPoint(TASK_REPOSITORY_PAGE_CONTRIBUTION_EXTENSION);
		IExtension[] editorExtensions = editorExtensionPoint.getExtensions();
		for (IExtension extension : editorExtensions) {
			IConfigurationElement[] elements = extension.getConfigurationElements();
			for (IConfigurationElement element : elements) {
				if (element.getName().equals(TASK_REPOSITORY_PAGE_CONTRIBUTION)) {
					String kind = element.getAttribute(KIND);
					if (kind == null || kind.length() == 0 || kind.equals(getConnectorKind())) {
						String id = element.getAttribute(ID);
						try {
							if (id == null || id.length() == 0) {
								throw new IllegalStateException(TASK_REPOSITORY_PAGE_CONTRIBUTION + "/@" + ID //$NON-NLS-1$
										+ " is required"); //$NON-NLS-1$
							}
							Object contributor = element.createExecutableExtension(CLASS);
							AbstractTaskRepositoryPageContribution pageContributor = (AbstractTaskRepositoryPageContribution) contributor;
							pageContributor.setId(id);
							if (pageContributor.isEnabled()) {
								contributors.add(pageContributor);
							}
						} catch (Throwable e) {
							StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, "Could not load " //$NON-NLS-1$
									+ TASK_REPOSITORY_PAGE_CONTRIBUTION + " '" + id + "' from plug-in " //$NON-NLS-1$//$NON-NLS-2$
									+ element.getContributor().getName(), e));
						}
					}
				}
			}
		}

		return contributors;
	}

	private static class ContributionComparator implements Comparator<AbstractTaskRepositoryPageContribution> {

		public int compare(AbstractTaskRepositoryPageContribution o1, AbstractTaskRepositoryPageContribution o2) {
			if (o1 == o2) {
				return 0;
			}
			String s1 = o1.getTitle();
			String s2 = o2.getTitle();
			int i = s1.compareTo(s2);
			if (i == 0) {
				i = o1.getId().compareTo(o2.getId());
			}
			return i;
		}

	}
}
