/*******************************************************************************
 * Copyright (c) 2007, 2013 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     EclipseSource - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.p2.publisher;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.equinox.internal.p2.artifact.repository.CompositeArtifactRepository;
import org.eclipse.equinox.internal.p2.metadata.repository.CompositeMetadataRepository;
import org.eclipse.equinox.internal.p2.publisher.Activator;
import org.eclipse.equinox.internal.p2.publisher.Messages;
import org.eclipse.equinox.p2.core.*;
import org.eclipse.equinox.p2.metadata.IArtifactKey;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.artifact.ArtifactKeyQuery;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.ServiceReference;

public abstract class AbstractPublisherApplication implements IApplication {

	// The mapping rules for in-place generation need to construct paths into the structure
	// of an eclipse installation; in the future the default artifact mapping declared in
	// SimpleArtifactRepository may change, for example, to not have a 'bundles' directory
	// instead of a 'plugins' directory, so a separate constant is defined and used here.
	static final protected String[][] INPLACE_MAPPING_RULES = { {"(& (classifier=osgi.bundle) (format=packed)", "${repoUrl}/features/${id}_${version}.jar.pack.gz"}, //$NON-NLS-1$//$NON-NLS-2$
			{"(& (classifier=osgi.bundle))", "${repoUrl}/plugins/${id}_${version}.jar"}, //$NON-NLS-1$//$NON-NLS-2$
			{"(& (classifier=binary))", "${repoUrl}/binary/${id}_${version}"}, //$NON-NLS-1$//$NON-NLS-2$
			{"(& (classifier=org.eclipse.update.feature))", "${repoUrl}/features/${id}_${version}.jar"}}; //$NON-NLS-1$//$NON-NLS-2$

	static final public String PUBLISH_PACK_FILES_AS_SIBLINGS = "publishPackFilesAsSiblings"; //$NON-NLS-1$

	protected PublisherInfo info;
	protected String source;
	protected URI metadataLocation;
	protected String metadataRepoName;
	protected URI artifactLocation;
	protected String artifactRepoName;
	protected URI[] contextMetadataRepositories;
	protected URI[] contextArtifactRepositories;
	//whether repository xml files should be compressed
	protected boolean compress = false;
	protected boolean inplace = false;
	protected boolean append = false;
	protected boolean reusePackedFiles = false;
	protected String[] configurations;
	private IStatus status;

	private ServiceReference<IProvisioningAgent> agentRef;

	protected IProvisioningAgent agent;

	/**
	 * Returns the error message for this application, or the empty string
	 * if the application terminated successfully.
	 */
	public IStatus getStatus() {
		return status;
	}

	protected void initialize(PublisherInfo publisherInfo) throws ProvisionException {
		if (inplace) {
			File location = new File(source);
			if (metadataLocation == null)
				metadataLocation = location.toURI();
			if (artifactLocation == null)
				artifactLocation = location.toURI();
			publisherInfo.setArtifactOptions(publisherInfo.getArtifactOptions() | IPublisherInfo.A_INDEX | IPublisherInfo.A_PUBLISH);
		}
		initializeRepositories(publisherInfo);
	}

	protected IStatus createConfigurationEror(String message) {
		return new Status(IStatus.ERROR, "org.eclipse.equinox.p2.publisher", message); //$NON-NLS-1$
	}

	private boolean isEmpty(IArtifactRepository repo) {
		IQueryResult<IArtifactKey> result = repo.query(QueryUtil.createLimitQuery(ArtifactKeyQuery.ALL_KEYS, 1), null);
		return result.isEmpty();
	}

	protected void initializeRepositories(PublisherInfo publisherInfo) throws ProvisionException {
		if (artifactLocation != null) {
			IArtifactRepository repo = Publisher.createArtifactRepository(agent, artifactLocation, artifactRepoName, compress, reusePackedFiles);
			if (!append && !isEmpty(repo)) {
				File repoLocation = URIUtil.toFile(artifactLocation);
				if (repoLocation != null && source != null) {
					if (repoLocation.isFile())
						repoLocation = repoLocation.getParentFile();
					if (repoLocation.equals(new File(source)))
						throw new IllegalArgumentException(NLS.bind(Messages.exception_artifactRepoNoAppendDestroysInput, URIUtil.toUnencodedString(artifactLocation)));
				}
				repo.removeAll(new NullProgressMonitor());
			}
			publisherInfo.setArtifactRepository(repo);
		} else if ((publisherInfo.getArtifactOptions() & IPublisherInfo.A_PUBLISH) > 0)
			throw new ProvisionException(createConfigurationEror(Messages.exception_noArtifactRepo));
		if (metadataLocation == null)
			throw new ProvisionException(createConfigurationEror(Messages.exception_noMetadataRepo));
		publisherInfo.setMetadataRepository(Publisher.createMetadataRepository(agent, metadataLocation, metadataRepoName, append, compress));

		if (contextMetadataRepositories != null && contextMetadataRepositories.length > 0) {
			CompositeMetadataRepository contextMetadata = CompositeMetadataRepository.createMemoryComposite(agent);
			if (contextMetadata != null) {
				for (int i = 0; i < contextMetadataRepositories.length; i++)
					contextMetadata.addChild(contextMetadataRepositories[i]);
				if (contextMetadata.getChildren().size() > 0)
					publisherInfo.setContextMetadataRepository(contextMetadata);
			}
		}
		if (contextArtifactRepositories != null && contextArtifactRepositories.length > 0) {
			CompositeArtifactRepository contextArtifact = CompositeArtifactRepository.createMemoryComposite(agent);
			if (contextArtifact != null) {
				for (int i = 0; i < contextArtifactRepositories.length; i++)
					contextArtifact.addChild(contextArtifactRepositories[i]);

				if (contextArtifact.getChildren().size() > 0)
					publisherInfo.setContextArtifactRepository(contextArtifact);
			}
		}
	}

	protected void processCommandLineArguments(String[] args, PublisherInfo publisherInfo) throws Exception {
		if (args == null)
			return;
		for (int i = 0; i < args.length; i++) {
			// check for args without parameters (i.e., a flag arg)
			processFlag(args[i], publisherInfo);

			// check for args with parameters. If we are at the last argument or if the next one
			// has a '-' as the first character, then we can't have an arg with a parm so continue.
			if (i == args.length - 1 || args[i + 1].startsWith("-")) //$NON-NLS-1$
				continue;
			processParameter(args[i], args[++i], publisherInfo);
		}
	}

	/**
	 * Process application arguments.
	 * @throws URISyntaxException thrown by subclasses
	 */
	protected void processParameter(String arg, String parameter, PublisherInfo publisherInfo) throws URISyntaxException {
		try {
			if (arg.equalsIgnoreCase("-metadataRepository") || arg.equalsIgnoreCase("-mr")) //$NON-NLS-1$ //$NON-NLS-2$
				metadataLocation = URIUtil.fromString(parameter);

			if (arg.equalsIgnoreCase("-artifactRepository") || arg.equalsIgnoreCase("-ar")) //$NON-NLS-1$ //$NON-NLS-2$
				artifactLocation = URIUtil.fromString(parameter);
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException(NLS.bind(Messages.exception_repoMustBeURL, parameter));
		}

		if (arg.equalsIgnoreCase("-metadataRepositoryName")) //$NON-NLS-1$
			metadataRepoName = parameter;

		if (arg.equalsIgnoreCase("-source")) { //$NON-NLS-1$
			// check here to see if the location actually exists so we can fail gracefully now rather than unpredictably later
			// see bug 272956 where we would fail with an NPE if someone gave us a URL instead of a file-system path
			if (!new File(parameter).exists())
				throw new IllegalArgumentException(NLS.bind(Messages.exception_sourcePath, parameter));
			source = parameter;
		}

		if (arg.equalsIgnoreCase("-artifactRepositoryName")) //$NON-NLS-1$
			artifactRepoName = parameter;

		if (arg.equalsIgnoreCase("-configs")) //$NON-NLS-1$
			publisherInfo.setConfigurations(AbstractPublisherAction.getArrayFromString(parameter, ",")); //$NON-NLS-1$

		if (arg.equalsIgnoreCase("-contextMetadata")) //$NON-NLS-1$
			setContextRepositories(processRepositoryList(parameter), contextArtifactRepositories);

		if (arg.equalsIgnoreCase("-contextArtifacts")) //$NON-NLS-1$
			setContextRepositories(contextMetadataRepositories, processRepositoryList(parameter));
	}

	private URI[] processRepositoryList(String parameter) {
		String[] list = AbstractPublisherAction.getArrayFromString(parameter, ","); //$NON-NLS-1$
		if (list == null || list.length == 0)
			return null;

		List<URI> result = new ArrayList<URI>(list.length);
		if (result != null) {
			for (int i = 0; i < list.length; i++) {
				try {
					result.add(URIUtil.fromString(list[i]));
				} catch (URISyntaxException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		return result.toArray(new URI[result.size()]);
	}

	protected void processFlag(String arg, PublisherInfo publisherInfo) {
		if (arg.equalsIgnoreCase("-publishArtifacts") || arg.equalsIgnoreCase("-pa")) //$NON-NLS-1$ //$NON-NLS-2$
			publisherInfo.setArtifactOptions(publisherInfo.getArtifactOptions() | IPublisherInfo.A_PUBLISH);

		if (arg.equalsIgnoreCase("-publishArtifactRepository") || arg.equalsIgnoreCase("-par")) //$NON-NLS-1$ //$NON-NLS-2$
			publisherInfo.setArtifactOptions(publisherInfo.getArtifactOptions() | IPublisherInfo.A_INDEX);

		if (arg.equalsIgnoreCase("-overwriteArtifacts")) //$NON-NLS-1$ 
			publisherInfo.setArtifactOptions(publisherInfo.getArtifactOptions() | IPublisherInfo.A_OVERWRITE);

		if (arg.equalsIgnoreCase("-append")) //$NON-NLS-1$
			append = true;

		if (arg.equalsIgnoreCase("-compress")) //$NON-NLS-1$
			compress = true;

		if (arg.equalsIgnoreCase("-reusePack200Files")) //$NON-NLS-1$
			reusePackedFiles = true;

		if (arg.equalsIgnoreCase("-inplace")) //$NON-NLS-1$
			inplace = true;
	}

	private void setupAgent() throws ProvisionException {
		agentRef = Activator.getContext().getServiceReference(IProvisioningAgent.class);
		if (agentRef != null) {
			agent = Activator.getContext().getService(agentRef);
			if (agent != null)
				return;
		}
		ServiceReference<IProvisioningAgentProvider> providerRef = Activator.getContext().getServiceReference(IProvisioningAgentProvider.class);
		if (providerRef == null)
			throw new RuntimeException("No provisioning agent provider is available"); //$NON-NLS-1$
		IProvisioningAgentProvider provider = Activator.getContext().getService(providerRef);
		if (provider == null)
			throw new RuntimeException("No provisioning agent provider is available"); //$NON-NLS-1$
		//obtain agent for currently running system
		agent = provider.createAgent(null);
		Activator.getContext().ungetService(providerRef);
	}

	public Object run(String args[]) throws Exception {
		try {
			info = createPublisherInfo();
			processCommandLineArguments(args, info);
			Object result = run(info);
			if (result != IApplication.EXIT_OK) {
				System.out.println(NLS.bind(Messages.message_publisherArguments, null));
				for (int i = 0; i < args.length; i++)
					System.out.println(args[i]);
			}
			return result;
		} catch (Exception e) {
			if (e.getMessage() != null)
				System.err.println(e.getMessage());
			else
				e.printStackTrace(System.err);
			throw e;
		}
	}

	protected PublisherInfo createPublisherInfo() {
		return new PublisherInfo();
	}

	public Object run(PublisherInfo publisherInfo) throws Exception {
		try {
			this.info = publisherInfo;
			setupAgent();
			initialize(publisherInfo);
			System.out.println(NLS.bind(Messages.message_generatingMetadata, publisherInfo.getSummary()));

			long before = System.currentTimeMillis();
			IPublisherAction[] actions = createActions();
			Publisher publisher = createPublisher(publisherInfo);
			IStatus result = publisher.publish(actions, new NullProgressMonitor());
			long after = System.currentTimeMillis();

			if (!result.isOK()) {
				// TODO: improve the string representation of the result
				System.out.println(result);
				Throwable th = result.getException();
				if (th != null) {
					System.out.println();
					System.out.println(NLS.bind(Messages.message_resultException, null));
					th.printStackTrace(System.out);
					System.out.println();
				}
			}
			if (!result.matches(IStatus.ERROR | IStatus.CANCEL)) {
				System.out.println(NLS.bind(Messages.message_generationCompleted, String.valueOf((after - before) / 1000)));
				return IApplication.EXIT_OK;
			}
		} catch (ProvisionException e) {
			status = e.getStatus();
			if (status.getSeverity() == IStatus.ERROR && status.getMessage() != null) {
				System.out.println(status.getMessage());
			}
		}
		return new Integer(1);
	}

	protected abstract IPublisherAction[] createActions();

	protected Publisher createPublisher(PublisherInfo publisherInfo) {
		return new Publisher(publisherInfo);
	}

	public Object start(IApplicationContext context) throws Exception {
		return run((String[]) context.getArguments().get("application.args")); //$NON-NLS-1$
	}

	public void stop() {
		if (agentRef != null) {
			Activator.getContext().ungetService(agentRef);
			agentRef = null;
		}
	}

	public void setArtifactLocation(URI location) {
		this.artifactLocation = location;
	}

	public void setMetadataLocation(URI location) {
		this.metadataLocation = location;
	}

	public boolean reuseExistingPack200Files() {
		return reusePackedFiles;
	}

	public void setReuseExistingPackedFiles(boolean value) {
		reusePackedFiles = value;
	}

	public void setCompress(boolean value) {
		compress = value;
	}

	public void setContextRepositories(URI[] metadata, URI[] artifacts) {
		this.contextMetadataRepositories = metadata;
		this.contextArtifactRepositories = artifacts;
	}
}
