/*
 * Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazonaws.codepipeline.jenkinsplugin;

import hudson.model.BuildListener;
import hudson.util.IOUtils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;

import com.amazonaws.codepipeline.jenkinsplugin.CodePipelineStateModel.CompressionType;

public final class CompressionTools {

    private CompressionTools() {}

    // Compressing the file to upload to S3 should use the same type of compression as the customer
    // used for the input artifact.
    public static File compressFile(
            final String projectName,
            final Path pathToCompress,
            final CompressionType compressionType,
            final BuildListener listener)
            throws IOException {
        File compressedArtifacts = null;

        try {
            switch (compressionType) {
                case Zip:
                    compressedArtifacts = File.createTempFile(projectName + "-", ".zip");
                    compressZipFile(compressedArtifacts, pathToCompress, listener);
                    break;
                case Tar:
                    compressedArtifacts = File.createTempFile(projectName + "-", ".tar");
                    compressTarFile(compressedArtifacts, pathToCompress, listener);
                    break;
                case TarGz:
                    compressedArtifacts = File.createTempFile(projectName + "-", ".tar.gz");
                    compressTarGzFile(compressedArtifacts, pathToCompress, listener);
                    break;
                case None:
                    throw new IllegalArgumentException("No compression type specified.");
            }
        } catch (final IOException e) {
            if (compressedArtifacts != null) {
                if (!compressedArtifacts.delete()) {
                    compressedArtifacts.deleteOnExit();
                }
            }
            throw e;
        }

        return compressedArtifacts;
    }

    public static void compressZipFile(
            final File temporaryZipFile,
            final Path pathToCompress,
            final BuildListener listener)
            throws IOException {
        try (final ZipArchiveOutputStream zipArchiveOutputStream =
                     new ZipArchiveOutputStream(
                     new BufferedOutputStream(
                     new FileOutputStream(temporaryZipFile)))) {

            compressArchive(
                    pathToCompress,
                    zipArchiveOutputStream,
                    new ArchiveEntryFactory(CompressionType.Zip),
                    CompressionType.Zip,
                    listener);
        }
    }

    public static void compressTarFile(
            final File temporaryTarFile,
            final Path pathToCompress,
            final BuildListener listener)
            throws IOException {
        try (final TarArchiveOutputStream tarArchiveOutputStream =
                     new TarArchiveOutputStream(
                     new BufferedOutputStream(
                     new FileOutputStream(temporaryTarFile)))) {

            compressArchive(
                    pathToCompress,
                    tarArchiveOutputStream,
                    new ArchiveEntryFactory(CompressionType.Tar),
                    CompressionType.Tar,
                    listener);
        }
    }

    public static void compressTarGzFile(
            final File temporaryTarGzFile,
            final Path pathToCompress,
            final BuildListener listener)
            throws IOException {
        try (final TarArchiveOutputStream tarGzArchiveOutputStream =
                new TarArchiveOutputStream(
                new BufferedOutputStream(
                new GzipCompressorOutputStream(
                new FileOutputStream(temporaryTarGzFile))))) {

            compressArchive(
                    pathToCompress,
                    tarGzArchiveOutputStream,
                    new ArchiveEntryFactory(CompressionType.TarGz),
                    CompressionType.TarGz,
                    listener);
        }
    }

    private static void compressArchive(
            final Path pathToCompress,
            final ArchiveOutputStream archiveOutputStream,
            final ArchiveEntryFactory archiveEntryFactory,
            final CompressionType compressionType,
            final BuildListener listener)
            throws IOException {
        final List<File> files = addFilesToCompress(pathToCompress, listener);

        LoggingHelper.log(listener, "Compressing directory '%s' as a '%s' archive",
                pathToCompress.toString(),
                compressionType.name());

        for (final File file : files) {
            final String newTarFileName = pathToCompress.relativize(file.toPath()).toString();
            final ArchiveEntry archiveEntry = archiveEntryFactory.create(file, newTarFileName);
            archiveOutputStream.putArchiveEntry(archiveEntry);

            try (final FileInputStream fileInputStream = new FileInputStream(file)) {
                IOUtils.copy(fileInputStream, archiveOutputStream);
            }

            archiveOutputStream.closeArchiveEntry();
        }
    }

    public static List<File> addFilesToCompress(final Path pathToCompress, final BuildListener listener) throws IOException {
        final List<File> files = new ArrayList<>();

        if (pathToCompress != null) {
            Files.walkFileTree(
                    pathToCompress,
                    EnumSet.of(FileVisitOption.FOLLOW_LINKS),
                    Integer.MAX_VALUE,
                    new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
                    files.add(file.toFile());
                    return FileVisitResult.CONTINUE;
                }
                @Override
                public FileVisitResult visitFileFailed(final Path file, final IOException e) throws IOException {
                    if (e != null) {
                        LoggingHelper.log(listener, "Failed to visit file '%s'. Error: %s.", file.toString(), e.getMessage());
                        LoggingHelper.log(listener, e);
                        throw e;
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        }

        return files;
    }

    public static Path resolveWorkspacePath(
            final File workspace,
            final String outputPath)
            throws FileNotFoundException {
        Path path = null;

        if (workspace != null) {
            if (!outputPath.contains(workspace.getAbsolutePath())) {
                path = Paths.get(workspace.getAbsolutePath(), outputPath);
            }
            else {
                path = Paths.get(outputPath);
            }
        }
        else {
            final String attemptPath;
            final URL tmp = CompressionTools.class.getClassLoader().getResource("");
            if (tmp != null) {
                attemptPath = tmp.getPath();
            }
            else {
                attemptPath = "";
            }

            if (attemptPath != null) {
                path = Paths.get(attemptPath);
            }
        }

        if (path == null) {
            throw new FileNotFoundException("Could not resolve path for " + outputPath);
        }

        path.resolve(outputPath);

        return path;
    }

}