/*******************************************************************************
 * Copyright (c) 2014 Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is 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:
 *   Red Hat, Inc. - initial API and implementation
 ******************************************************************************/
package org.jboss.tools.windup.ui.internal.archiver;

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

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.Path;
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.jboss.tools.windup.core.WindupCorePlugin;

/**
 * <p>
 * Operation for exporting a {@link File} and its children to a new .zip or .tar.gz file.
 * </p>
 * 
 * <p>
 * This class is based off of the {@link org.jboss.tools.windup.core.internal.archiver.ui.internal.wizards.datatransfer.ArchiveFileExportOperation}
 * class used to archive {@link org.eclipse.core.resources.IResource}s, this modified version of the operation is used to export {@link File}s.
 * </p>
 * 
 * @see org.jboss.tools.windup.core.internal.archiver.ui.internal.wizards.datatransfer.ArchiveFileExportOperation
 */
public class ArchiveFileExportOperation implements IRunnableWithProgress
{
    private IFileExporter exporter;

    private String destinationArchiveName;

    private IProgressMonitor monitor;

    private List<File> filesToExport;

    private File rootResource;

    private IPath rootArchiveDirectory;

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

    private boolean useCompression = true;

    private boolean useTarFormat = false;

    /**
     * <p>
     * Create an instance of this class. Use this constructor if you wish to export specific resources without a common parent resource.
     * </p>
     * 
     * @param files Files to export
     * @param archiveName Name of archive to export the files too
     */
    public ArchiveFileExportOperation(List<File> files, String archiveName)
    {
        super();

        // Eliminate redundancies in list of resources being exported
        Iterator<File> filesIter = files.iterator();
        while (filesIter.hasNext())
        {
            File file = filesIter.next();
            if (isDescendent(files, file))
            {
                filesIter.remove(); // Removes currentResource;
            }
        }

        this.filesToExport = files;
        this.destinationArchiveName = archiveName;
        this.rootArchiveDirectory = null;
    }

    /**
     * <p>
     * Create an instance of this class. Use this constructor if you wish to recursively export a single resource and all of its descendants.
     * </p>
     * 
     * @param file The root file to recursively export to an archive.
     * @param archiveName Name of archive to export the file and its descendants to
     */
    public ArchiveFileExportOperation(File file, String archiveName)
    {
        super();
        this.rootResource = file;
        this.destinationArchiveName = archiveName;
        this.rootArchiveDirectory = null;
    }

    /**
     * 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 parentFile Parent file of all of the files to export to the archive
     * @param filesToExport {@link List} of files to export to the archive using the given parent file as the root file
     * @param archiveName Name of the archive to export the files to
     */
    public ArchiveFileExportOperation(File parentFile, List<File> filesToExport,
                String archiveName)
    {
        this(parentFile, archiveName);
        this.filesToExport = filesToExport;
    }

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

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

    /**
     * <p>
     * Used to set an optional root directory for all archived files to be placed under in the archive. If specified the archive will have one root
     * directory with this name and all added {@link File}s to the archive will go under this one root directory.
     * </p>
     * 
     * @param rootArchiveDirectoryName name of the directory to put at the root of the archive
     */
    public void setRootArchiveDirectoryName(String rootArchiveDirectoryName)
    {
        this.rootArchiveDirectory = new Path(rootArchiveDirectoryName);
    }

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

        this.monitor = progressMonitor;

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

