/**
 *
 * Copyright 2004 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.tar;

import javax.inject.Named;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
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.ResourceUtils;
import org.codehaus.plexus.archiver.util.Streams;
import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributes;
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.iq80.snappy.SnappyOutputStream;

import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;

/**
 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
 */
@Named("tar")
public class TarArchiver extends AbstractArchiver {

    /**
     * Indicates whether the user has been warned about long files already.
     */
    private boolean longWarningGiven = false;

    private TarLongFileMode longFileMode = TarLongFileMode.warn;

    private TarCompressionMethod compression = TarCompressionMethod.none;

    private final TarOptions options = new TarOptions();

    private TarArchiveOutputStream tOut;

    /**
     * Set how to handle long files, those with a path&gt;100 chars.
     * Optional, default=warn.
     * <p>
     * Allowable values are </p>
     * <ul>
     * <li> truncate - paths are truncated to the maximum length </li>
     * <li> fail - paths greater than the maximum cause a build exception </li>
     * <li> warn - paths greater than the maximum cause a warning and GNU is used </li>
     * <li> gnu - GNU extensions are used for any paths greater than the maximum. </li>
     * <li> posix - posix extensions are used for any paths greater than the maximum. </li>
     * <li> posixwarn - posix extensions are used (with warning) for any paths greater than the maximum. </li>
     * <li> omit - paths greater than the maximum are omitted from the archive </li>
     * </ul>
     *
     * @param mode the mode to handle long file names.
     */
    public void setLongfile(TarLongFileMode mode) {
        this.longFileMode = mode;
    }

    /**
     * Set compression method.
     * Allowable values are
     * <ul>
     * <li> none - no compression
     * <li> gzip - Gzip compression
     * <li> bzip2 - Bzip2 compression
     * </ul>
     *
     * @param mode the compression method.
     */
    public void setCompression(TarCompressionMethod mode) {
        this.compression = mode;
    }

    @Override
    protected void execute() throws ArchiverException, IOException {
        if (!checkForced()) {
            return;
        }

        ResourceIterator iter = getResources();
        if (!iter.hasNext()) {
            throw new EmptyArchiveException("archive cannot be empty");
        }

        File tarFile = getDestFile();

        if (tarFile == null) {
            throw new ArchiverException("You must set the destination tar file.");
        }
        if (tarFile.exists() && !tarFile.isFile()) {
            throw new ArchiverException(tarFile + " isn't a file.");
        }
        if (tarFile.exists() && !tarFile.canWrite()) {
            throw new ArchiverException(tarFile + " is read-only.");
        }

        getLogger().info("Building tar: " + tarFile.getAbsolutePath());

        try {
            tOut = new TarArchiveOutputStream(compress(compression, Files.newOutputStream(tarFile.toPath())), "UTF8");
            if (longFileMode.isTruncateMode()) {
                tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_TRUNCATE);
            } else if (longFileMode.isPosixMode() || longFileMode.isPosixWarnMode()) {
                tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
                // Todo: Patch 2.5.1   for this fix. Also make closeable fix on 2.5.1
                tOut.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
            } else if (longFileMode.isFailMode() || longFileMode.isOmitMode()) {
                tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_ERROR);
            } else {
                // warn or GNU
                tOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
            }

            longWarningGiven = false;
            while (iter.hasNext()) {
                ArchiveEntry entry = iter.next();
                // Check if we don't add tar file in itself
                if (ResourceUtils.isSame(entry.getResource(), tarFile)) {
                    throw new ArchiverException("A tar file cannot include itself.");
                }
                String fileName = entry.getName();
                String name = StringUtils.replace(fileName, File.separatorChar, '/');

                tarFile(entry, tOut, name);
            }

