/*******************************************************************************
 * Copyright (c) 2009, 2010 Tasktop Technologies 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:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/
package org.eclipse.mylyn.internal.discovery.core.model;

import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IBundleGroup;
import org.eclipse.core.runtime.IBundleGroupProvider;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.internal.discovery.core.DiscoveryCore;
import org.eclipse.mylyn.internal.discovery.core.util.WebUtil;
import org.eclipse.osgi.service.resolver.VersionRange;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Bundle;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.Version;

/**
 * A means of discovering connectors.
 * 
 * @author David Green
 */
public class ConnectorDiscovery {

	private List<DiscoveryConnector> connectors = Collections.emptyList();

	private List<DiscoveryCategory> categories = Collections.emptyList();

	private List<DiscoveryCertification> certifications = Collections.emptyList();

	private List<DiscoveryConnector> filteredConnectors = Collections.emptyList();

	private final List<AbstractDiscoveryStrategy> discoveryStrategies = new ArrayList<AbstractDiscoveryStrategy>();

	private Dictionary<Object, Object> environment = System.getProperties();

	private boolean verifyUpdateSiteAvailability = false;

	private Map<String, Version> featureToVersion = null;

	public ConnectorDiscovery() {
	}

	/**
	 * get the discovery strategies to use.
	 */
	public List<AbstractDiscoveryStrategy> getDiscoveryStrategies() {
		return discoveryStrategies;
	}

	/**
	 * Initialize this by performing discovery. Discovery may take a long time as it involves network access.
	 * PRECONDITION: must add at least one {@link #getDiscoveryStrategies() discovery strategy} prior to calling.
	 * 
	 * @return
	 */
	public IStatus performDiscovery(IProgressMonitor monitor) {
		MultiStatus status = new MultiStatus(DiscoveryCore.ID_PLUGIN, 0,
				Messages.ConnectorDiscovery_Failed_to_discovery_all_Error, null);
		if (discoveryStrategies.isEmpty()) {
			throw new IllegalStateException();
		}
		connectors = new ArrayList<DiscoveryConnector>();
		filteredConnectors = new ArrayList<DiscoveryConnector>();
		categories = new ArrayList<DiscoveryCategory>();
		certifications = new ArrayList<DiscoveryCertification>();

		final int totalTicks = 100000;
		final int discoveryTicks = totalTicks - (totalTicks / 10);
		final int filterTicks = totalTicks - discoveryTicks;
		monitor.beginTask(Messages.ConnectorDiscovery_task_discovering_connectors, totalTicks);
		try {
			for (AbstractDiscoveryStrategy discoveryStrategy : discoveryStrategies) {
				discoveryStrategy.setCategories(categories);
				discoveryStrategy.setConnectors(connectors);
				discoveryStrategy.setCertifications(certifications);
				try {
					discoveryStrategy.performDiscovery(new SubProgressMonitor(monitor, discoveryTicks
							/ discoveryStrategies.size()));
				} catch (CoreException e) {
					status.add(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
							Messages.ConnectorDiscovery_Strategy_failed_Error, discoveryStrategy.getClass()
									.getSimpleName()), e));
				}
			}

