/*******************************************************************************
 * Copyright (c) 2000, 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
 *******************************************************************************/
package org.eclipse.ui.internal.wizards.datatransfer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
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.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.operation.ModalContext;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;

/**
 *	Operation for exporting a resource and its children to a new .zip or
 *  .tar.gz file.
 *
 *  @since 3.1
 */
public class ArchiveFileExportOperation implements IRunnableWithProgress {
	private IFileExporter exporter;

	private String destinationFilename;

	private IProgressMonitor monitor;

	private List<? extends IResource> resourcesToExport;

	private IResource resource;

	private List<IStatus> errorTable = new ArrayList<>(1); // IStatus

	private boolean useCompression = true;

	private boolean resolveLinks = false;

	private boolean useTarFormat = false;

	private boolean createLeadupStructure = true;

	/**
	 *	Create an instance of this class.  Use this constructor if you wish to
	 *	export specific resources without a common parent resource
	 *
	 *	@param resources java.util.Vector
	 *	@param filename java.lang.String
	 */
	public ArchiveFileExportOperation(List<? extends IResource> resources, String filename) {
		super();

		// Eliminate redundancies in list of resources being exported
		Iterator<? extends IResource> elementsEnum = resources.iterator();
		while (elementsEnum.hasNext()) {
			IResource currentResource = elementsEnum.next();
			if (isDescendent(resources, currentResource)) {
				elementsEnum.remove(); //Removes currentResource;
			}
		}

		resourcesToExport = resources;
		destinationFilename = filename;
	}

	/**
	 *  Create an instance of this class.  Use this constructor if you wish
	 *  to recursively export a single resource.
	 *
	 *  @param res org.eclipse.core.resources.IResource;
	 *  @param filename java.lang.String
	 */
	public ArchiveFileExportOperation(IResource res, String filename) {
		super();
		resource = res;
		destinationFilename = filename;
	}

	/**
	 *  Create an instance of this class.  Use this constructor if you wish to
	 *  export specific resources with a common parent resource (affects container
	 *  directory creation)
	 *
	 *  @param res org.eclipse.core.resources.IResource
	 *  @param resources java.util.Vector
	 *  @param filename java.lang.String
	 */
	public ArchiveFileExportOperation(IResource res, List<IResource> resources, String filename) {
		this(res, filename);
		resourcesToExport = resources;
	}

	/**
	 * Add a new entry to the error table with the passed information
	 */
	protected void addError(String message, Throwable e) {
		errorTable.add(new Status(IStatus.ERROR,
				IDEWorkbenchPlugin.IDE_WORKBENCH, 0, message, e));
	}

	/**
	 *  Answer the total number of file resources that exist at or below self
	 *  in the resources hierarchy.
	 *
	 *  @return int
	 *  @param checkResource org.eclipse.core.resources.IResource
	 */
	protected int countChildrenOf(IResource checkResource) throws CoreException {
		if (checkResource.getType() == IResource.FILE) {
			return 1;
		}

		int count = 0;
		if (checkResource.isAccessible()) {
			for (IResource child : ((IContainer) checkResource).members()) {
				count += countChildrenOf(child);
			}
		}

		return count;
	}

	/**
	 *	Answer a boolean indicating the number of file resources that were
	 *	specified for export
	 *
	 *	@return int
	 */
	protected int countSelectedResources() throws CoreException {
		int result = 0;
		Iterator<? extends IResource> resources = resourcesToExport.iterator();
		while (resources.hasNext()) {
			result += countChildrenOf(resources.next());
		}

		return result;
	}

	/**
	 *  Export the passed resource to the destination .zip. Export with
	 * no path leadup
	 *
	 *  @param exportResource org.eclipse.core.resources.IResource
	 */
	protected void exportResource(IResource exportResource)
			throws InterruptedException {
		exportResource(exportResource, 1);
	}

	/**
	 * Creates and returns the string that should be used as the name of the entry in the archive.
	 * @param exportResource the resource to export
	 * @param leadupDepth the number of resource levels to be included in the path including the resourse itself.
	 */
	private String createDestinationName(int leadupDepth, IResource exportResource) {
		IPath fullPath = exportResource.getFullPath();
		if (createLeadupStructure) {
			return fullPath.makeRelative().toString();
		}
		return fullPath.removeFirstSegments(fullPath.segmentCount() - leadupDepth).makeRelative().toString();
	}

