/*******************************************************************************
 * Copyright (c) 2010, 2015 EclipseSource Inc. 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:
 *     EclipseSource Inc. - initial API and implementation
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 477527
 *******************************************************************************/
package org.eclipse.pde.internal.core.target;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.IFileSystem;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.IProvidedCapability;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.pde.core.target.ITargetDefinition;
import org.eclipse.pde.core.target.ITargetLocation;
import org.eclipse.pde.core.target.NameVersionDescriptor;
import org.eclipse.pde.core.target.TargetBundle;
import org.eclipse.pde.core.target.TargetFeature;
import org.eclipse.pde.internal.core.PDECoreMessages;

/**
 * This job exports the bundles and features that make up your target.
 */
public class ExportTargetJob extends Job {

	private final URI fDestination;
	private boolean fclearDestinationDirectory = true;
	private IFileStore featureDir;
	private IFileStore pluginDir;
	private IFileSystem fileSystem;
	private Map<String, NameVersionDescriptor[]> filter;
	private final ITargetDefinition fTarget;

	public ExportTargetJob(ITargetDefinition target, URI destination, boolean clearDestinationDirectory) {
		super("Export Current Target Definition"); //$NON-NLS-1$
		fTarget = target;
		fDestination = destination;
		fclearDestinationDirectory = clearDestinationDirectory;
	}

	@Override
	protected IStatus run(IProgressMonitor monitor) {
		try {
			constructFilter(fTarget);
			ITargetLocation[] containers = fTarget.getTargetLocations();
			if (containers == null) {
				containers = new ITargetLocation[0];
			}
			int totalWork = containers.length;
			monitor.beginTask(PDECoreMessages.ExportTargetDefinition_task, totalWork);

			monitor.subTask(PDECoreMessages.ExportTargetJob_ConfiguringDestination);
			setupDestination(monitor);

			monitor.subTask(PDECoreMessages.ExportTargetJob_ExportingTargetContents);
			for (ITargetLocation targetLocation : containers) {
				ITargetLocation container = targetLocation;
				container.resolve(fTarget, monitor);
				if (!(container instanceof IUBundleContainer)) {
					exportContainer(container, fTarget, featureDir, pluginDir, fileSystem, monitor);
				}
			}
			exportProfile(fTarget, fDestination, monitor);
		} catch (CoreException e) {
			return Status.error("Failed to export the target", e); //$NON-NLS-1$
		} finally {
			monitor.done();
		}
		return Status.OK_STATUS;
	}

	private void constructFilter(ITargetDefinition target) {
		NameVersionDescriptor[] included = target.getIncluded();
		if (included == null) {
			return;
		}
		filter = new HashMap<>();
		for (NameVersionDescriptor inclusion : included) {
			NameVersionDescriptor[] versions = filter.get(inclusion.getId());
			if (versions == null) {
				filter.put(inclusion.getId(), new NameVersionDescriptor[] {inclusion});
			} else {
				NameVersionDescriptor[] versions2 = new NameVersionDescriptor[versions.length + 1];
				System.arraycopy(versions, 0, versions2, 0, versions.length);
				versions2[versions.length] = inclusion;
				filter.put(inclusion.getId(), versions2);
			}
		}
	}

	private void setupDestination(IProgressMonitor monitor) throws CoreException {
		fileSystem = EFS.getLocalFileSystem();
		if (!fileSystem.canWrite()) {
			throw new CoreException(Status.error("Destination directory not writable.")); //$NON-NLS-1$
		}
		IFileStore destination = fileSystem.getStore(fDestination);
		featureDir = destination.getChild("features"); //$NON-NLS-1$ExportTargetJob
		pluginDir = destination.getChild("plugins"); //$NON-NLS-1$
		if (fclearDestinationDirectory) {
			monitor.subTask(PDECoreMessages.ExportTargetDeleteOldData); //Deleting old data...
			featureDir.delete(EFS.NONE, null);
			pluginDir.delete(EFS.NONE, null);
		}
		featureDir.mkdir(EFS.NONE, null);
		pluginDir.mkdir(EFS.NONE, null);
	}

	private boolean shouldExport(NameVersionDescriptor descriptor) {
		// currently PDE does not selectively include/exclude features
		if (filter == null || descriptor.getType().equals(NameVersionDescriptor.TYPE_FEATURE)) {
			return true;
		}
		NameVersionDescriptor[] versions = filter.get(descriptor.getId());
		if (versions == null) {
			return false;
		}
		for (NameVersionDescriptor nameVersionDescriptor : versions) {
			String version = nameVersionDescriptor.getVersion();
			if ((version == null || version.equals(descriptor.getVersion())) && descriptor.getType().equals(nameVersionDescriptor.getType())) {
				return true;
			}
		}
		return false;
	}

	private boolean shouldExport(TargetFeature feature) {
		if (filter == null) {
			return true;
		}
		NameVersionDescriptor descriptor = new NameVersionDescriptor(feature.getId(), feature.getVersion(), NameVersionDescriptor.TYPE_FEATURE);
		return shouldExport(descriptor);
	}

	private boolean shouldExport(TargetBundle bundle) {
		if (filter == null) {
			return true;
		}
		NameVersionDescriptor descriptor = new NameVersionDescriptor(bundle.getBundleInfo().getSymbolicName(), bundle.getBundleInfo().getVersion(), NameVersionDescriptor.TYPE_PLUGIN);
		return shouldExport(descriptor);
	}

