/*******************************************************************************
 * 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
 *     David Dubrow - fix for bug 313412 
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.discovery.operations;

import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.discovery.model.CatalogItem;
import org.eclipse.equinox.internal.p2.ui.ProvUIMessages;
import org.eclipse.equinox.internal.p2.ui.discovery.DiscoveryUi;
import org.eclipse.equinox.internal.p2.ui.discovery.util.WorkbenchUtil;
import org.eclipse.equinox.internal.p2.ui.discovery.wizards.Messages;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.operations.*;
import org.eclipse.equinox.p2.query.IQuery;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.equinox.p2.ui.ProvisioningUI;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;

/**
 * A job that configures a p2 install action for installing one or more
 * {@link CatalogItem connectors}. The bulk of the installation work is done by
 * p2; this class just sets up the p2 repository meta-data and selects the
 * appropriate features to install. After running the job the install action
 * must be run to perform the installation.
 * 
 * @author David Green
 * @author Steffen Pingel
 */
public class DiscoveryInstallOperation implements IRunnableWithProgress {

	private final List<CatalogItem> installableConnectors;

	private final ProvisioningUI provisioningUI;

	private Set<URI> repositoryLocations;

	public DiscoveryInstallOperation(List<CatalogItem> installableConnectors) {
		if (installableConnectors == null || installableConnectors.isEmpty()) {
			throw new IllegalArgumentException();
		}
		this.installableConnectors = new ArrayList<>(installableConnectors);
		this.provisioningUI = ProvisioningUI.getDefaultUI();
	}

	@Override
	public void run(IProgressMonitor progressMonitor) throws InvocationTargetException, InterruptedException {
		try {
			SubMonitor monitor = SubMonitor.convert(progressMonitor, Messages.InstallConnectorsJob_task_configuring,
					150);
			try {
				final IInstallableUnit[] ius = computeInstallableUnits(monitor.newChild(50));

				checkCancelled(monitor);

				final InstallOperation installOperation = resolve(monitor.newChild(50), ius,
						repositoryLocations.toArray(new URI[0]));

				checkCancelled(monitor);

				if (installOperation.getResolutionResult().getSeverity() > IStatus.WARNING) {
					monitor.setTaskName(ProvUIMessages.ProvisioningOperationWizard_Remediation_Operation);
					final RemediationOperation remediationOperation = new RemediationOperation(
							provisioningUI.getSession(), installOperation.getProfileChangeRequest());
					remediationOperation.resolveModal(monitor.newChild(50));
					Display.getDefault().asyncExec(() -> provisioningUI.openInstallWizard(Arrays.asList(ius),
							installOperation, remediationOperation, null));
				} else {
					Display.getDefault().asyncExec(
							() -> provisioningUI.openInstallWizard(Arrays.asList(ius), installOperation, null));
				}
			} finally {
				monitor.done();
			}
		} catch (OperationCanceledException e) {
			throw new InterruptedException();
		} catch (Exception e) {
			throw new InvocationTargetException(e);
		}
	}

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

	private InstallOperation resolve(IProgressMonitor monitor, final IInstallableUnit[] ius, URI[] repositories) {
		final InstallOperation installOperation = provisioningUI.getInstallOperation(Arrays.asList(ius), repositories);
		installOperation.resolveModal(SubMonitor.convert(monitor, installableConnectors.size()));
		return installOperation;
	}