            tOut.close();
        } finally {
            IOUtil.close(tOut);
        }
    }

    /**
     * tar a file
     *
     * @param entry the file to tar
     * @param tOut the output stream
     * @param vPath the path name of the file to tar
     *
     * @throws IOException on error
     */
    protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String vPath)
            throws ArchiverException, IOException {

        // don't add "" to the archive
        if (vPath.length() <= 0) {
            return;
        }

        if (entry.getResource().isDirectory() && !vPath.endsWith("/")) {
            vPath += "/";
        }

        if (vPath.startsWith("/") && !options.getPreserveLeadingSlashes()) {
            int l = vPath.length();
            if (l <= 1) {
                // we would end up adding "" to the archive
                return;
            }
            vPath = vPath.substring(1, l);
        }

        int pathLength = vPath.length();
        InputStream fIn = null;

        try {
            TarArchiveEntry te;
            if (!longFileMode.isGnuMode()
                    && pathLength >= org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN) {
                int maxPosixPathLen = org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
                        + org.apache.commons.compress.archivers.tar.TarConstants.PREFIXLEN;
                if (longFileMode.isPosixMode()) {
                } else if (longFileMode.isPosixWarnMode()) {
                    if (pathLength > maxPosixPathLen) {
                        getLogger().warn("Entry: " + vPath + " longer than " + maxPosixPathLen + " characters.");
                        if (!longWarningGiven) {
                            getLogger()
                                    .warn("Resulting tar file can only be processed "
                                            + "successfully by GNU compatible tar commands");
                            longWarningGiven = true;
                        }
                    }
                } else if (longFileMode.isOmitMode()) {
                    getLogger().info("Omitting: " + vPath);
                    return;
                } else if (longFileMode.isWarnMode()) {
                    getLogger()
                            .warn("Entry: " + vPath + " longer than "
                                    + org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
                                    + " characters.");
                    if (!longWarningGiven) {
                        getLogger()
                                .warn("Resulting tar file can only be processed "
                                        + "successfully by GNU compatible tar commands");
                        longWarningGiven = true;
                    }
                } else if (longFileMode.isFailMode()) {
                    throw new ArchiverException("Entry: " + vPath + " longer than "
                            + org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
                            + " characters.");
                } else {
                    throw new IllegalStateException("Non gnu mode should never get here?");
                }
            }

            if (entry.getType() == ArchiveEntry.SYMLINK) {
                final SymlinkDestinationSupplier plexusIoSymlinkResource =
                        (SymlinkDestinationSupplier) entry.getResource();

                te = new TarArchiveEntry(vPath, TarArchiveEntry.LF_SYMLINK);
                te.setLinkName(plexusIoSymlinkResource.getSymlinkDestination());
            } else {
                te = new TarArchiveEntry(vPath);
            }

            if (getLastModifiedTime() == null) {
                long teLastModified = entry.getResource().getLastModified();
                te.setModTime(
                        teLastModified == PlexusIoResource.UNKNOWN_MODIFICATION_DATE
                                ? System.currentTimeMillis()
                                : teLastModified);
            } else {
                te.setModTime(getLastModifiedTime().toMillis());
            }

            if (entry.getType() == ArchiveEntry.SYMLINK) {
                te.setSize(0);

            } else if (!entry.getResource().isDirectory()) {
                final long size = entry.getResource().getSize();
                te.setSize(size == PlexusIoResource.UNKNOWN_RESOURCE_SIZE ? 0 : size);
            }
            te.setMode(entry.getMode());

            PlexusIoResourceAttributes attributes = entry.getResourceAttributes();

            te.setUserName(
                    (attributes != null && attributes.getUserName() != null)
                            ? attributes.getUserName()
                            : options.getUserName());
            te.setGroupName(
                    (attributes != null && attributes.getGroupName() != null)
                            ? attributes.getGroupName()
                            : options.getGroup());

            final int userId =
                    (attributes != null && attributes.getUserId() != null) ? attributes.getUserId() : options.getUid();
            if (userId >= 0) {
                te.setUserId(userId);
            }

            final int groupId = (attributes != null && attributes.getGroupId() != null)
                    ? attributes.getGroupId()
                    : options.getGid();
            if (groupId >= 0) {
                te.setGroupId(groupId);
            }

            tOut.putArchiveEntry(te);

            try {
                if (entry.getResource().isFile() && !(entry.getType() == ArchiveEntry.SYMLINK)) {
                    fIn = entry.getInputStream();

                    Streams.copyFullyDontCloseOutput(fIn, tOut, "xAR");
                }

            } catch (Throwable e) {
                getLogger().warn("When creating tar entry", e);
            } finally {
                tOut.closeArchiveEntry();
            }
        } finally {
            IOUtil.close(fIn);
        }
    }

    /**
     * Valid Modes for Compression attribute to Tar Task
     */
    public class TarOptions {

        private String userName = "";

        private String groupName = "";

        private int uid;

        private int gid;

        private boolean preserveLeadingSlashes = false;

        /**
         * The username for the tar entry
         * This is not the same as the UID.
         *
         * @param userName the user name for the tar entry.
         */
        public void setUserName(String userName) {
            this.userName = userName;
        }

        /**
         * @return the user name for the tar entry
         */
        public String getUserName() {
            return userName;
        }

        /**
         * The uid for the tar entry
         * This is not the same as the User name.
         *
         * @param uid the id of the user for the tar entry.
         */
        public void setUid(int uid) {
            this.uid = uid;
        }

        /**
         * @return the uid for the tar entry
         */
        public int getUid() {
            return uid;
        }

        /**
         * The groupname for the tar entry; optional, default=""
         * This is not the same as the GID.
         *
         * @param groupName the group name string.
         */
        public void setGroup(String groupName) {
            this.groupName = groupName;
        }

        /**
         * @return the group name string.
         */
        public String getGroup() {
            return groupName;
        }

        /**
         * The GID for the tar entry; optional, default="0"
         * This is not the same as the group name.
         *
         * @param gid the group id.
         */
        public void setGid(int gid) {
            this.gid = gid;
        }

        /**
         * @return the group identifier.
         */
        public int getGid() {
            return gid;
        }

        /**
         * @return the leading slashes flag.
         */
        public boolean getPreserveLeadingSlashes() {
            return preserveLeadingSlashes;
        }

        /**
         * Flag to indicates whether leading `/'s should
         * be preserved in the file names.
         * Optional, default is <code>false</code>.
         *
         * @param preserveLeadingSlashes the leading slashes flag.
         */
        public void setPreserveLeadingSlashes(boolean preserveLeadingSlashes) {
            this.preserveLeadingSlashes = preserveLeadingSlashes;
        }
    }

    /**
     * Valid Modes for Compression attribute to Tar Task
     */
    public enum TarCompressionMethod {
        none,
        gzip,
        bzip2,
        snappy,
        xz,
        zstd
    }

    private OutputStream compress(TarCompressionMethod tarCompressionMethod, final OutputStream ostream)
            throws IOException {
        if (TarCompressionMethod.gzip.equals(tarCompressionMethod)) {
            return bufferedOutputStream(new GZIPOutputStream(ostream));
        } else if (TarCompressionMethod.bzip2.equals(tarCompressionMethod)) {
            return new BZip2CompressorOutputStream(bufferedOutputStream(ostream));
        } else if (TarCompressionMethod.snappy.equals(tarCompressionMethod)) {
            return new SnappyOutputStream(bufferedOutputStream(ostream));
        } else if (TarCompressionMethod.xz.equals(tarCompressionMethod)) {
            return new XZCompressorOutputStream(bufferedOutputStream(ostream));
        } else if (TarCompressionMethod.zstd.equals(tarCompressionMethod)) {
            return new ZstdCompressorOutputStream(bufferedOutputStream(ostream));
        }

        return ostream;
    }

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

    @Override
    protected void cleanUp() throws IOException {
        super.cleanUp();
        if (this.tOut != null) {
            this.tOut.close();
        }
    }

    @Override
    protected void close() throws IOException {
        if (this.tOut != null) {
            this.tOut.close();
        }
    }

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