        try
        {
            // determine the amount of work to be done
            int totalWork = IProgressMonitor.UNKNOWN;
            if (this.filesToExport == null)
            {
                totalWork = countChildrenOf(this.rootResource);
            }
            else
            {
                totalWork = countSelectedFilesToExport();
            }

            this.monitor.beginTask(Messages.ArchiveFileExport_exportingTitle, totalWork);

            if (this.filesToExport == null)
            {
                exportResource(this.rootResource);
            }
            else
            {
                // ie.- a list of specific resources to export was specified
                exportSpecifiedResources();
            }

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

    /**
     * <p>
     * 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>.
     * </p>
     * 
     * @return the status of the operation.
     */
    public IStatus getStatus()
    {
        IStatus[] errors = new IStatus[this.errorTable.size()];
        this.errorTable.toArray(errors);
        return new MultiStatus(
                    WindupCorePlugin.PLUGIN_ID,
                    IStatus.OK,
                    errors,
                    Messages.ArchiveFileExport_problemsExporting,
                    null);
    }

    /**
     * <p>
     * Initialize this operation.
     * </p>
     * 
     * @throws java.io.IOException this can happen when performing file IO
     */
    private void initialize() throws IOException
    {
        if (this.useTarFormat)
        {
            this.exporter = new TarFileExporter(this.destinationArchiveName, this.useCompression);
        }
        else
        {
            this.exporter = new ZipFileExporter(this.destinationArchiveName, this.useCompression);
        }
    }

    /**
     * Add a new entry to the error table with the passed information
     */
    private void addError(String message, Throwable e)
    {
        this.errorTable.add(new Status(IStatus.ERROR,
                    WindupCorePlugin.PLUGIN_ID, 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
     */
    private int countChildrenOf(File parentFile)
    {
        int count = 0;

        // count all of the files to archive without using recursion
        LinkedList<File> filesToCount = new LinkedList<File>();
        filesToCount.add(parentFile);
        while (!filesToCount.isEmpty())
        {
            File fileToCount = filesToCount.pop();

            if (fileToCount.isFile())
            {
                ++count;
            }
            else
            {
                filesToCount.addAll(Arrays.asList(fileToCount.listFiles()));
            }

        }

        return count;
    }

    /**
     * Answer a boolean indicating the number of file resources that were specified for export
     * 
     * @return number of files to export
     */
    private int countSelectedFilesToExport()
    {
        int result = 0;
        Iterator<File> resources = filesToExport.iterator();
        while (resources.hasNext())
        {
            result += countChildrenOf(resources.next());
        }

        return result;
    }

    /**
     * Creates and returns the string that should be used as the name of the entry in the archive.
     * 
     * @param fileToExport {@link File} to to export
     * @param leadupDepth the number of resource levels to be included in the path including the resource itself.
     */
    private String createDestinationName(File fileToExport, IPath relativeTo)
    {
        IPath fullPath = new Path(fileToExport.getAbsolutePath());
        IPath archiveRelativePath = fullPath.makeRelativeTo(relativeTo);

        // if a root directory is specified then all files should be nested under it
        if (this.rootArchiveDirectory != null)
        {
            archiveRelativePath = this.rootArchiveDirectory.append(archiveRelativePath);
        }

        return archiveRelativePath.toString();
    }

    /**
     * <p>
     * Export the passed resource to the destination .zip. Export with no path leadup
     * </p>
     * 
     * @param fileToExport {@link File} to export.
     */
    private void exportResource(File fileToExport)
                throws InterruptedException
    {

        exportResource(fileToExport, new Path(fileToExport.getParent()));
    }

    /**
     * <p>
     * Export the passed resource to the destination archive
     * </p>
     * 
     * @param fileToExport {@link File} to export
     * @param leadupDepth the number of directory levels to be included in the path including the {@link File} itself.
     */
    private void exportResource(File fileToExport, IPath relativeTo)
                throws InterruptedException
    {

        if (fileToExport.exists())
        {
            /*
             * if the File to export is a file export it else if File is a directory recursivly export all of it's childre
             */
            if (fileToExport.isFile())
            {
                String destinationName = createDestinationName(fileToExport, relativeTo);
                this.monitor.subTask(destinationName);

                try
                {
                    this.exporter.write(fileToExport, destinationName);
                }
                catch (IOException e)
                {
                    addError(NLS.bind(
                                Messages.ArchiveFileExport_errorExporting,
                                fileToExport.getPath(),
                                e.getMessage()), e);
                }

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

                // create an entry for empty containers
                File[] children = fileToExport.listFiles();
                if (children.length == 0)
                {
                    String destinationName = createDestinationName(fileToExport, relativeTo);
                    try
                    {
                        this.exporter.write(fileToExport, destinationName + IPath.SEPARATOR);
                    }
                    catch (IOException e)
                    {
                        addError(NLS.bind(
                                    Messages.ArchiveFileExport_errorExporting,
                                    fileToExport.getPath(),
                                    e.getMessage()), e);
                    }
                }

                for (int i = 0; i < children.length; i++)
                {
                    exportResource(children[i], relativeTo);
                }
            }
        }
    }

    /**
     * <p>
     * Export the files contained in the previously-defined filesToExport collection
     * </p>
     */
    private void exportSpecifiedResources() throws InterruptedException
    {
        Iterator<File> resources = filesToExport.iterator();

        while (resources.hasNext())
        {
            exportResource(resources.next());
        }
    }

    /**
     * <p>
     * Answer a boolean indicating whether the passed child is a descendant of one or more members of the passed resources collection
     * </p>
     * 
     * @param parentFiles Check if the given child is a child of any of these parent files
     * @param child Check if this child is a child of any of the given parent files
     * 
     * @return <code>true</code> if the given file is a descendant of any of the given parent files, <code>false</code> otherwise
     */
    private boolean isDescendent(List<File> parentFiles, File child)
    {
        if (child != null)
        {
            File parent = child.getParentFile();
            if (parentFiles.contains(parent))
            {
                return true;
            }

            return isDescendent(parentFiles, parent);
        }
        else
        {
            return false;
        }
    }
}
