/*******************************************************************************
 * Copyright (c) 2008, 2010 Code 9 and others. All rights reserved. This
 * program and the accompanying materials are made available under the terms of
 * the Eclipse Public License v1.0 which accompanies this distribution, and is
 * available at http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors: 
 *   Code 9 - initial API and implementation
 *   IBM - ongoing development
 ******************************************************************************/
package org.eclipse.equinox.p2.publisher;

import java.net.URI;
import java.util.Collection;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.Tracing;
import org.eclipse.equinox.internal.p2.publisher.Activator;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.repository.*;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;

public class Publisher {
	static final public String PUBLISH_PACK_FILES_AS_SIBLINGS = "publishPackFilesAsSiblings"; //$NON-NLS-1$
	private static final long SERVICE_TIMEOUT = 5000;

	private IPublisherInfo info;
	private IPublisherResult results;

	/**
	 * Returns a metadata repository that corresponds to the given settings.  If a repository at the 
	 * given location already exists, it is updated with the settings and returned.  If no repository
	 * is found then a new Simple repository is created, configured and returned
	 * @param agent the provisioning agent to use when creating the repository
	 * @param location the URL location of the repository
	 * @param name the name of the repository
	 * @param append whether or not the repository should appended or cleared
	 * @param compress whether or not to compress the repository index
	 * @return the discovered or created repository
	 * @throws ProvisionException
	 */
	public static IMetadataRepository createMetadataRepository(IProvisioningAgent agent, URI location, String name, boolean append, boolean compress) throws ProvisionException {
		try {
			IMetadataRepository result = loadMetadataRepository(agent, location, true, true);
			if (result != null && result.isModifiable()) {
				result.setProperty(IRepository.PROP_COMPRESSED, compress ? "true" : "false"); //$NON-NLS-1$//$NON-NLS-2$
				if (!append)
					result.removeAll();
				return result;
			}
		} catch (ProvisionException e) {
			//fall through and create a new repository
		}

		// 	the given repo location is not an existing repo so we have to create something
		IMetadataRepositoryManager manager = getService(agent, IMetadataRepositoryManager.SERVICE_NAME);
		String repositoryName = name == null ? location + " - metadata" : name; //$NON-NLS-1$
		IMetadataRepository result = manager.createRepository(location, repositoryName, IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, null);
		if (result != null) {
			manager.removeRepository(result.getLocation());
			result.setProperty(IRepository.PROP_COMPRESSED, compress ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
			return result;
		}
		// I don't think we can really get here, but just in case, we better throw a provisioning exception
		String msg = org.eclipse.equinox.internal.p2.metadata.repository.Messages.repoMan_internalError;
		throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.INTERNAL_ERROR, msg, null));
	}

	/**
	 * Load a metadata repository from the given location.
	 * @param location the URI location of the repository
	 * @param modifiable whether to ask the manager for a modifiable repository
	 * @param removeFromManager remove the loaded repository from the manager if it wasn't already loaded
	 * @return the loaded repository
	 * @throws ProvisionException
	 */
	public static IMetadataRepository loadMetadataRepository(IProvisioningAgent agent, URI location, boolean modifiable, boolean removeFromManager) throws ProvisionException {
		IMetadataRepositoryManager manager = getService(agent, IMetadataRepositoryManager.SERVICE_NAME);
		boolean existing = manager.contains(location);

		IMetadataRepository result = manager.loadRepository(location, modifiable ? IRepositoryManager.REPOSITORY_HINT_MODIFIABLE : 0, null);
		if (!existing && removeFromManager)
			manager.removeRepository(location);
		return result;
	}

	/**
	 * Returns an artifact repository that corresponds to the given settings.  If a repository at the 
	 * given location already exists, it is updated with the settings and returned.  If no repository
	 * is found then a new Simple repository is created, configured and returned
	 * @param agent the provisioning agent to use when creating the repository
	 * @param location the URL location of the repository
	 * @param name the name of the repository
	 * @param compress whether or not to compress the repository index
	 * @param reusePackedFiles whether or not to include discovered Pack200 files in the repository
	 * @return the discovered or created repository
	 * @throws ProvisionException
	 */
	public static IArtifactRepository createArtifactRepository(IProvisioningAgent agent, URI location, String name, boolean compress, boolean reusePackedFiles) throws ProvisionException {
		try {
			IArtifactRepository result = loadArtifactRepository(agent, location, true, true);
			if (result != null && result.isModifiable()) {
				result.setProperty(IRepository.PROP_COMPRESSED, compress ? "true" : "false"); //$NON-NLS-1$//$NON-NLS-2$
				if (reusePackedFiles)
					result.setProperty(PUBLISH_PACK_FILES_AS_SIBLINGS, "true"); //$NON-NLS-1$
				return result;
			}
		} catch (ProvisionException e) {
			//fall through and create a new repository
		}

		IArtifactRepositoryManager manager = getService(agent, IArtifactRepositoryManager.SERVICE_NAME);
		String repositoryName = name != null ? name : location + " - artifacts"; //$NON-NLS-1$
		IArtifactRepository result = manager.createRepository(location, repositoryName, IArtifactRepositoryManager.TYPE_SIMPLE_REPOSITORY, null);
		if (result != null) {
			manager.removeRepository(result.getLocation());
			if (reusePackedFiles)
				result.setProperty(PUBLISH_PACK_FILES_AS_SIBLINGS, "true"); //$NON-NLS-1$
			result.setProperty(IRepository.PROP_COMPRESSED, compress ? "true" : "false"); //$NON-NLS-1$//$NON-NLS-2$
			return result;
		}
		// I don't think we can really get here, but just in case, we better throw a provisioning exception
		String msg = org.eclipse.equinox.internal.p2.artifact.repository.Messages.repoMan_internalError;
		throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.INTERNAL_ERROR, msg, null));
	}

	/**
	 * Load an artifact repository from the given location.
	 * @param location the URI location of the repository
	 * @param modifiable whether to ask the manager for a modifiable repository
	 * @param removeFromManager remove the loaded repository from the manager if it wasn't already loaded
	 * @return the loaded repository
	 * @throws ProvisionException
	 */
	public static IArtifactRepository loadArtifactRepository(IProvisioningAgent agent, URI location, boolean modifiable, boolean removeFromManager) throws ProvisionException {
		IArtifactRepositoryManager manager = getService(agent, IArtifactRepositoryManager.SERVICE_NAME);
		boolean existing = manager.contains(location);

		IArtifactRepository result = manager.loadRepository(location, modifiable ? IRepositoryManager.REPOSITORY_HINT_MODIFIABLE : 0, null);
		if (!existing && removeFromManager)
			manager.removeRepository(location);
		return result;
	}

	public Publisher(IPublisherInfo info) {
		this.info = info;
		results = new PublisherResult();
	}

	/**
	 * Obtains a service from the agent, waiting for a reasonable timeout period
	 * if the service is not yet available. This method never returns <code>null</code>;
	 * an exception is thrown if the service could not be obtained.
	 * 
	 * @param <T> The type of the service to return
	 * @param agent The agent to obtain the service from
	 * @param serviceName The name of the service to obtain
	 * @return The service instance
	 */
	@SuppressWarnings("unchecked")
	protected static <T> T getService(IProvisioningAgent agent, String serviceName) {
		T service = (T) agent.getService(serviceName);
		if (service != null)
			return service;
		long start = System.currentTimeMillis();
		do {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				//ignore and keep waiting
			}
			service = (T) agent.getService(serviceName);
			if (service != null)
				return service;
		} while ((System.currentTimeMillis() - start) < SERVICE_TIMEOUT);
		//could not obtain the service
		throw new IllegalStateException("Unable to obtain required service: " + serviceName); //$NON-NLS-1$
	}

	public Publisher(IPublisherInfo info, IPublisherResult results) {
		this.info = info;
		this.results = results;
	}

	class ArtifactProcess implements IRunnableWithProgress {

		private final IPublisherAction[] actions;
		private final IPublisherInfo info;
		private IStatus result = null;

		public ArtifactProcess(IPublisherAction[] actions, IPublisherInfo info) {
			this.info = info;
			this.actions = actions;
		}

		public void run(IProgressMonitor monitor) {
			MultiStatus finalStatus = new MultiStatus("this", 0, "publishing result", null); //$NON-NLS-1$//$NON-NLS-2$
			for (int i = 0; i < actions.length; i++) {
				if (monitor.isCanceled()) {
					result = Status.CANCEL_STATUS;
					return;
				}
				IStatus status = actions[i].perform(info, results, monitor);
				finalStatus.merge(status);
				monitor.worked(1);
			}
			result = finalStatus;
		}

		public IStatus getStatus() {
			return result;
		}

	}

	public IStatus publish(IPublisherAction[] actions, IProgressMonitor monitor) {
		if (monitor == null)
			monitor = new NullProgressMonitor();
		SubMonitor sub = SubMonitor.convert(monitor, actions.length);
		if (Tracing.DEBUG_PUBLISHING)
			Tracing.debug("Invoking publisher"); //$NON-NLS-1$
		try {
			ArtifactProcess artifactProcess = new ArtifactProcess(actions, info);

			IStatus finalStatus = null;
			if (info.getArtifactRepository() != null) {
				finalStatus = info.getArtifactRepository().executeBatch(artifactProcess, sub);
				if (!finalStatus.matches(IStatus.ERROR | IStatus.CANCEL))
					// If the batch process didn't report any errors, then 
					// Use the status from our actions
					finalStatus = artifactProcess.getStatus();
			} else {
				artifactProcess.run(sub);
				finalStatus = artifactProcess.getStatus();
			}

			if (Tracing.DEBUG_PUBLISHING)
				Tracing.debug("Publishing complete. Result=" + finalStatus); //$NON-NLS-1$

			// if there were no errors, publish all the ius.
			if (finalStatus.isOK() || finalStatus.matches(IStatus.INFO | IStatus.WARNING))
				savePublishedIUs();

			if (!finalStatus.isOK())
				return finalStatus;
			return Status.OK_STATUS;
		} finally {
			sub.done();
		}
	}

	protected void savePublishedIUs() {
		IMetadataRepository metadataRepository = info.getMetadataRepository();
		if (metadataRepository != null) {
			Collection<IInstallableUnit> ius = results.getIUs(null, null);
			metadataRepository.addInstallableUnits(ius);
		}
	}
}
