/*******************************************************************************
 * Copyright (c) 2010, 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.ui.discovery.repository;

import java.net.URI;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.discovery.AbstractCatalogSource;
import org.eclipse.equinox.internal.p2.discovery.AbstractDiscoveryStrategy;
import org.eclipse.equinox.internal.p2.discovery.model.CatalogCategory;
import org.eclipse.equinox.internal.p2.discovery.model.CatalogItem;
import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability;
import org.eclipse.equinox.internal.p2.metadata.TranslationSupport;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.IRequirement;
import org.eclipse.equinox.p2.operations.ProvisioningSession;
import org.eclipse.equinox.p2.operations.RepositoryTracker;
import org.eclipse.equinox.p2.query.*;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.equinox.p2.ui.ProvisioningUI;

/**
 * @author Steffen Pingel
 */
public class RepositoryDiscoveryStrategy extends AbstractDiscoveryStrategy {

	private static final String IU_PROPERTY_CATEGORY = "org.eclipse.equinox.p2.type.category"; //$NON-NLS-1$

	private static final String PLUGIN_ID = "org.eclipse.equinox.p2.discovery.repository"; //$NON-NLS-1$

	private final List<URI> locations;

	private final Map<IMetadataRepository, RepositorySource> sourceByRepository;

	private final Map<String, CatalogCategory> categoryById;

	private final Map<String, CatalogItem> catalogItemById;

	public RepositoryDiscoveryStrategy() {
		this.locations = new ArrayList<>();
		this.sourceByRepository = new HashMap<>();
		this.categoryById = new HashMap<>();
		this.catalogItemById = new HashMap<>();
	}

	public void addLocation(URI location) {
		locations.add(location);
	}

	public void removeLocation(URI location) {
		locations.remove(location);
	}

	@Override
	public void performDiscovery(IProgressMonitor progressMonitor) throws CoreException {
		// ignore
		SubMonitor monitor = SubMonitor.convert(progressMonitor);
		monitor.setWorkRemaining(100);
		try {
			List<IMetadataRepository> repositories = addRepositories(monitor.newChild(50));
			queryInstallableUnits(monitor.newChild(50), repositories);
			connectCategories();
		} catch (ProvisionException e) {
			throw new CoreException(new Status(IStatus.ERROR, PLUGIN_ID, "Failed to process repository contents", e)); //$NON-NLS-1$
		}
	}

	private void connectCategories() {
		for (CatalogCategory category : categories) {
			if (category.getData() instanceof IInstallableUnit) {
				IInstallableUnit categoryIU = (IInstallableUnit) category.getData();
				Collection<IRequirement> required = categoryIU.getRequirements();
				for (IRequirement requirement : required) {
					if (requirement instanceof IRequiredCapability) {
						IRequiredCapability capability = (IRequiredCapability) requirement;
						CatalogItem item = catalogItemById.get(capability.getName());
						if (item != null) {
							item.setCategoryId(category.getId());
						}
					}
				}
			}
		}
	}

	private List<IMetadataRepository> addRepositories(SubMonitor monitor) throws ProvisionException {
		ProvisioningSession session = ProvisioningUI.getDefaultUI().getSession();

		monitor.setWorkRemaining(locations.size());

		RepositoryTracker repositoryTracker = ProvisioningUI.getDefaultUI().getRepositoryTracker();
		for (URI location : locations) {
			repositoryTracker.addRepository(location, null, session);
			monitor.worked(1);
		}

		// fetch meta-data for these repositories
		ArrayList<IMetadataRepository> repositories = new ArrayList<>();
		IMetadataRepositoryManager manager = session.getProvisioningAgent()
				.getService(IMetadataRepositoryManager.class);
		for (URI uri : locations) {
			IMetadataRepository repository = manager.loadRepository(uri, monitor.newChild(1));
			repositories.add(repository);
		}
		return repositories;
	}

	private void checkCancelled(IProgressMonitor monitor) {
		if (monitor.isCanceled()) {
			throw new OperationCanceledException();
		}
	}

	private void queryInstallableUnits(SubMonitor monitor, List<IMetadataRepository> repositories) {
		monitor.setWorkRemaining(repositories.size());
		for (final IMetadataRepository repository : repositories) {
			checkCancelled(monitor);
			IQuery<IInstallableUnit> query = QueryUtil.createMatchQuery(//
					"id ~= /*.feature.group/ " + // //$NON-NLS-1$
							"? providedCapabilities.exists(p | p.namespace == 'org.eclipse.equinox.p2.iu' && p.name ~= /*.feature.group/) " //$NON-NLS-1$
							+ //
							": properties['org.eclipse.equinox.p2.type.category'] == true"); //$NON-NLS-1$
			IQueryResult<IInstallableUnit> result = repository.query(query, monitor.newChild(1));
			for (IInstallableUnit iInstallableUnit : result) {
				process(repository, iInstallableUnit);
			}
		}
	}

	protected void process(IMetadataRepository repository, IInstallableUnit candidate) {
		if (isCategory(candidate)) {
			processCategory(repository, candidate);
		} else {
			processCatalogItem(repository, candidate);
		}
	}

	private CatalogItem processCatalogItem(IMetadataRepository repository, IInstallableUnit candidate) {
		CatalogItem item = catalogItemById.get(candidate.getId());
		if (item != null) {
			return item;
		}

		item = new CatalogItem();
		item.setId(candidate.getId());
		item.setDescription(getProperty(candidate, IInstallableUnit.PROP_DESCRIPTION));
		item.setName(getProperty(candidate, IInstallableUnit.PROP_NAME));
		item.setProvider(getProperty(candidate, IInstallableUnit.PROP_PROVIDER));
		item.setSource(getSource(repository));
		item.setData(candidate);
		item.setSiteUrl(repository.getLocation().toString());
		item.getInstallableUnits().add(item.getId());

		catalogItemById.put(item.getId(), item);
		items.add(item);
		return item;
	}

	public String getProperty(IInstallableUnit candidate, String key) {
		String value = TranslationSupport.getInstance().getIUProperty(candidate, key);
		return (value != null) ? value : "";
	}

	private AbstractCatalogSource getSource(IMetadataRepository repository) {
		RepositorySource source = sourceByRepository.get(repository);
		if (source == null) {
			source = new RepositorySource(repository);
			sourceByRepository.put(repository, source);
		}
		return source;
	}

	private CatalogCategory processCategory(IMetadataRepository repository, IInstallableUnit candidate) {
		CatalogCategory category = categoryById.get(candidate.getId());
		if (category != null) {
			return category;
		}

		category = new CatalogCategory();
		category.setId(candidate.getId());
		category.setDescription(getProperty(candidate, IInstallableUnit.PROP_DESCRIPTION));
		category.setName(getProperty(candidate, IInstallableUnit.PROP_NAME));
		category.setSource(getSource(repository));
		category.setData(candidate);

		categoryById.put(category.getId(), category);
		categories.add(category);
		return category;
	}

	private Boolean isCategory(IInstallableUnit candidate) {
		return Boolean.valueOf(candidate.getProperty(IU_PROPERTY_CATEGORY));
	}

}