	/**
	 *  Export the passed resource to the destination .zip
	 *
	 *  @param exportResource org.eclipse.core.resources.IResource
	 *  @param leadupDepth the number of resource levels to be included in
	 *                     the path including the resourse itself.
	 */
	protected void exportResource(IResource exportResource, int leadupDepth)
			throws InterruptedException {
		if (!exportResource.isAccessible() || (!resolveLinks && exportResource.isLinked())) {
			return;
		}

		if (exportResource.getType() == IResource.FILE) {
			String destinationName = createDestinationName(leadupDepth, exportResource);
			monitor.subTask(destinationName);

			try {
				exporter.write((IFile) exportResource, destinationName);
			} catch (IOException | CoreException e) {
				addError(NLS.bind(DataTransferMessages.DataTransfer_errorExporting, exportResource.getFullPath().makeRelative(), e.getMessage()), e);
			}

			monitor.worked(1);
			ModalContext.checkCanceled(monitor);
		} else {
			IResource[] children = null;

			try {
				children = ((IContainer) exportResource).members();
			} catch (CoreException e) {
				// this should never happen because an #isAccessible check is done before #members is invoked
				addError(NLS.bind(DataTransferMessages.DataTransfer_errorExporting, exportResource.getFullPath()), e);
			}

			if (children.length == 0) { // create an entry for empty containers, see bug 278402
				String destinationName = createDestinationName(leadupDepth, exportResource);
				try {
					exporter.write((IContainer) exportResource, destinationName + IPath.SEPARATOR);
				} catch (IOException e) {
					addError(NLS.bind(DataTransferMessages.DataTransfer_errorExporting, exportResource.getFullPath().makeRelative(), e.getMessage()), e);
				}
			}

			for (IResource child : children) {
				exportResource(child, leadupDepth + 1);
			}

		}
	}

	/**
	 *	Export the resources contained in the previously-defined
	 *	resourcesToExport collection
	 */
	protected void exportSpecifiedResources() throws InterruptedException {
		Iterator<? extends IResource> resources = resourcesToExport.iterator();

		while (resources.hasNext()) {
			IResource currentResource = resources.next();
			exportResource(currentResource);
		}
	}

	/**
	 * Returns the status of the operation.
	 * If there were any errors, the result is a status object containing
	 * individual status objects for each error.
	 * If there were no errors, the result is a status object with error code <code>OK</code>.
	 *
	 * @return the status
	 */
	public IStatus getStatus() {
		IStatus[] errors = new IStatus[errorTable.size()];
		errorTable.toArray(errors);
		return new MultiStatus(
				IDEWorkbenchPlugin.IDE_WORKBENCH,
				IStatus.OK,
				errors,
				DataTransferMessages.FileSystemExportOperation_problemsExporting,
				null);
	}

	/**
	 *	Initialize this operation
	 *
	 *	@exception java.io.IOException
	 */
	protected void initialize() throws IOException {
		if(useTarFormat) {
			exporter = new TarFileExporter(destinationFilename, useCompression, resolveLinks);
		} else {
			exporter = new ZipFileExporter(destinationFilename, useCompression, resolveLinks);
		}
	}

	/**
	 *  Answer a boolean indicating whether the passed child is a descendent
	 *  of one or more members of the passed resources collection
	 *
	 *  @return boolean
	 *  @param resources java.util.Vector
	 *  @param child org.eclipse.core.resources.IResource
	 */
	protected boolean isDescendent(List<? extends IResource> resources, IResource child) {
		if (child.getType() == IResource.PROJECT) {
			return false;
		}

		IResource parent = child.getParent();
		if (resources.contains(parent)) {
			return true;
		}

		return isDescendent(resources, parent);
	}

	/**
	 *	Export the resources that were previously specified for export
	 *	(or if a single resource was specified then export it recursively)
	 */
	@Override
	public void run(IProgressMonitor progressMonitor)
			throws InvocationTargetException, InterruptedException {
		this.monitor = progressMonitor;

		try {
			initialize();
		} catch (IOException e) {
			throw new InvocationTargetException(e, NLS.bind(DataTransferMessages.ZipExport_cannotOpen, e.getMessage()));
		}

		try {
			// ie.- a single resource for recursive export was specified
			int totalWork = IProgressMonitor.UNKNOWN;
			try {
				if (resourcesToExport == null) {
					totalWork = countChildrenOf(resource);
				} else {
					totalWork = countSelectedResources();
				}
			} catch (CoreException e) {
				// Should not happen
			}
			monitor.beginTask(DataTransferMessages.DataTransfer_exportingTitle, totalWork);
			if (resourcesToExport == null) {
				exportResource(resource);
			} else {
				// ie.- a list of specific resources to export was specified
				exportSpecifiedResources();
			}

			try {
				exporter.finished();
			} catch (IOException e) {
				throw new InvocationTargetException(
						e,
						NLS.bind(DataTransferMessages.ZipExport_cannotClose, e.getMessage()));
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 *	Set this boolean indicating whether each exported resource's path should
	 *	include containment hierarchies as dictated by its parents
	 *
	 *	@param value boolean
	 */
	public void setCreateLeadupStructure(boolean value) {
		createLeadupStructure = value;
	}

	/**
	 *	Set this boolean indicating whether exported resources should
	 *	be compressed (as opposed to simply being stored)
	 *
	 *	@param value boolean
	 */
	public void setUseCompression(boolean value) {
		useCompression = value;
	}

	/**
	 * Set this boolean indicating whether the file should be output
	 * in tar.gz format rather than .zip format.
	 *
	 * @param value boolean
	 */
	public void setUseTarFormat(boolean value) {
		useTarFormat = value;
	}

	/**
	 * Set this boolean indicating whether linked resources should be resolved
	 * and exported (as opposed to simply ignored)
	 *
	 * @param value
	 *            boolean
	 */
	public void setIncludeLinkedResources(boolean value) {
		resolveLinks = value;
	}
}
