/*******************************************************************************
 * Copyright (c) 2000, 2015 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.File;
import java.io.IOException;
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.PlatformUI;
import org.eclipse.ui.dialogs.IOverwriteQuery;


/**
 *	Operation for exporting the contents of a resource to the local file system.
 */
public class FileSystemExportOperation implements IRunnableWithProgress {
	private IPath path;

	private IProgressMonitor monitor;

	private FileSystemExporter exporter = new FileSystemExporter();

	private List resourcesToExport;

	private IOverwriteQuery overwriteCallback;

	private IResource resource;

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

	//The constants for the overwrite 3 state
	private static final int OVERWRITE_NOT_SET = 0;

	private static final int OVERWRITE_NONE = 1;

	private static final int OVERWRITE_ALL = 2;

	private int overwriteState = OVERWRITE_NOT_SET;

	private boolean createLeadupStructure = true;

	private boolean createContainerDirectories = true;

	private boolean resolveLinks = false;

	/**
	 *  Create an instance of this class.  Use this constructor if you wish to
	 *  recursively export a single resource
	 */
	public FileSystemExportOperation(IResource res, String destinationPath,
			IOverwriteQuery overwriteImplementor) {
		super();
		resource = res;
		path = IPath.fromOSString(destinationPath);
		overwriteCallback = overwriteImplementor;
	}

	/**
	 *  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)
	 */
	public FileSystemExportOperation(IResource res, List resources,
			String destinationPath, IOverwriteQuery overwriteImplementor) {
		this(res, destinationPath, overwriteImplementor);
		resourcesToExport = resources;
	}

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

		int count = 0;
		if (parentResource.isAccessible()) {
			for (IResource child : ((IContainer) parentResource).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 resources = resourcesToExport.iterator();

		while (resources.hasNext()) {
			result += countChildrenOf((IResource) resources.next());
		}

		return result;
	}

	/**
	 *  Create the directories required for exporting the passed resource,
	 *  based upon its container hierarchy
	 *
	 *  @param childResource org.eclipse.core.resources.IResource
	 */
	protected void createLeadupDirectoriesFor(IResource childResource) {
		IPath resourcePath = childResource.getFullPath().removeLastSegments(1);

		for (int i = 0; i < resourcePath.segmentCount(); i++) {
			path = path.append(resourcePath.segment(i));
			exporter.createFolder(path);
		}
	}

	/**
	 *	Recursively export the previously-specified resource
	 */
	protected void exportAllResources() throws InterruptedException {
		if (resource.getType() == IResource.FILE) {
			exportFile((IFile) resource, path);
		} else {
			try {
				exportChildren(((IContainer) resource).members(), path);
			} catch (CoreException e) {
				// not safe to show a dialog
				// should never happen because the file system export wizard ensures that the
				// single resource chosen for export is both existent and accessible
				errorTable.add(e.getStatus());
			}
		}
	}

	/**
	 *	Export all of the resources contained in the passed collection
	 *
	 *	@param children java.util.Enumeration
	 *	@param currentPath IPath
	 */
	protected void exportChildren(IResource[] children, IPath currentPath)
			throws InterruptedException {
		for (IResource child : children) {
			if (!child.isAccessible() || (!resolveLinks && child.isLinked())) {
				continue;
			}

			if (child.getType() == IResource.FILE) {
				exportFile((IFile) child, currentPath);
			} else {
				IPath destination = currentPath.append(child.getName());
				exporter.createFolder(destination);
				try {
					exportChildren(((IContainer) child).members(), destination);
				} catch (CoreException e) {
					// not safe to show a dialog
					// should never happen because:
					// i. this method is called recursively iterating over the result of #members,
					//		which only answers existing children
					// ii. there is an #isAccessible check done before #members is invoked
					errorTable.add(e.getStatus());
				}
			}
		}
	}

	/**
	 *  Export the passed file to the specified location
	 *
	 *  @param file org.eclipse.core.resources.IFile
	 *  @param location org.eclipse.core.runtime.IPath
	 */
	protected void exportFile(IFile file, IPath location)
			throws InterruptedException {
		IPath fullPath = location.append(file.getName());
		monitor.subTask(file.getFullPath().toString());
		String properPathString = fullPath.toOSString();
		File targetFile = new File(properPathString);

		if (targetFile.exists()) {
			if (!targetFile.canWrite()) {
				errorTable.add(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID,
						0, NLS.bind(DataTransferMessages.DataTransfer_cannotOverwrite, targetFile.getAbsolutePath()),
						null));
				monitor.worked(1);
				return;
			}

			if (overwriteState == OVERWRITE_NONE) {
				return;
			}

			if (overwriteState != OVERWRITE_ALL) {
				String overwriteAnswer = overwriteCallback
						.queryOverwrite(properPathString);

				if (overwriteAnswer.equals(IOverwriteQuery.CANCEL)) {
					throw new InterruptedException();
				}

				if (overwriteAnswer.equals(IOverwriteQuery.NO)) {
					monitor.worked(1);
					return;
				}

				if (overwriteAnswer.equals(IOverwriteQuery.NO_ALL)) {
					monitor.worked(1);
					overwriteState = OVERWRITE_NONE;
					return;
				}

				if (overwriteAnswer.equals(IOverwriteQuery.ALL)) {
					overwriteState = OVERWRITE_ALL;
				}
			}
		}

		try {
			exporter.write(file, fullPath);
		} catch (IOException | CoreException e) {
			errorTable.add(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, 0,
					NLS.bind(DataTransferMessages.DataTransfer_errorExporting, fullPath, e.getMessage()), e));
		}

