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

import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.discovery.model.*;
import org.eclipse.osgi.service.resolver.VersionRange;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;

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

	private List<CatalogItem> items = Collections.emptyList();

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

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

	private List<CatalogItem> filteredItems = Collections.emptyList();

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

	private List<Tag> tags = Collections.emptyList();

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

	private boolean verifyUpdateSiteAvailability = false;

	private Map<String, Version> featureToVersion = null;

	public Catalog() {
		// constructor
	}

	/**
	 * 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.
	 */
	public IStatus performDiscovery(IProgressMonitor monitor) {
		MultiStatus status = new MultiStatus(DiscoveryCore.ID_PLUGIN, 0, Messages.Catalog_Failed_to_discovery_all_Error, null);
		if (discoveryStrategies.isEmpty()) {
			throw new IllegalStateException();
		}
		List<CatalogItem> newItems = new ArrayList<>();
		List<CatalogCategory> newCategories = new ArrayList<>();
		List<Certification> newCertifications = new ArrayList<>();
		List<Tag> newTags = new ArrayList<>();

		final int totalTicks = 100000;
		final int discoveryTicks = totalTicks - (totalTicks / 10);
		monitor.beginTask(Messages.Catalog_task_discovering_connectors, totalTicks);
		try {
			for (AbstractDiscoveryStrategy discoveryStrategy : discoveryStrategies) {
				if (monitor.isCanceled()) {
					status.add(Status.CANCEL_STATUS);
					break;
				}
				discoveryStrategy.setCategories(newCategories);
				discoveryStrategy.setItems(newItems);
				discoveryStrategy.setCertifications(newCertifications);
				discoveryStrategy.setTags(newTags);
				try {
					discoveryStrategy.performDiscovery(SubMonitor.convert(monitor, discoveryTicks / discoveryStrategies.size()));
				} catch (CoreException e) {
					status.add(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(Messages.Catalog_Strategy_failed_Error, discoveryStrategy.getClass().getSimpleName()), e));
				}
			}

			update(newCategories, newItems, newCertifications, newTags);
		} finally {
			monitor.done();
		}
		return status;
	}

	protected void update(List<CatalogCategory> newCategories, List<CatalogItem> newItems, List<Certification> newCertifications, List<Tag> newTags) {
		this.categories = newCategories;
		this.items = newItems;
		this.certifications = newCertifications;
		this.tags = newTags;
		this.filteredItems = new ArrayList<>();

		filterDescriptors();
		connectCategoriesToDescriptors();
		connectCertificationsToDescriptors();
	}

	/**
	 * get the top-level categories
	 * 
	 * @return the categories, or an empty list if there are none.
	 */
	public List<CatalogCategory> 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<CatalogItem> getItems() {
		return items;
	}

	public List<Tag> getTags() {
		return tags;
	}

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

	/**
	 * get a list of known certifications
	 * 
	 * @return the certifications, or an empty list if there are none.
	 */
	public List<Certification> getCertifications() {
		return certifications;
	}

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

	/**
	 * The environment used to resolve {@link CatalogItem#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 CatalogItem#getAvailable()
	 * @see #setVerifyUpdateSiteAvailability(boolean)
	 */
	public boolean isVerifyUpdateSiteAvailability() {
		return verifyUpdateSiteAvailability;
	}

	/**
	 * indicate if update site availability should be verified. The default is false.
	 * 
	 * @see CatalogItem#getAvailable()
	 * @see #isVerifyUpdateSiteAvailability()
	 */
	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 CatalogItem#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 CatalogItem#getFeatureFilter() feature filters}.
	 */
	public void setFeatureToVersion(Map<String, Version> featureToVersion) {
		this.featureToVersion = featureToVersion;
	}

	private void connectCertificationsToDescriptors() {
		Map<String, Certification> idToCertification = new HashMap<>();
		for (Certification certification : certifications) {
			Certification previous = idToCertification.put(certification.getId(), certification);
			if (previous != null) {
				LogHelper.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 (CatalogItem connector : items) {
			if (connector.getCertificationId() != null) {
				Certification certification = idToCertification.get(connector.getCertificationId());
				if (certification != null) {
					connector.setCertification(certification);
				} else {
					LogHelper.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, CatalogCategory> idToCategory = new HashMap<>();
		for (CatalogCategory category : categories) {
			CatalogCategory previous = idToCategory.put(category.getId(), category);
			if (previous != null) {
				LogHelper.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(Messages.Catalog_duplicate_category_id, new Object[] {category.getId(), category.getSource().getId(), previous.getSource().getId()})));
			}
		}

		for (CatalogItem connector : items) {
			CatalogCategory category = idToCategory.get(connector.getCategoryId());
			if (category != null) {
				category.getItems().add(connector);
				connector.setCategory(category);
			} else {
				LogHelper.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(Messages.Catalog_bundle_references_unknown_category, new Object[] {connector.getCategoryId(), connector.getId(), connector.getSource().getId()})));
			}
		}
	}

	/**
	 * eliminate any connectors whose {@link CatalogItem#getPlatformFilter() platform filters} don't match
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	private void filterDescriptors() {
		for (CatalogItem connector : new ArrayList<>(items)) {
			if (connector.getPlatformFilter() != null && connector.getPlatformFilter().trim().length() > 0) {
				boolean match = false;
				try {
					Filter filter = FrameworkUtil.createFilter(connector.getPlatformFilter());
					// TODO Doing raw conversion here for simplicity; could convert to Dictionary<String, ?>
					// but the filter impl must still handle cases where non String keys are used.
					match = filter.match((Dictionary) environment);
				} catch (InvalidSyntaxException e) {
					LogHelper.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(Messages.Catalog_illegal_filter_syntax, new Object[] {connector.getPlatformFilter(), connector.getId(), connector.getSource().getId()})));
				}
				if (!match) {
					items.remove(connector);
					filteredItems.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) {
					items.remove(connector);
					filteredItems.add(connector);
					break;
				}
			}
		}
	}

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

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

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

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

	public void setTags(List<Tag> tags) {
		this.tags = new ArrayList<>(tags);
	}

}
