package com.atlassian.bitbucket.mesh.util;

import com.atlassian.bitbucket.mesh.io.MoreFiles;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;
import java.util.Set;

public class ResourceUtils {

    private ResourceUtils() {
        throw new UnsupportedOperationException(getClass().getName() +
                " is a utility class and should not be instantiated");
    }

    /**
     * Unpacks the hook script or binary at {@code resourcePath} to {@code destination} (replacing any already present
     * file), then makes the hook executable if the current platform is POSIX compatible.
     *
     * @param resourcePath the path of the hook script/binary resource (so the path on the classpath)
     * @param destination the location where the hook script/binary should be unpacked to
     * @throws IOException if the hook resource cannot be read, the destination cannot be written to, or cannot
     *                     be made executable
     */
    public static void unpackHook(@Nonnull String resourcePath, @Nonnull Path destination) throws IOException {
        try (InputStream inputStream = ResourceUtils.class.getResourceAsStream(resourcePath)) {
            if (inputStream == null) {
                //Throw an IOException (instead of letting Files.copy below throw a NPE) to make exception handling
                //a bit simpler
                throw new NoSuchFileException(resourcePath);
            }
            Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING);
            setExecutable(destination);
        }
    }

    /**
     * Unpacks the tar file at {@code resourcePath} to {@code destination}, replacing any files already present
     * at that location if a file with the same name is present in the tar file. The {@code executablePredicate}
     * should identify all files from the tar that should be made executable.
     *
     * @param resourcePath the path of the tar archive resource (so the path on the classpath)
     * @param destination the location where the tar should be unpacked to
     * @throws IOException if the tar cannot be read, destination cannot be written to, or one of the files cannot
     *                     be made executable
     */
    public static void unpackTar(String resourcePath, Path destination) throws IOException {
        boolean posix = MoreFiles.isPosixSupported(destination);
        try (TarArchiveInputStream inputStream =
                     new TarArchiveInputStream(ResourceUtils.class.getResourceAsStream(resourcePath))) {

            for (TarArchiveEntry entry = inputStream.getNextTarEntry(); entry != null; entry = inputStream.getNextTarEntry()) {
                //This tar file is created using the maven-assembly-plugin, and it includes explicit directory
                //entries. By the time a file entry is found, its containing directory will always exist
                Path file = destination.resolve(entry.getName());
                if (entry.isDirectory()) {
                    MoreFiles.mkdir(file);
                } else {
                    copy(inputStream, file);
                    if (posix && (entry.getMode() & 0100) > 0) {
                        setExecutable(file);
                    }
                }
            }
        }
    }

    /**
     * Copies the provided {@code InputStream} to the specified path, replacing the current file if it exists.
     *
     * @param source the stream to copy from
     * @param target the file to copy to
     * @throws IOException if the file cannot be copied
     */
    private static void copy(InputStream source, Path target) throws IOException {
        Path tmp = target.resolveSibling(target.getFileName() + ".tmp");
        Files.copy(source, tmp, StandardCopyOption.REPLACE_EXISTING);
        Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
    }

    /**
     * For POSIX-compliant filesystems, ensures the owner has {@link PosixFilePermission#OWNER_EXECUTE execute}
     * permission for the specified file. This is required to ensure hooks and other scripts can run.
     *
     * @param file the file to set executable
     * @throws IOException if the file cannot be set executable
     */
    private static void setExecutable(Path file) throws IOException {
        if (MoreFiles.isPosixSupported(file.getParent())) {
            Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(file, LinkOption.NOFOLLOW_LINKS);
            if (!permissions.contains(PosixFilePermission.OWNER_EXECUTE)) {
                permissions = EnumSet.copyOf(permissions);
                permissions.add(PosixFilePermission.OWNER_EXECUTE);

                Files.setPosixFilePermissions(file, permissions);
            }
        }
    }
}