			filterDescriptors();
			if (verifyUpdateSiteAvailability) {
				verifySiteAvailability(new SubProgressMonitor(monitor, filterTicks));
			}
			connectCategoriesToDescriptors();
			connectCertificationsToDescriptors();
		} finally {
			monitor.done();
		}
		return status;
	}

	/**
	 * get the top-level categories
	 * 
	 * @return the categories, or an empty list if there are none.
	 */
	public List<DiscoveryCategory> getCategories() {
		return categories;
	}

	/**
	 * get the connectors that were discovered and not filtered
	 * 
	 * @return the connectors, or an empty list if there are none.
	 */
	public List<DiscoveryConnector> getConnectors() {
		return connectors;
	}

	/**
	 * get the connectors that were discovered but filtered
	 * 
	 * @return the filtered connectors, or an empty list if there were none.
	 */
	public List<DiscoveryConnector> getFilteredConnectors() {
		return filteredConnectors;
	}

	/**
	 * The environment used to resolve {@link ConnectorDescriptor#getPlatformFilter() platform filters}. Defaults to the
	 * current environment.
	 */
	public Dictionary<Object, Object> getEnvironment() {
		return environment;
	}

	/**
	 * The environment used to resolve {@link ConnectorDescriptor#getPlatformFilter() platform filters}. Defaults to the
	 * current environment.
	 */
	public void setEnvironment(Dictionary<Object, Object> environment) {
		if (environment == null) {
			throw new IllegalArgumentException();
		}
		this.environment = environment;
	}

	/**
	 * indicate if update site availability should be verified. The default is false.
	 * 
	 * @see DiscoveryConnector#getAvailable()
	 * @see #verifySiteAvailability(IProgressMonitor)
	 */
	public boolean isVerifyUpdateSiteAvailability() {
		return verifyUpdateSiteAvailability;
	}

	/**
	 * indicate if update site availability should be verified. The default is false.
	 * 
	 * @see DiscoveryConnector#getAvailable()
	 * @see #verifySiteAvailability(IProgressMonitor)
	 */
	public void setVerifyUpdateSiteAvailability(boolean verifyUpdateSiteAvailability) {
		this.verifyUpdateSiteAvailability = verifyUpdateSiteAvailability;
	}

	/**
	 * <em>not for general use: public for testing purposes only</em> A map of installed features to their version. Used
	 * to resolve {@link ConnectorDescriptor#getFeatureFilter() feature filters}.
	 */
	public Map<String, Version> getFeatureToVersion() {
		return featureToVersion;
	}

	/**
	 * <em>not for general use: public for testing purposes only</em> A map of installed features to their version. Used
	 * to resolve {@link ConnectorDescriptor#getFeatureFilter() feature filters}.
	 */
	public void setFeatureToVersion(Map<String, Version> featureToVersion) {
		this.featureToVersion = featureToVersion;
	}

	private void connectCertificationsToDescriptors() {
		Map<String, DiscoveryCertification> idToCertification = new HashMap<String, DiscoveryCertification>();
		for (DiscoveryCertification certification : certifications) {
			DiscoveryCertification previous = idToCertification.put(certification.getId(), certification);
			if (previous != null) {
				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
						"Duplicate certification id ''{0}'': declaring sources: {1}, {2}", //$NON-NLS-1$
						new Object[] { certification.getId(), certification.getSource().getId(),
								previous.getSource().getId() })));
			}
		}

		for (DiscoveryConnector connector : connectors) {
			if (connector.getCertificationId() != null) {
				DiscoveryCertification certification = idToCertification.get(connector.getCertificationId());
				if (certification != null) {
					connector.setCertification(certification);
				} else {
					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
							"Unknown category ''{0}'' referenced by connector ''{1}'' declared in {2}", new Object[] { //$NON-NLS-1$
							connector.getCertificationId(), connector.getId(), connector.getSource().getId() })));
				}
			}
		}
	}

	private void connectCategoriesToDescriptors() {
		Map<String, DiscoveryCategory> idToCategory = new HashMap<String, DiscoveryCategory>();
		for (DiscoveryCategory category : categories) {
			DiscoveryCategory previous = idToCategory.put(category.getId(), category);
			if (previous != null) {
				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
						Messages.ConnectorDiscovery_duplicate_category_id, new Object[] { category.getId(),
								category.getSource().getId(), previous.getSource().getId() })));
			}
		}

		for (DiscoveryConnector connector : connectors) {
			DiscoveryCategory category = idToCategory.get(connector.getCategoryId());
			if (category != null) {
				category.getConnectors().add(connector);
				connector.setCategory(category);
			} else {
				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
						Messages.ConnectorDiscovery_bundle_references_unknown_category,
						new Object[] { connector.getCategoryId(), connector.getId(), connector.getSource().getId() })));
			}
		}
	}

	/**
	 * eliminate any connectors whose {@link ConnectorDescriptor#getPlatformFilter() platform filters} don't match
	 */
	@SuppressWarnings({ "rawtypes" })
	private void filterDescriptors() {
		for (DiscoveryConnector connector : new ArrayList<DiscoveryConnector>(connectors)) {
			if (connector.getPlatformFilter() != null && connector.getPlatformFilter().trim().length() > 0) {
				boolean match = false;
				try {
					Filter filter = FrameworkUtil.createFilter(connector.getPlatformFilter());
					match = filter.match((Dictionary) environment);
				} catch (InvalidSyntaxException e) {
					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
							Messages.ConnectorDiscovery_illegal_filter_syntax,
							new Object[] { connector.getPlatformFilter(), connector.getId(),
									connector.getSource().getId() })));
				}
				if (!match) {
					connectors.remove(connector);
					filteredConnectors.add(connector);
				}
			}
			for (FeatureFilter featureFilter : connector.getFeatureFilter()) {
				if (featureToVersion == null) {
					featureToVersion = computeFeatureToVersion();
				}
				boolean match = false;
				Version version = featureToVersion.get(featureFilter.getFeatureId());
				if (version != null) {
					VersionRange versionRange = new VersionRange(featureFilter.getVersion());
					if (versionRange.isIncluded(version)) {
						match = true;
					}
				}
				if (!match) {
					connectors.remove(connector);
					filteredConnectors.add(connector);
					break;
				}
			}
		}
	}

	private Map<String, Version> computeFeatureToVersion() {
		Map<String, Version> featureToVersion = new HashMap<String, Version>();
		for (IBundleGroupProvider provider : Platform.getBundleGroupProviders()) {
			for (IBundleGroup bundleGroup : provider.getBundleGroups()) {
				for (Bundle bundle : bundleGroup.getBundles()) {
					featureToVersion.put(bundle.getSymbolicName(), bundle.getVersion());
				}
			}
		}
		return featureToVersion;
	}

	/**
	 * Determine update site availability. This may be performed automatically as part of discovery when
	 * {@link #isVerifyUpdateSiteAvailability()} is true, or it may be invoked later by calling this method.
	 */
	public void verifySiteAvailability(IProgressMonitor monitor) {
		// NOTE: we don't put java.net.URLs in the map since it involves DNS activity when
		//       computing the hash code.
		Map<String, Collection<DiscoveryConnector>> urlToDescriptors = new HashMap<String, Collection<DiscoveryConnector>>();

		for (DiscoveryConnector descriptor : connectors) {
			if (!descriptor.isInstallable()) {
				continue;
			}
			String url = descriptor.getSiteUrl();
			if (!url.endsWith("/")) { //$NON-NLS-1$
				url += "/"; //$NON-NLS-1$
			}
			Collection<DiscoveryConnector> collection = urlToDescriptors.get(url);
			if (collection == null) {
				collection = new ArrayList<DiscoveryConnector>();
				urlToDescriptors.put(url, collection);
			}
			collection.add(descriptor);
		}
		final int totalTicks = urlToDescriptors.size();
		monitor.beginTask(Messages.ConnectorDiscovery_task_verifyingAvailability, totalTicks);
		try {
			if (!urlToDescriptors.isEmpty()) {
				ExecutorService executorService = Executors.newFixedThreadPool(Math.min(urlToDescriptors.size(), 4));
				try {
					List<Future<VerifyUpdateSiteJob>> futures = new ArrayList<Future<VerifyUpdateSiteJob>>(
							urlToDescriptors.size());
					for (String url : urlToDescriptors.keySet()) {
						futures.add(executorService.submit(new VerifyUpdateSiteJob(url)));
					}
					for (Future<VerifyUpdateSiteJob> jobFuture : futures) {
						try {
							for (;;) {
								try {
									VerifyUpdateSiteJob job = jobFuture.get(1L, TimeUnit.SECONDS);

									Collection<DiscoveryConnector> descriptors = urlToDescriptors.get(job.url);
									for (DiscoveryConnector descriptor : descriptors) {
										descriptor.setAvailable(job.ok);
									}
									break;
								} catch (TimeoutException e) {
									if (monitor.isCanceled()) {
										return;
									}
								}
							}
						} catch (InterruptedException e) {
							monitor.setCanceled(true);
							return;
						} catch (ExecutionException e) {
							if (e.getCause() instanceof OperationCanceledException) {
								monitor.setCanceled(true);
								return;
							}
							IStatus status;
							if (e.getCause() instanceof CoreException) {
								status = ((CoreException) e.getCause()).getStatus();
							} else {
								status = new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
										Messages.ConnectorDiscovery_unexpected_exception, e.getCause());
							}
							StatusHandler.log(status);
						}
						monitor.worked(1);
					}
				} finally {
					executorService.shutdownNow();
				}
			}
		} finally {
			monitor.done();
		}
	}

	private static class VerifyUpdateSiteJob implements Callable<VerifyUpdateSiteJob> {

		private final String url;

		private boolean ok = false;

		public VerifyUpdateSiteJob(String url) {
			this.url = url;
		}

		public VerifyUpdateSiteJob call() throws Exception {
			URL baseUrl = new URL(url);
			List<URI> locations = new ArrayList<URI>();
			for (String location : new String[] {
					"content.jar", "content.xml", "compositeContent.jar", "compositeContent.xml", "site.xml" }) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
				locations.add(new URL(baseUrl, location).toURI());
			}
			ok = WebUtil.verifyAvailability(locations, true, new NullProgressMonitor());
			return this;
		}

	}

	public void dispose() {
		for (final AbstractDiscoveryStrategy strategy : discoveryStrategies) {
			SafeRunner.run(new ISafeRunnable() {

				public void run() throws Exception {
					strategy.dispose();
				}

				public void handleException(Throwable exception) {
					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
							Messages.ConnectorDiscovery_exception_disposing + strategy.getClass().getName(), exception));
				}
			});
		}
	}
}