	private boolean shouldExport(IInstallableUnit iu) {
		if (filter == null) {
			return true;
		}
		NameVersionDescriptor descriptor = null;
		String feature = getCapability(iu, "org.eclipse.update.feature"); //$NON-NLS-1$
		if (feature != null) {
			descriptor = new NameVersionDescriptor(feature, iu.getVersion().toString(), NameVersionDescriptor.TYPE_FEATURE);
		} else if (iu.getId().endsWith(".feature.group")) { //$NON-NLS-1$
			descriptor = new NameVersionDescriptor(iu.getId(), iu.getVersion().toString(), NameVersionDescriptor.TYPE_FEATURE);
		} else if ("bundle".equalsIgnoreCase(getCapability(iu, "org.eclipse.equinox.p2.eclipse.type"))) { //$NON-NLS-1$ //$NON-NLS-2$
			descriptor = new NameVersionDescriptor(iu.getId(), iu.getVersion().toString(), NameVersionDescriptor.TYPE_PLUGIN);
		} else if ("source".equalsIgnoreCase(getCapability(iu, "org.eclipse.equinox.p2.eclipse.type"))) { //$NON-NLS-1$ //$NON-NLS-2$
			descriptor = new NameVersionDescriptor(iu.getId(), iu.getVersion().toString(), NameVersionDescriptor.TYPE_PLUGIN);
		}
		// default to true unless we know otherwise. This ensures that random metadata bits
		// are moved over to the target as they might be needed in a future provisioning operation.
		// We could move only unknown IUs that do NOT have artifacts... Have to think about that.
		return descriptor == null ? true : shouldExport(descriptor);
	}

	private String getCapability(IInstallableUnit iu, String namespace) {
		for (IProvidedCapability capability : iu.getProvidedCapabilities()) {
			if (capability.getNamespace().equals(namespace)) {
				return capability.getName();
			}
		}
		return null;
	}

	private void exportContainer(ITargetLocation container, ITargetDefinition target, IFileStore featureDir, IFileStore pluginDir, IFileSystem fileSystem, IProgressMonitor monitor) throws CoreException {
		TargetFeature[] features = container.getFeatures();
		if (features != null) {
			monitor.subTask(PDECoreMessages.ExportTargetExportFeatures);
			for (TargetFeature feature : features) {
				if (shouldExport(feature)) {
					String location = feature.getLocation();
					if (location != null) {
						copy(location, featureDir, fileSystem, monitor);
					}
				}
			}
		}

		TargetBundle[] bundles = container.getBundles();
		if (bundles != null) {
			monitor.subTask(PDECoreMessages.ExportTargetExportPlugins);
			for (TargetBundle bundle : bundles) {
				if (shouldExport(bundle)) {
					copy(bundle.getBundleInfo().getLocation().getPath(), pluginDir, fileSystem, monitor);
				}
			}
		}
	}

	private IStatus copy(String src, IFileStore destinationParent, IFileSystem fileSystem, IProgressMonitor monitor) throws CoreException {
		IPath srcPath = IPath.fromOSString(src);

		IFileStore source = fileSystem.getStore(srcPath);
		String elementName = srcPath.segment(srcPath.segmentCount() - 1);
		IFileStore destination = destinationParent.getChild(elementName);

		SubMonitor subMonitor = SubMonitor.convert(monitor, 1);

		if (destination.fetchInfo().exists()) {
			return Status.OK_STATUS;
		}
		if (source.fetchInfo().isDirectory()) {
			destination.mkdir(EFS.NONE, new NullProgressMonitor());
		}
		source.copy(destination, EFS.OVERWRITE, subMonitor.split(1));
		return Status.OK_STATUS;
	}

	@SuppressWarnings("restriction")
	private org.eclipse.equinox.p2.internal.repository.tools.RepositoryDescriptor createRepoDescriptor(URI location,
			String name, String kind) {
		org.eclipse.equinox.p2.internal.repository.tools.RepositoryDescriptor result = new org.eclipse.equinox.p2.internal.repository.tools.RepositoryDescriptor();
		result.setLocation(location);
		result.setKind(kind);
		result.setName(name);
		if (fclearDestinationDirectory) {
			result.setAppend(false);
		}
		return result;
	}

	@SuppressWarnings("restriction")
	private static final String KIND_METADATA = org.eclipse.equinox.p2.internal.repository.tools.RepositoryDescriptor.KIND_METADATA;
	@SuppressWarnings("restriction")
	private static final String KIND_ARTIFACT = org.eclipse.equinox.p2.internal.repository.tools.RepositoryDescriptor.KIND_ARTIFACT;

	@SuppressWarnings("restriction")
	private void exportProfile(ITargetDefinition target, URI destination, IProgressMonitor monitor)
			throws CoreException {
		var exporter = new org.eclipse.equinox.p2.internal.repository.tools.Repo2Runnable();
		exporter.addDestination(createRepoDescriptor(destination, P2TargetUtils.getProfileId(target), KIND_METADATA));
		exporter.addDestination(createRepoDescriptor(destination, P2TargetUtils.getProfileId(target), KIND_ARTIFACT));
		exporter.addSource(createRepoDescriptor(P2TargetUtils.getBundlePool().getLocation(), null, KIND_ARTIFACT));

		IQueryResult<IInstallableUnit> ius = P2TargetUtils.getIUs(target, monitor);
		ArrayList<IInstallableUnit> toExport = new ArrayList<>();
		for (IInstallableUnit iu : ius) {
			if (shouldExport(iu)) {
				toExport.add(iu);
			}
		}
		exporter.setSourceIUs(toExport);
		exporter.run(monitor);
	}
}