	public IInstallableUnit[] computeInstallableUnits(SubMonitor monitor) throws CoreException {
		try {
			monitor.setWorkRemaining(100);
			// add repository urls and load meta data
			List<IMetadataRepository> repositories = addRepositories(monitor.newChild(50));
			final List<IInstallableUnit> installableUnits = queryInstallableUnits(monitor.newChild(50), repositories);
			removeOldVersions(installableUnits);
			checkForUnavailable(installableUnits);
			return installableUnits.toArray(new IInstallableUnit[installableUnits.size()]);

			// MultiStatus status = new MultiStatus(DiscoveryUi.ID_PLUGIN, 0,
			// Messages.PrepareInstallProfileJob_ok, null);
			// ius = installableUnits.toArray(new
			// IInstallableUnit[installableUnits.size()]);
			// ProfileChangeRequest profileChangeRequest =
			// InstallAction.computeProfileChangeRequest(ius, profileId,
			// status, new SubProgressMonitor(monitor, installableConnectors.size()));
			// if (status.getSeverity() > IStatus.WARNING) {
			// throw new CoreException(status);
			// }
			// if (profileChangeRequest == null) {
			// // failed but no indication as to why
			// throw new CoreException(new Status(IStatus.ERROR, DiscoveryUi.ID_PLUGIN,
			// Messages.PrepareInstallProfileJob_computeProfileChangeRequestFailed, null));
			// }
			// PlannerResolutionOperation operation = new PlannerResolutionOperation(
			// Messages.PrepareInstallProfileJob_calculatingRequirements, profileId,
			// profileChangeRequest, null,
			// status, true);
			// IStatus operationStatus = operation.execute(new SubProgressMonitor(monitor,
			// installableConnectors.size()));
			// if (operationStatus.getSeverity() > IStatus.WARNING) {
			// throw new CoreException(operationStatus);
			// }
			//
			// plannerResolutionOperation = operation;

		} catch (URISyntaxException e) {
			// should never happen, since we already validated URLs.
			throw new CoreException(new Status(IStatus.ERROR, DiscoveryUi.ID_PLUGIN,
					Messages.InstallConnectorsJob_unexpectedError_url, e));
		} catch (MalformedURLException e) {
			// should never happen, since we already validated URLs.
			throw new CoreException(new Status(IStatus.ERROR, DiscoveryUi.ID_PLUGIN,
					Messages.InstallConnectorsJob_unexpectedError_url, e));
		} finally {
			monitor.done();
		}
	}

	/**
	 * Verifies that we found what we were looking for: it's possible that we have
	 * connector descriptors that are no longer available on their respective sites.
	 * In that case we must inform the user. Unfortunately this is the earliest
	 * point at which we can know.
	 */
	private void checkForUnavailable(final List<IInstallableUnit> installableUnits) throws CoreException {
		// at least one selected connector could not be found in a repository
		Set<String> foundIds = new HashSet<>();
		for (IInstallableUnit unit : installableUnits) {
			foundIds.add(unit.getId());
		}

		String message = ""; //$NON-NLS-1$
		String detailedMessage = ""; //$NON-NLS-1$
		for (CatalogItem descriptor : installableConnectors) {
			StringBuilder unavailableIds = null;
			for (String id : descriptor.getInstallableUnits()) {
				if (!foundIds.contains(id)) {
					if (unavailableIds == null) {
						unavailableIds = new StringBuilder();
					} else {
						unavailableIds.append(Messages.InstallConnectorsJob_commaSeparator);
					}
					unavailableIds.append(id);
				}
			}
			if (unavailableIds != null) {
				if (message.length() > 0) {
					message += Messages.InstallConnectorsJob_commaSeparator;
				}
				message += descriptor.getName();

				if (detailedMessage.length() > 0) {
					detailedMessage += Messages.InstallConnectorsJob_commaSeparator;
				}
				detailedMessage += NLS.bind(Messages.PrepareInstallProfileJob_notFoundDescriptorDetail,
						new Object[] { descriptor.getName(), unavailableIds.toString(), descriptor.getSiteUrl() });
			}
		}

		if (message.length() > 0) {
			// instead of aborting here we ask the user if they wish to proceed anyways
			final boolean[] okayToProceed = new boolean[1];
			final String finalMessage = message;
			Display.getDefault().syncExec(() -> okayToProceed[0] = MessageDialog.openQuestion(WorkbenchUtil.getShell(),
					Messages.InstallConnectorsJob_questionProceed,
					NLS.bind(Messages.InstallConnectorsJob_questionProceed_long, new Object[] { finalMessage })));
			if (!okayToProceed[0]) {
				throw new CoreException(new Status(IStatus.ERROR, DiscoveryUi.ID_PLUGIN,
						NLS.bind(Messages.InstallConnectorsJob_connectorsNotAvailable, detailedMessage), null));
			}
		}
	}

