/*******************************************************************************
 * Copyright (c) 2007, 2017 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *  	Compeople AG (Stefan Liebig) - various ongoing maintenance
 *   	Genuitec LLC - various bug fixes
 *      Sonatype, Inc. - transport split
 *      Yatta Solutions GmbH - bug 457619
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.artifact.repository;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.artifact.repository.simple.SimpleArtifactDescriptor;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.repository.Transport;
import org.eclipse.equinox.internal.provisional.p2.artifact.repository.processing.ProcessingStepHandler;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.internal.provisional.p2.repository.IStateful;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IArtifactKey;
import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.spi.ArtifactDescriptor;
import org.eclipse.osgi.util.NLS;

/**
 * A request to mirror (copy) an artifact into a given destination artifact repository.
 */
public class MirrorRequest extends ArtifactRequest {

	/**
	 * A Status code that represents an error while processing the artifact. This error is not
	 * related to transport, but rather a problem with the processing step.
	 */
	public static final int ARTIFACT_PROCESSING_ERROR = 2;

	/**
	 * A boolean property controlling whether reporting download statistics is
	 * enabled.
	 */
	public static final boolean DOWNLOAD_STATS_ENABLED = !"false" //$NON-NLS-1$
			.equals(Activator.getContext().getProperty("eclipse.p2.reportDownloadStatistics")); //$NON-NLS-1$

	/**
	 * Maximum number of times a request for a single artifact should be tried
	 */
	private static final int MAX_RETRY_REQUEST = 200;

	/**
	 * The name of a repository property on an artifact repository, indicating the base URI
	 * to be used for reporting download statistics.
	 */
	private static final String PROP_STATS_URI = "p2.statsURI"; //$NON-NLS-1$

	/**
	 * The name of a property on an artifact descriptor, indicating the relative download URI
	 * to be used to report download statistics for that artifact. The value of this property,
	 * if present, is appended to the {@link #PROP_STATS_URI} to create the full URI
	 * for reporting download statistics for that artifact.
	 */
	private static final String PROP_DOWNLOAD_STATS = "download.stats"; //$NON-NLS-1$
	/**
	 * The additional parameters for downloading statistics
	 */
	private String downloadStatsParamters;

	protected final IArtifactRepository target;

	private final Map<String, String> targetDescriptorProperties;
	private final Map<String, String> targetRepositoryProperties;
	protected IArtifactDescriptor descriptor;

	public MirrorRequest(IArtifactKey key, IArtifactRepository targetRepository, Map<String, String> targetDescriptorProperties, Map<String, String> targetRepositoryProperties, Transport transport) {
		this(key, targetRepository, targetDescriptorProperties, targetRepositoryProperties, transport, null);
	}

	public MirrorRequest(IArtifactKey key, IArtifactRepository targetRepository, Map<String, String> targetDescriptorProperties, Map<String, String> targetRepositoryProperties, Transport transport, String statsParameters) {
		super(key, transport);
		target = targetRepository;
		if (targetDescriptorProperties == null || targetDescriptorProperties.isEmpty()) {
			this.targetDescriptorProperties = null;
		} else {
			this.targetDescriptorProperties = new HashMap<>();
			this.targetDescriptorProperties.putAll(targetDescriptorProperties);
		}

		if (targetRepositoryProperties == null || targetRepositoryProperties.isEmpty()) {
			this.targetRepositoryProperties = null;
		} else {
			this.targetRepositoryProperties = new HashMap<>();
			this.targetRepositoryProperties.putAll(targetRepositoryProperties);
		}
		this.downloadStatsParamters = statsParameters;
	}

	@Override
	protected void setSourceRepository(IArtifactRepository value) {
		if (value != getSourceRepository()) {
			// This refreshes the descriptor from the new repository as it might be different.
			// If a MirrorRequest fails for a local repository the descriptor might indicate a "folder based" artifact
			// which causes the MirrorRequest to fail again with a remote repository if the descriptor is not fetched again.
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=457619
			descriptor = null;
		}
		super.setSourceRepository(value);
	}