		monitor.worked(1);
		ModalContext.checkCanceled(monitor);
	}

	/**
	 *	Export the resources contained in the previously-defined
	 *	resourcesToExport collection
	 */
	protected void exportSpecifiedResources() throws InterruptedException {
		Iterator resources = resourcesToExport.iterator();
		IPath initPath = (IPath) path.clone();

		while (resources.hasNext()) {
			IResource currentResource = (IResource) resources.next();
			if (!currentResource.isAccessible() || (!resolveLinks && currentResource.isLinked())) {
				continue;
			}

			path = initPath;

			if (resource == null) {
				// No root resource specified and creation of containment directories
				// is required.  Create containers from depth 2 onwards (ie.- project's
				// child inclusive) for each resource being exported.
				if (createLeadupStructure) {
					createLeadupDirectoriesFor(currentResource);
				}

			} else {
				// Root resource specified.  Must create containment directories
				// from this point onwards for each resource being exported
				IPath containersToCreate = currentResource.getFullPath()
						.removeFirstSegments(
								resource.getFullPath().segmentCount())
						.removeLastSegments(1);

				for (int i = 0; i < containersToCreate.segmentCount(); i++) {
					path = path.append(containersToCreate.segment(i));
					exporter.createFolder(path);
				}
			}

			if (currentResource.getType() == IResource.FILE) {
				exportFile((IFile) currentResource, path);
			} else {
				if (createContainerDirectories) {
					path = path.append(currentResource.getName());
					exporter.createFolder(path);
				}

				try {
					exportChildren(((IContainer) currentResource).members(),
							path);
				} catch (CoreException e) {
					// should never happen because #isAccessible is called before #members is invoked,
					// which implicitly does an existence check
					errorTable.add(e.getStatus());
				}
			}
		}
	}

	/**
	 * Returns the status of the export 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(
				PlatformUI.PLUGIN_ID,
				IStatus.OK,
				errors,
				DataTransferMessages.FileSystemExportOperation_problemsExporting,
				null);
	}

	/**
	 *  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.List
	 *  @param child org.eclipse.core.resources.IResource
	 */
	protected boolean isDescendent(List 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 InterruptedException {
		this.monitor = progressMonitor;

		if (resource != null) {
			if (createLeadupStructure) {
				createLeadupDirectoriesFor(resource);
			}

			if (createContainerDirectories
					&& resource.getType() != IResource.FILE) {
				// ensure it's a container
				path = path.append(resource.getName());
				exporter.createFolder(path);
			}
		}

		try {
			int totalWork = IProgressMonitor.UNKNOWN;
			try {
				if (resourcesToExport == null) {
					totalWork = countChildrenOf(resource);
				} else {
					totalWork = countSelectedResources();
				}
			} catch (CoreException e) {
				// Should not happen
				errorTable.add(e.getStatus());
			}
			monitor.beginTask(DataTransferMessages.DataTransfer_exportingTitle, totalWork);
			if (resourcesToExport == null) {
				exportAllResources();
			} else {
				exportSpecifiedResources();
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 *	Set this boolean indicating whether a directory should be created for
	 *	Folder resources that are explicitly passed for export
	 *
	 *	@param value boolean
	 */
	public void setCreateContainerDirectories(boolean value) {
		createContainerDirectories = value;
	}

	/**
	 *	Set this boolean indicating whether each exported resource's complete 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 automatically
	 *	overwrite existing files when a conflict occurs. If not
	 *	query the user.
	 *
	 *	@param value boolean
	 */
	public void setOverwriteFiles(boolean value) {
		if (value) {
			overwriteState = OVERWRITE_ALL;
		}
	}

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