/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.utils;

import org.apache.commons.io.IOUtils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Deque;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Utility class for zip operations
 */
public class ConfigZipUtils {

    private static final int BUFFER_SIZE = 8388608;

    private static final String ZIP_PATH_SEPARATOR = "/";

    private ConfigZipUtils() {
    }

    /**
     * Zips all the files in <code>fromDir</code> into a new <code>zipFile File</code>. Uses the
     * <code>Deflater.BEST_COMPRESSION</code> compression level, if speed is important use the other zip method with the
     * desired compression/speed compromise.
     *
     * @param fromDir directory from which the files to be zipped are taken.
     * @param zipFile the new zip file to create.
     * @throws IOException
     */
    public static void zip(final File fromDir, final File zipFile) throws IOException {
        zip(fromDir, zipFile, Deflater.BEST_COMPRESSION);
    }

    /**
     * Zips all the files in <code>fromDir</code> into a new <code>zipFile File</code>. Uses the
     * <code>Deflater.BEST_COMPRESSION</code> compression level, if speed is important use the other zip method with the
     * desired compression/speed compromise.
     *
     * @param fromFiles files from which the files to be zipped are taken.
     * @param zipFile   the new zip file to create.
     * @throws IOException
     */
    public static void zip(final List<File> fromFiles, final File zipFile) throws IOException {
        zip(fromFiles, zipFile, Deflater.BEST_COMPRESSION);
    }

    public static void zipLarge(final List<File> fromFiles, final File zipFile) throws IOException {
        zipLarge(fromFiles, zipFile, Deflater.BEST_COMPRESSION);
    }

    /**
     * Zips all the files in <code>fromDir</code> into a new <code>zipFile File</code>.
     *
     * @param fromFiles        files from which the files to be zipped are taken.
     * @param zipFile          the new zip file to create.
     * @param compressionLevel the desired compression level.
     * @throws IOException
     */
    public static void zip(final List<File> fromFiles, final File zipFile, final int compressionLevel)
            throws IOException {

        try (OutputStream zipFileStream = new BufferedOutputStream(new FileOutputStream(zipFile));
             ZipOutputStream zipStream = new ZipOutputStream(zipFileStream)) {

            zipStream.setLevel(compressionLevel);

            for (File fromFile : fromFiles) {
                final ZipEntry entry = new ZipEntry(fromFile.getName());
                zipStream.putNextEntry(entry);
                try (InputStream fis = new BufferedInputStream(new FileInputStream(fromFile))) {
                    IOUtils.copy(fis, zipStream);
                    zipStream.closeEntry();
                }
            }
        }
    }

    public static void zipLarge(final List<File> fromFiles, final File zipFile, final int compressionLevel)
            throws IOException {

        try (OutputStream zipFileStream = new BufferedOutputStream(new FileOutputStream(zipFile));
             ZipOutputStream zipStream = new ZipOutputStream(zipFileStream)) {

            zipStream.setLevel(compressionLevel);

            for (File fromFile : fromFiles) {
                final ZipEntry entry = new ZipEntry(fromFile.getName());
                zipStream.putNextEntry(entry);
                try (InputStream fis = new BufferedInputStream(new FileInputStream(fromFile))) {
                    IOUtils.copyLarge(fis, zipStream);
                    zipStream.closeEntry();
                }
            }
        }
    }

    /**
     * Zips all the files in <code>fromDir</code> into a new <code>zipFile File</code>.
     *
     * @param fromDir          directory from which the files to be zipped are taken.
     * @param zipFile          the new zip file to create.
     * @param compressionLevel the desired compression level.
     * @throws IOException
     */
    public static synchronized void zip(final File fromDir, final File zipFile, final int compressionLevel)
            throws IOException {

        try (OutputStream zipFileStream = new BufferedOutputStream(new FileOutputStream(zipFile));
             ZipOutputStream zipStream = new ZipOutputStream(zipFileStream)) {

            zipStream.setLevel(compressionLevel);

            for (File f : fromDir.listFiles()) {
                ZipEntry entry = new ZipEntry(f.getName());
                zipStream.putNextEntry(entry);
                try (InputStream fis = new BufferedInputStream(new FileInputStream(f))) {
                    IOUtils.copy(fis, zipStream);
                }
            }

        }
    }

    /**
     * From http://www.rgagnon.com/javadetails/java-0067.html
     *
     * @param file
     * @param outDir should exists previously
     * @throws IOException
     */
    public static synchronized void unzipFile(final File file, final String outDir) throws IOException {

        try (ZipFile zipFile = new ZipFile(file)) {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();


            while (entries.hasMoreElements()) {
                ZipEntry e = entries.nextElement();

                unzip(zipFile.getInputStream(e), outDir + e.getName());
            }
        }
    }

    /**
     * Method to extract to content of a zip file in a output folder.
     *
     * @param is
     * @param outDir should exists previously
     * @throws IOException
     */

    public static synchronized void unzipFile(final InputStream is, final String outDir) throws IOException {

        ZipEntry e;
        try (ZipInputStream zis = new ZipInputStream(is)) {
            while ((e = zis.getNextEntry()) != null) {
                unzip(zis, outDir + e.getName());
            }
        }
    }

    /**
     * Zips all the folders and files inside the specified directory respecting the folder structure.
     *
     * @param directory to be zipped
     * @param zipFile   the new zip file to create
     * @throws IOException
     */
    public static void zipDir(final File directory, final File zipFile) throws IOException {

        URI base = directory.toURI();
        Deque<File> queue = new LinkedList<>();
        queue.push(directory);

        try (OutputStream out = new FileOutputStream(zipFile);
             ZipOutputStream zout = new ZipOutputStream(out)) {

            File dir;
            while (!queue.isEmpty()) {
                dir = queue.pop();
                for (File kid : dir.listFiles()) {
                    String name = base.relativize(kid.toURI()).getPath();
                    if (kid.isDirectory()) {
                        queue.push(kid);
                        if (!name.endsWith(ZIP_PATH_SEPARATOR)) {
                            name = name + ZIP_PATH_SEPARATOR;
                        }
                        zout.putNextEntry(new ZipEntry(name));
                    } else {
                        zout.putNextEntry(new ZipEntry(name));
                        copy(kid, zout);
                        zout.closeEntry();
                    }
                }
            }
        }
    }

    private static synchronized void unzip(final InputStream in, final String s) throws IOException {

        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(s))) {
            IOUtils.copy(in, out);
        }
    }

    private static void copy(final InputStream in, final OutputStream out) throws IOException {
        byte[] buffer = new byte[BUFFER_SIZE];
        while (true) {
            int readCount = in.read(buffer);
            if (readCount < 0) {
                break;
            }
            out.write(buffer, 0, readCount);
        }
    }

    private static void copy(final File file, final OutputStream out) throws IOException {
        try (InputStream in = new FileInputStream(file)) {
            copy(in, out);
        }
    }

}