	@Override
	public void perform(IArtifactRepository sourceRepository, IProgressMonitor monitor) {
		monitor.subTask(NLS.bind(Messages.downloading, getArtifactKey().getId()));
		setSourceRepository(sourceRepository);
		// Do we already have the artifact in the target?
		if (target.contains(getArtifactKey())) {
			setResult(new Status(IStatus.OK, Activator.ID, NLS.bind(Messages.available_already_in, getArtifactKey())));
			return;
		}

		// if the request does not have a descriptor then try to fill one in by getting
		// the list of all and randomly picking one that appears to be optimized.
		IArtifactDescriptor optimized = null;
		IArtifactDescriptor canonical = null;
		if (descriptor == null) {
			IArtifactDescriptor[] descriptors = source.getArtifactDescriptors(getArtifactKey());
			if (descriptors.length > 0) {
				for (IArtifactDescriptor descriptor2 : descriptors) {
					if (descriptor2.getProperty(IArtifactDescriptor.FORMAT) == null)
						canonical = descriptor2;
					else if (ProcessingStepHandler.canProcess(descriptor2))
						optimized = descriptor2;
				}
				boolean chooseCanonical = source.getLocation().getScheme().equals("file"); //$NON-NLS-1$
				// If the source repo is local then look for a canonical descriptor so we don't waste processing time.
				descriptor = chooseCanonical ? canonical : optimized;
				// if the descriptor is still null then we could not find our first choice of format so switch the logic.
				if (descriptor == null)
					descriptor = !chooseCanonical ? canonical : optimized;
			}
		}

		// if the descriptor is not set now then the repo does not have the requested artifact
		// TODO improve the reporting here.  It may be the case that the repo has the artifact
		// but the client does not have a processor
		if (descriptor == null) {
			setResult(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.artifact_not_found, getArtifactKey())));
			return;
		}

		IArtifactDescriptor destinationDescriptor = getDestinationDescriptor(descriptor, descriptor == canonical);
		IStatus status = transfer(destinationDescriptor, descriptor, monitor);
		// if ok, cancelled or transfer has already been done with the canonical form return with status set
		if (status.getSeverity() == IStatus.CANCEL) {
			setResult(status);
			return;
		}
		if (monitor.isCanceled()) {
			setResult(Status.CANCEL_STATUS);
			return;
		}
		if (status.isOK()) {
			setResult(status);
			return;
		}

		// failed, first remove possibly erroneously added descriptor
		if (target.contains(destinationDescriptor))
			target.removeDescriptor(destinationDescriptor);

		if (descriptor == canonical || canonical == null) {
			setResult(status);
			return;
		}

		IStatus canonicalStatus = transfer(getDestinationDescriptor(canonical, true), canonical, monitor);
		// To prevent the optimized transfer status severity from dominating the canonical, only merge
		// if the canonical severity is equal to or higher than the optimized transfer severity.
		if (canonicalStatus.getSeverity() < status.getSeverity())
			setResult(canonicalStatus);
		else
			setResult(new MultiStatus(Activator.ID, canonicalStatus.getCode() != 0 ? canonicalStatus.getCode() : status.getCode(), new IStatus[] {status, canonicalStatus}, Messages.MirrorRequest_multipleDownloadProblems, null));
	}

	private IArtifactDescriptor getDestinationDescriptor(IArtifactDescriptor sourceDescriptor, boolean isCanonical) {
		// Get the descriptor to use to store the artifact
		// Since we are mirroring, ensure we clear out data from the original descriptor that may
		// not apply in the new repo location.
		// TODO this is brittle.  perhaps the repo itself should do this?  there are cases where
		// we really do need to give the repo the actual descriptor to use however...
		IArtifactDescriptor destinationDescriptor = target.createArtifactDescriptor(sourceDescriptor.getArtifactKey());
		//		destinationDescriptor.setProcessingSteps(EMPTY_STEPS);
		//		destinationDescriptor.setProperty(IArtifactDescriptor.DOWNLOAD_MD5, null);
		//		destinationDescriptor.setProperty(IArtifactDescriptor.DOWNLOAD_CONTENTTYPE, null);
		//		destinationDescriptor.setProperty(IArtifactDescriptor.FORMAT, null);
		if (targetDescriptorProperties != null && destinationDescriptor instanceof ArtifactDescriptor)
			((ArtifactDescriptor) destinationDescriptor).addProperties(targetDescriptorProperties);
		if (targetRepositoryProperties != null && destinationDescriptor instanceof SimpleArtifactDescriptor)
			((SimpleArtifactDescriptor) destinationDescriptor).addRepositoryProperties(targetRepositoryProperties);
		return destinationDescriptor;
	}

	/**
	 * Keep retrying the source repository until it reports back that it will be impossible
	 * to get the artifact from it.
	 * @param destinationDescriptor
	 * @param sourceDescriptor
	 * @param monitor
	 * @return the status of the transfer operation
	 */
	protected IStatus transfer(IArtifactDescriptor destinationDescriptor, IArtifactDescriptor sourceDescriptor, IProgressMonitor monitor) {
		MultiStatus allResults = new MultiStatus(Activator.ID, 0, NLS.bind(Messages.MirrorRequest_transferFailed, sourceDescriptor), null);
		IStatus lastResult = Status.OK_STATUS;
		// go until we get one (OK), there are no more mirrors to consider or the operation is cancelled.
		// Put a hard limit of MAX_RETRY_REQUEST so we don't end up in an infinite loop.
		// Really, if you've tried MAX_RETRY_REQUEST times without success, it's time to give up
		// TODO Should we log that we gave up because of the retry count?
		// TODO this needs to be redone with a much better mirror management scheme.

		int counter = 0;
		do {
			lastResult = transferSingle(destinationDescriptor, sourceDescriptor, monitor);
			allResults.add(lastResult);
			if (lastResult.getException() instanceof Error) {
				// Error is more severe than Exception - e.g. an OutOfMemoryError should not be ignored and might hinder further processing
				throw (Error) lastResult.getException();
			}
		} while (lastResult.getSeverity() == IStatus.ERROR && lastResult.getCode() == IArtifactRepository.CODE_RETRY && counter++ < MAX_RETRY_REQUEST);
		IProvisioningEventBus bus = source.getProvisioningAgent().getService(IProvisioningEventBus.class);
		if (bus != null)
			bus.publishEvent(new MirrorEvent(source, sourceDescriptor, lastResult.isOK() ? lastResult : (allResults.getChildren().length <= 1 ? lastResult : allResults)));
		if (lastResult.isOK()) {
			collectStats(sourceDescriptor, monitor);
			return lastResult;
		} else if (allResults.getChildren().length <= 1) {
			return lastResult;
		}
		return allResults;
	}

	/**
	 * Collect download statistics, if specified by the descriptor and the source repository
	 */
	private void collectStats(IArtifactDescriptor sourceDescriptor, IProgressMonitor monitor) {
		if (!DOWNLOAD_STATS_ENABLED) {
			return;
		}
		final String statsProperty = sourceDescriptor.getProperty(PROP_DOWNLOAD_STATS);
		if (statsProperty == null)
			return;
		String statsRoot = sourceDescriptor.getRepository().getProperties().get(PROP_STATS_URI);
		if (statsRoot == null)
			return;
		URI statsURI;
		try {
			statsURI = URIUtil.append(new URI(statsRoot), statsProperty);
			if (downloadStatsParamters != null) {
				try {
					statsURI = new URI(statsURI.getScheme(), statsURI.getUserInfo(), statsURI.getHost(), statsURI.getPort(), statsURI.getPath(), statsURI.getQuery() == null ? downloadStatsParamters : downloadStatsParamters + "&" + statsURI.getQuery(), statsURI.getFragment()); //$NON-NLS-1$
				} catch (URISyntaxException e) {
					LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Unable to create download statistics due to invalid URL query: " + statsRoot + " suffix: " + statsProperty + " query: " + downloadStatsParamters)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				}
			}
		} catch (URISyntaxException e) {
			LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Unable to report download statistics due to invalid URL: " + statsRoot + " suffix: " + statsProperty)); //$NON-NLS-1$ //$NON-NLS-2$
			return;
		}
		try {
			transport.getLastModified(statsURI, monitor);
		} catch (FileNotFoundException e) {
			//ignore because it is expected that the statistics URI doesn't represent an existing file
		} catch (Exception e) {
			LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Failure reporting download statistics to URL: " + statsURI, e)); //$NON-NLS-1$
		}
	}

	private IStatus transferSingle(IArtifactDescriptor destinationDescriptor, IArtifactDescriptor sourceDescriptor, IProgressMonitor monitor) {
		OutputStream destination;
		try {
			destination = target.getOutputStream(destinationDescriptor);
		} catch (ProvisionException e) {
			return e.getStatus();
		}

		IStatus status = null;
		Throwable priorException = null;
		// Do the actual transfer
		try {
			status = getArtifact(sourceDescriptor, destination, monitor);
			if (destination instanceof IStateful && status != null && !status.isOK()) {
				IStatus destStatus = ((IStateful) destination).getStatus();
				IStatus root = extractRootCause(status);
				Throwable e = root != null ? root.getException() : null;
				((IStateful) destination).setStatus(new MultiStatus(Activator.ID, status.getCode(), new IStatus[] {status, destStatus}, status.getMessage(), e));
			}
		} catch (RuntimeException e) {
			priorException = e;
			throw e;
		} catch (Error e) {
			priorException = e;
			throw e;
		} finally {
			try {
				destination.close();
			} catch (IOException e) {
				if (priorException == null) // don't mask a formerly thrown Exception/Error
				{
					if (status != null && status.getSeverity() == IStatus.ERROR && status.getCode() == IArtifactRepository.CODE_RETRY)
						return new MultiStatus(Activator.ID, status.getCode(), new IStatus[] {status}, NLS.bind(Messages.error_closing_stream, getArtifactKey(), target.getLocation()), e);
					return new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.error_closing_stream, getArtifactKey(), target.getLocation()), e);
				}
				// otherwise it is already thrown
			}
		}
		return status;
	}

	protected IStatus getArtifact(IArtifactDescriptor sourceDescriptor, OutputStream destination, IProgressMonitor monitor) {
		return getSourceRepository().getArtifact(sourceDescriptor, destination, monitor);
	}

	/**
	 * Extract the root cause. The root cause is the first severe non-MultiStatus status
	 * containing an exception when searching depth first otherwise null.
	 * @param status
	 * @return root cause
	 */
	private static IStatus extractRootCause(IStatus status) {
		if (status == null)
			return null;
		if (!status.isMultiStatus())
			return constraintStatus(status);

		IStatus[] children = ((MultiStatus) status).getChildren();
		if (children == null)
			return constraintStatus(status);

		for (IStatus element : children) {
			IStatus deeper = extractRootCause(element);
			if (deeper != null)
				return deeper;
		}

		return constraintStatus(status);
	}

	private static IStatus constraintStatus(IStatus status) {
		return status.getSeverity() == IStatus.ERROR && status.getException() != null ? status : null;
	}

	@Override
	public String toString() {
		return Messages.mirroring + getArtifactKey() + " into " + target; //$NON-NLS-1$
	}
}
