/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codehaus.plexus.archiver.dir;

import javax.inject.Named;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.List;

import org.codehaus.plexus.archiver.AbstractArchiver;
import org.codehaus.plexus.archiver.ArchiveEntry;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.ResourceIterator;
import org.codehaus.plexus.archiver.exceptions.EmptyArchiveException;
import org.codehaus.plexus.archiver.util.ArchiveEntryUtils;
import org.codehaus.plexus.archiver.util.ResourceUtils;
import org.codehaus.plexus.components.io.attributes.SymlinkUtils;
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;

/**
 * A plexus archiver implementation that stores the files to archive in a directory.
 */
@Named("dir")
public class DirectoryArchiver extends AbstractArchiver {

    private final List<Runnable> directoryChmods = new ArrayList<>();
    private long filesCopied;

    /**
     * Default constructor.
     */
    public DirectoryArchiver() {
        // preserver default behavior
        setForced(false);
    }

    public void resetArchiver() throws IOException {
        cleanUp();
    }

    @Override
    public void execute() throws ArchiverException, IOException {
        // Most of this method was copied from org.codehaus.plexus.archiver.tar.TarArchiver
        // and modified to store files in a directory, not a tar archive.
        final ResourceIterator iter = getResources();
        if (!iter.hasNext()) {
            throw new EmptyArchiveException("archive cannot be empty");
        }

        final File destDirectory = getDestFile();
        if (destDirectory == null) {
            throw new ArchiverException("You must set the destination directory.");
        }
        if (destDirectory.exists() && !destDirectory.isDirectory()) {
            throw new ArchiverException(destDirectory + " is not a directory.");
        }
        if (destDirectory.exists() && !destDirectory.canWrite()) {
            throw new ArchiverException(destDirectory + " is not writable.");
        }

        getLogger().info("Copying files to {}", destDirectory.getAbsolutePath());

        try {
            while (iter.hasNext()) {
                final ArchiveEntry f = iter.next();
                // Check if we don't add directory file in itself
                if (ResourceUtils.isSame(f.getResource(), destDirectory)) {
                    throw new ArchiverException("The destination directory cannot include itself.");
                }
                String fileName = f.getName();
                final String destDir = destDirectory.getCanonicalPath();
                fileName = destDir + File.separator + fileName;
                PlexusIoResource resource = f.getResource();
                if (resource instanceof SymlinkDestinationSupplier) {
                    String dest = ((SymlinkDestinationSupplier) resource).getSymlinkDestination();
                    File target = new File(dest);
                    File symlink = new File(fileName);
                    makeParentDirectories(symlink);
                    SymlinkUtils.createSymbolicLink(symlink, target);
                } else {
                    copyFile(f, fileName);
                }
            }

            directoryChmods.forEach(Runnable::run);
            directoryChmods.clear();

            if (filesCopied > 0) {
                getLogger()
                        .info(
                                "{} file{} copied to {}",
                                filesCopied,
                                filesCopied > 0 ? "s" : "",
                                destDirectory.getAbsolutePath());
            } else {
                getLogger().info("All files are uptodate in {}", destDirectory.getAbsolutePath());
            }

            filesCopied = 0;

        } catch (final IOException ioe) {
            final String message = "Problem copying files : " + ioe.getMessage();
            throw new ArchiverException(message, ioe);
        }
    }

    /**
     * Copies the specified file to the specified path, creating any ancestor directory structure as necessary.
     *
     * @param entry The file to copy (IOException will be thrown if this does not exist)
     * @param vPath The fully qualified path to copy the file to.
     *
     * @throws ArchiverException If there is a problem creating the directory structure
     * @throws IOException If there is a problem copying the file
     */
    protected void copyFile(final ArchiveEntry entry, final String vPath) throws ArchiverException, IOException {
        // don't add "" to the archive
        if (vPath.length() <= 0) {
            return;
        }

        final PlexusIoResource in = entry.getResource();
        final File outFile = new File(vPath);

        final long inLastModified = in.getLastModified();

        if (!isForced()) {
            final long outLastModified = outFile.lastModified();
            if (ResourceUtils.isUptodate(inLastModified, outLastModified)) {
                return;
            }
        }

        if (!in.isDirectory()) {
            makeParentDirectories(outFile);
            try (InputStream input = entry.getInputStream()) {
                ResourceUtils.copyFile(input, outFile);
            }
            filesCopied++;
            setFileModes(entry, outFile, inLastModified);
        } else { // file is a directory
            if (outFile.exists()) {
                if (!outFile.isDirectory()) {
                    // should we just delete the file and replace it with a directory?
                    // throw an exception, let the user delete the file manually.
                    throw new ArchiverException("Expected directory and found file at copy destination of "
                            + in.getName() + " to " + outFile);
                }
            } else if (!outFile.mkdirs()) {
                // Failure, unable to create specified directory for some unknown reason.
                throw new ArchiverException("Unable to create directory or parent directory of " + outFile);
            }

            directoryChmods.add(() -> {
                try {
                    setFileModes(entry, outFile, inLastModified);
                } catch (IOException e) {
                    throw new ArchiverException("Failed setting file attributes", e);
                }
            });
        }
    }

    private static void makeParentDirectories(File file) {
        if (!file.getParentFile().exists()) {
            // create the parent directory...
            if (!file.getParentFile().mkdirs()) {
                // Failure, unable to create specified directory for some unknown reason.
                throw new ArchiverException("Unable to create directory or parent directory of " + file);
            }
        }
    }

    private void setFileModes(ArchiveEntry entry, File outFile, long inLastModified) throws IOException {
        if (!isIgnorePermissions()) {
            ArchiveEntryUtils.chmod(outFile, entry.getMode());
        }

        if (getLastModifiedTime() == null) {
            FileTime fromMillis = FileTime.fromMillis(
                    inLastModified == PlexusIoResource.UNKNOWN_MODIFICATION_DATE
                            ? System.currentTimeMillis()
                            : inLastModified);
            Files.setLastModifiedTime(outFile.toPath(), fromMillis);
        } else {
            Files.setLastModifiedTime(outFile.toPath(), getLastModifiedTime());
        }
    }

    @Override
    protected void cleanUp() throws IOException {
        super.cleanUp();
        setIncludeEmptyDirs(false);
        setIncludeEmptyDirs(true);
    }

    @Override
    protected void close() throws IOException {}

    @Override
    protected String getArchiveType() {
        return "directory";
    }

    @Override
    public boolean isSupportingForced() {
        return true;
    }
}
