/*******************************************************************************
 * Copyright (c) 2013, 2017 Ericsson AB 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:
 *     Ericsson AB - initial API and implementation
 *     Ericsson AB (Pascal Rapicault)
 *     Ericsson AB (Hamdan Msheik)
 *     Red Hat Inc. - Bug 460967 
 ******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.sdk.scheduler.migration;

import java.io.File;
import java.net.URI;
import java.util.*;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.equinox.internal.p2.core.helpers.ServiceHelper;
import org.eclipse.equinox.internal.p2.engine.EngineActivator;
import org.eclipse.equinox.internal.p2.metadata.query.UpdateQuery;
import org.eclipse.equinox.internal.p2.ui.sdk.scheduler.*;
import org.eclipse.equinox.p2.core.IAgentLocation;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
import org.eclipse.equinox.p2.engine.query.IUProfilePropertyQuery;
import org.eclipse.equinox.p2.engine.query.UserVisibleRootQuery;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.IRepositoryManager;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.equinox.p2.ui.ProvisioningUI;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;

public class MigrationSupport {
	private static final String ECLIPSE_P2_SKIP_MIGRATION_WIZARD = "eclipse.p2.skipMigrationWizard"; //$NON-NLS-1$
	private static final String ECLIPSE_P2_SKIP_MOVED_INSTALL_DETECTION = "eclipse.p2.skipMovedInstallDetection"; //$NON-NLS-1$

	// The return value indicates if the migration dialog has been shown or not. It
	// does not indicate whether the migration has completed.
	public boolean performMigration(IProvisioningAgent agent, IProfileRegistry registry, IProfile currentProfile) {
		boolean skipWizard = Boolean.TRUE.toString()
				.equalsIgnoreCase(System.getProperty(ECLIPSE_P2_SKIP_MIGRATION_WIZARD));
		if (skipWizard)
			return false;

		IProfile previousProfile = null;
		URI[] reposToMigrate = null;
		if (!skipFirstTimeMigration() && !configurationSpecifiedManually()
				&& isFirstTimeRunningThisSharedInstance(agent, registry, currentProfile)) {
			File searchRoot = getSearchLocation();
			if (searchRoot == null)
				return false;

			IProvisioningAgent otherConfigAgent = new PreviousConfigurationFinder(
					getConfigurationLocation().getParentFile()).findPreviousInstalls(searchRoot, getInstallFolder());
			if (otherConfigAgent == null) {
				return false;
			}
			previousProfile = otherConfigAgent.getService(IProfileRegistry.class).getProfile(IProfileRegistry.SELF);
			if (previousProfile == null)
				return false;

			reposToMigrate = otherConfigAgent.getService(IMetadataRepositoryManager.class)
					.getKnownRepositories(IRepositoryManager.REPOSITORIES_NON_SYSTEM);
			reposToMigrate = copyOf(reposToMigrate, reposToMigrate.length + 1);
			reposToMigrate[reposToMigrate.length - 1] = getURIForProfile(otherConfigAgent, previousProfile);
		}

		if (previousProfile == null && baseChangedSinceLastPresentationOfWizard(agent, registry, currentProfile))
			previousProfile = findMostRecentReset(registry, currentProfile);

		if (previousProfile == null)
			return false;

		Collection<IInstallableUnit> unitsToMigrate = findUnitstoMigrate(previousProfile, currentProfile);
		if (!unitsToMigrate.isEmpty()) {
			openMigrationWizard(previousProfile, unitsToMigrate, reposToMigrate);
		} else {
			// There is nothing to migrate, so we mark the migration complete
			rememberMigrationCompleted();
		}
		return true;
	}

	private static URI[] copyOf(URI[] original, int newLength) {
		URI[] copy = new URI[newLength];
		int copyCount = Math.min(original.length, newLength);
		System.arraycopy(original, 0, copy, 0, copyCount);
		return copy;
	}

	private URI getURIForProfile(IProvisioningAgent agent, IProfile profile) {
		IAgentLocation agentLocation = agent.getService(IAgentLocation.class);
		return URIUtil.append(agentLocation.getRootLocation(),
				"org.eclipse.equinox.p2.engine/profileRegistry/" + profile.getProfileId() + ".profile"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private File getInstallFolder() {
		Location configurationLocation = ServiceHelper.getService(EngineActivator.getContext(), Location.class,
				Location.INSTALL_FILTER);
		return new File(configurationLocation.getURL().getPath());
	}

	// The search location is two level up from the configuration location.
	private File getSearchLocation() {
		File parent = getConfigurationLocation().getParentFile();
		if (parent == null)
			return null;
		return parent.getParentFile();
	}

	private File getConfigurationLocation() {
		Location configurationLocation = ServiceHelper.getService(EngineActivator.getContext(), Location.class,
				Location.CONFIGURATION_FILTER);
		File configurationFolder = new File(configurationLocation.getURL().getPath());
		return configurationFolder;
	}

	// Check if the user has explicitly specified -configuration on the command line
	private boolean configurationSpecifiedManually() {
		String commandLine = System.getProperty("eclipse.commands"); //$NON-NLS-1$
		if (commandLine == null)
			return false;
		return commandLine.contains("-configuration\n"); //$NON-NLS-1$
	}

	private boolean skipFirstTimeMigration() {
		return Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty(ECLIPSE_P2_SKIP_MOVED_INSTALL_DETECTION));
	}

	private boolean isFirstTimeRunningThisSharedInstance(IProvisioningAgent agent, IProfileRegistry registry,
			IProfile currentProfile) {
		long[] history = registry.listProfileTimestamps(currentProfile.getProfileId());
		boolean isInitial = IProfile.STATE_SHARED_INSTALL_VALUE_INITIAL
				.equals(registry.getProfileStateProperties(currentProfile.getProfileId(), history[0])
						.get(IProfile.STATE_PROP_SHARED_INSTALL));
		if (isInitial) {
			if (getLastMigration() >= history[0])
				return false;
			// This detect the case where the user has not done any migration.
			Map<String, String> sharedRelatedValues = registry.getProfileStateProperties(currentProfile.getProfileId(),
					IProfile.STATE_PROP_SHARED_INSTALL);
			if (sharedRelatedValues.containsValue(IProfile.STATE_SHARED_INSTALL_VALUE_NEW))
				return false;
			return true;
		}
		return false;
	}

	/**
	 * @param previousProfile is the profile used previous to the current one
	 * @param currentProfile  is the current profile used by eclipse.
	 * @return true if set difference between previousProfile units and
	 *         currentProfile units not empty, otherwise false
	 */
	protected Collection<IInstallableUnit> findUnitstoMigrate(IProfile previousProfile, IProfile currentProfile) {
		// First, try the case of inclusion
		Set<IInstallableUnit> previousProfileUnits = getUserRoots(previousProfile);
		Set<IInstallableUnit> currentProfileUnits = currentProfile.available(new UserVisibleRootQuery(), null).toSet();
		previousProfileUnits.removeAll(currentProfileUnits);

		// For the IUs left in the previous profile, look for those that could be in the
		// base but not as roots
		Iterator<IInstallableUnit> previousProfileIterator = previousProfileUnits.iterator();
		while (previousProfileIterator.hasNext()) {
			if (!currentProfile.available(QueryUtil.createIUQuery(previousProfileIterator.next()), null).isEmpty())
				previousProfileIterator.remove();
		}

		// For the IUs left in the previous profile, look for those that could be
		// available in the root but as higher versions (they could be root or not)
		previousProfileIterator = previousProfileUnits.iterator();
		while (previousProfileIterator.hasNext()) {
			if (!currentProfile.available(new UpdateQuery(previousProfileIterator.next()), null).isEmpty())
				previousProfileIterator.remove();
		}

		return previousProfileUnits;
	}

	private Set<IInstallableUnit> getUserRoots(IProfile previousProfile) {
		IQueryResult<IInstallableUnit> allRoots = previousProfile.query(new UserVisibleRootQuery(), null);
		Set<IInstallableUnit> rootsFromTheBase = previousProfile
				.query(new IUProfilePropertyQuery("org.eclipse.equinox.p2.base", "true"), null).toUnmodifiableSet();
		Set<IInstallableUnit> userRoots = allRoots.toSet();
		userRoots.removeAll(rootsFromTheBase);
		return userRoots;
	}

	protected void openMigrationWizard(final IProfile inputProfile, final Collection<IInstallableUnit> unitsToMigrate,
			final URI[] reposToMigrate) {
		Display d = Display.getDefault();
		d.asyncExec(() -> {
			WizardDialog migrateWizard = new MigrationWizardDialog(getWorkbenchWindowShell(),
					new MigrationWizard(inputProfile, unitsToMigrate, reposToMigrate, reposToMigrate != null));
			migrateWizard.create();
			migrateWizard.open();
		});
	}

	private boolean baseChangedSinceLastPresentationOfWizard(IProvisioningAgent agent, IProfileRegistry registry,
			IProfile profile) {
		long lastProfileMigrated = getLastMigration();
		long lastResetTimestamp = findMostRecentResetTimestamp(registry, profile);
		return lastProfileMigrated <= lastResetTimestamp;
	}

	// The timestamp from which we migrated or -1
	private long findMostRecentResetTimestamp(IProfileRegistry registry, IProfile profile) {
		long[] history = registry.listProfileTimestamps(profile.getProfileId());
		int index = history.length - 1;
		boolean found = false;
		while (!(found = IProfile.STATE_SHARED_INSTALL_VALUE_BEFOREFLUSH
				.equals(registry.getProfileStateProperties(profile.getProfileId(), history[index])
						.get(IProfile.STATE_PROP_SHARED_INSTALL)))
				&& index > 0) {
			index--;
		}
		if (!found)
			return -1;
		return history[index];
	}

	private IProfile findMostRecentReset(IProfileRegistry registry, IProfile profile) {
		long ts = findMostRecentResetTimestamp(registry, profile);
		if (ts == -1)
			return null;
		return registry.getProfile(profile.getProfileId(), ts);
	}

	Shell getWorkbenchWindowShell() {
		IWorkbenchWindow activeWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		return activeWindow != null ? activeWindow.getShell() : null;
	}

	public void rememberMigrationCompleted() {
		IProfileRegistry registry = ProvisioningUI.getDefaultUI().getSession().getProvisioningAgent()
				.getService(IProfileRegistry.class);
		long[] history = registry.listProfileTimestamps(ProvisioningUI.getDefaultUI().getProfileId());
		AutomaticUpdatePlugin.getDefault().getPreferenceStore()
				.setValue(AutomaticUpdateScheduler.MIGRATION_DIALOG_SHOWN, history[history.length - 1]);
		AutomaticUpdatePlugin.getDefault().savePreferences();
	}

	// Get the timestamp that we migrated from. O if we have not migrated.
	public long getLastMigration() {
		return AutomaticUpdatePlugin.getDefault().getPreferenceStore()
				.getLong(AutomaticUpdateScheduler.MIGRATION_DIALOG_SHOWN);
	}
}