	/**
	 * Filters those installable units that have a duplicate in the list with a
	 * higher version number. it's possible that some repositories will host
	 * multiple versions of a particular feature. we assume that the user wants the
	 * highest version.
	 */
	private void removeOldVersions(final List<IInstallableUnit> installableUnits) {
		Map<String, Version> symbolicNameToVersion = new HashMap<>();
		for (IInstallableUnit unit : installableUnits) {
			Version version = symbolicNameToVersion.get(unit.getId());
			if (version == null || version.compareTo(unit.getVersion()) < 0) {
				symbolicNameToVersion.put(unit.getId(), unit.getVersion());
			}
		}
		if (symbolicNameToVersion.size() != installableUnits.size()) {
			for (IInstallableUnit unit : new ArrayList<>(installableUnits)) {
				Version version = symbolicNameToVersion.get(unit.getId());
				if (!version.equals(unit.getVersion())) {
					installableUnits.remove(unit);
				}
			}
		}
	}

	/**
	 * Perform a query to get the installable units. This causes p2 to determine
	 * what features are available in each repository. We select installable units
	 * by matching both the feature id and the repository; it is possible though
	 * unlikely that the same feature id is available from more than one of the
	 * selected repositories, and we must ensure that the user gets the one that
	 * they asked for.
	 */
	private List<IInstallableUnit> queryInstallableUnits(SubMonitor monitor, List<IMetadataRepository> repositories)
			throws URISyntaxException {
		final List<IInstallableUnit> installableUnits = new ArrayList<>();

		monitor.setWorkRemaining(repositories.size());
		for (final IMetadataRepository repository : repositories) {
			checkCancelled(monitor);
			final Set<String> installableUnitIdsThisRepository = getDescriptorIds(repository);
			for (IInstallableUnit iu : repository.query(createInstalledIUsQuery(), monitor.newChild(1))) {
				String id = iu.getId();
				if (installableUnitIdsThisRepository.contains(id))
					installableUnits.add(iu);
			}
		}
		return installableUnits;
	}

	protected IQuery<IInstallableUnit> createInstalledIUsQuery() {
		return QueryUtil.createIUGroupQuery();
	}

	private List<IMetadataRepository> addRepositories(SubMonitor monitor)
			throws MalformedURLException, URISyntaxException, ProvisionException {
		// tell p2 that it's okay to use these repositories
		ProvisioningSession session = ProvisioningUI.getDefaultUI().getSession();
		RepositoryTracker repositoryTracker = ProvisioningUI.getDefaultUI().getRepositoryTracker();
		repositoryLocations = new HashSet<>();
		monitor.setWorkRemaining(installableConnectors.size() * 5);
		for (CatalogItem descriptor : installableConnectors) {
			URI uri = new URL(descriptor.getSiteUrl()).toURI();
			if (repositoryLocations.add(uri)) {
				checkCancelled(monitor);
				repositoryTracker.addRepository(uri, null, session);
				// ProvisioningUtil.addMetaDataRepository(url.toURI(), true);
				// ProvisioningUtil.addArtifactRepository(url.toURI(), true);
				// ProvisioningUtil.setColocatedRepositoryEnablement(url.toURI(), true);
			}
			monitor.worked(1);
		}

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

	private Set<String> getDescriptorIds(final IMetadataRepository repository) throws URISyntaxException {
		final Set<String> installableUnitIdsThisRepository = new HashSet<>();
		// determine all installable units for this repository
		for (CatalogItem descriptor : installableConnectors) {
			try {
				if (repository.getLocation().equals(new URL(descriptor.getSiteUrl()).toURI())) {
					installableUnitIdsThisRepository.addAll(descriptor.getInstallableUnits());
				}
			} catch (MalformedURLException e) {
				// will never happen, ignore
			}
		}
		return installableUnitIdsThisRepository;
	}

}
