/*
 * @(#)Zip.java
 *
 * Copyright (c) JSCAPE LLC.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.zip;

import com.jscape.util.FileTools;
import com.jscape.util.IOTools;

import java.io.*;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;


/**
 * Implements the basic functionality of ZIP compression and decompression.
 */
public class Zip
        implements Archive {

    public static final char FORWARD_SLASH = '/';
    public static final char BACK_SLASH = '\\';
    public static final int BUF_SIZE = 8192 * 2;

    private String baseDir = "";
    private final transient List<ArchiveListener> listeners = new CopyOnWriteArrayList<ArchiveListener>();
    private boolean interrupted = false;

    /**
     * Creates a new Zip instance.
     */
    public Zip() {
        
    }

    /**
     * Creates a new ZIP archive.
     *
     * @param file the input file
     *
     * @throws ArchiveException if error occurs
     */
    public void compress(File file)
            throws ArchiveException {
        try {
            File zipFile;
            zipFile = file.isDirectory()
                      ? new File(file, file.getName() + ".zip")
                      : new File(file.getAbsolutePath() + ".zip");

            compress(file, zipFile);
        } catch (Exception e) {
            throw new ArchiveException(e);
        }
    }

    /**
     * Creates a new ZIP archive.
     *
     * @param file       the input file
     * @param targetPath the output file or directory
     *
     * @throws ArchiveException if error occurs
     */
    public void compress(File file, File targetPath)
            throws ArchiveException {
        ZipOutputStream out = null;

        try {
            File targetFile = targetFor(targetPath, file.getName());
            out = new ZipOutputStream(new FileOutputStream(targetFile));

            addDir(out, file, "", targetPath);
        } catch (Exception e) {
            throw new ArchiveException(e);
        } finally {
            IOTools.close(out);
        }
    }

    /**
     * Compresses directory recursively.
     *
     * @param out     the instance of ZIP file
     * @param file    the file or directory to compress
     * @param baseDir the path of the directory
     * @param output  the current output file
     *
     * @throws ArchiveException if error occurs
     */
    private void addDir(ZipOutputStream out, File file, String baseDir, File output)
            throws ArchiveException {
        File[] files = null;

        if (file.isDirectory()) {
            //Add ZIP entry to output stream.
            if (baseDir.length() == 0) {
                this.baseDir = file.getName();
            } else {
                this.baseDir = baseDir + File.separator + file.getName();
            }
            files = file.listFiles();
        } else {
            files = new File[1];
            files[0] = file;
        }

        byte[] buf = new byte[BUF_SIZE];
        long compressed = 0;
        double totalSize = FileTools.getLength(file, true);

        if (files.length == 0) { // Empty dir
            try {
                String entryName = this.baseDir.length() == 0 ? "" : this.baseDir + File.separator;
                out.putNextEntry(new ZipEntry(entryName));
            } catch (IOException ioe) {
                throw new ArchiveException(ioe);
            }

        } else {
            FileInputStream in = null;
            for (int i = 0; i < files.length; i++) {
                try {
                    File currentfile = files[i];

                    if (currentfile.getAbsolutePath().equals(output.getAbsolutePath())) {
                        continue;
                    }

                    if (currentfile.isDirectory()) {
                        this.addDir(out, currentfile, this.baseDir, output);
                        int index = this.baseDir.lastIndexOf(File.separator);
                        this.baseDir = this.baseDir.substring(0, index);
                        continue;

                    }

                    in = new FileInputStream(currentfile);

                    if (file.isFile() && files.length == 1) {
                        out.putNextEntry(new ZipEntry(currentfile.getName()));
                    } else {
                        String entryName = this.baseDir.length() == 0 ? currentfile.getName() : this.baseDir + File.separator + currentfile.getName();
                        out.putNextEntry(new ZipEntry(entryName));
                    }

                    // Transfer bytes from the file to the ZIP file
                    int len;
                    while ((len = in.read(buf)) > 0) {
                        out.write(buf, 0, len);
                        compressed += len;
                        this.fireCompressionProgressEvent(new CompressionEvent(this, compressed, totalSize, ProgressEventMode.Compression));
                    }

                    this.fireCompressedEvent(new ArchiveCompressedEvent(this, currentfile));

                    // Complete the entry
                    out.closeEntry();
                    in.close();
                } catch (Exception zipex) {
                    throw new ArchiveException(zipex);
                } finally {
                    try {
                        out.closeEntry();
//                        out.flush();
//                        out.finish();
                        in.close();
                    } catch (Exception ez) {
                        // ignore
                    }
                }
            }
        }

    }

    /**
     * Calculate file size from an array of files.
     *
     * @param files
     *
     * @return size
     */
    private long getTotalSize(File[] files) {
        long ret = 0L;
        for (File file : files) {
            ret += file.length();
        }
        return ret;
    }

    /**
     * Calculate file size from a vector of files.
     *
     * @param files
     *
     * @return size
     */
    private long getTotalSize(Vector files) {
        long ret = 0L;
        Enumeration e = files.elements();
        while (e.hasMoreElements()) {
            ret += ((File) e.nextElement()).length();
        }
        return ret;
    }

    /**
     * Interrupt this instance & stop any ongoing compression / decompression.
     */
    public void interrupt() {
        this.interrupted = true;
    }

    /**
     * Reset the interrupted flag.
     */
    public void reset() {
        this.interrupted = false;
    }

    /**
     * Returns whether this instance has been interrupted.
     *
     * @return interrupted
     */
    public boolean isInterrupted() {
        return this.interrupted;
    }

    /**
     * Expects a Vector of files and zips the content to the output file
     *
     * @param sourceVector
     * @param output
     *
     * @throws ArchiveException
     */
    public void compress(File output, Vector sourceVector)
            throws ArchiveException {

        ZipOutputStream os = null;
        try {
            os = new ZipOutputStream(new FileOutputStream(output.getAbsolutePath()));

            byte[] buffer = new byte[BUF_SIZE];

            long totalSize = getSize(sourceVector);
            double compressed = 0;

            Iterator sourceVectorItr = sourceVector.iterator();
            while (sourceVectorItr.hasNext()) {
                File source = (File) sourceVectorItr.next();

                if (source.isDirectory()) {
                    Iterator files = getList(source.getAbsolutePath()).iterator();

                    while (files.hasNext()) {
                        File entry = (File) files.next();

                        ZipEntry ze = new ZipEntry(entry.getAbsolutePath().substring(entry.getAbsolutePath().indexOf(source.getName())));
                        os.putNextEntry(ze);

                        FileInputStream in = new FileInputStream(entry);

                        try {
                            int length = 0;
                            while ((length = in.read(buffer)) > 0) {
                                os.write(buffer, 0, length);
                                compressed += length;
                                this.fireCompressionProgressEvent(new CompressionEvent(this, compressed, (double) totalSize, ProgressEventMode.Compression));
                            }

                            this.fireCompressedEvent(new ArchiveCompressedEvent(this, source));
                            os.closeEntry();
                        } finally {
                            IOTools.close(in);
                        }
                    }
                } else if (source.isFile()) {
                    ZipEntry ze = new ZipEntry(source.getAbsolutePath().substring(source.getAbsolutePath().indexOf(source.getName())));
                    os.putNextEntry(ze);

                    FileInputStream in = new FileInputStream(source);

                    try {
                        int length = 0;
                        while ((length = in.read(buffer)) > 0) {
                            os.write(buffer, 0, length);
                            compressed += length;
                            this.fireCompressionProgressEvent(new CompressionEvent(this, compressed, (double) totalSize, ProgressEventMode.Compression));
                        }
                        this.fireCompressedEvent(new ArchiveCompressedEvent(this, source));
                        os.closeEntry();
                    } finally {
                        IOTools.close(in);
                    }
                }
            }
        } catch (Throwable e) {
            throw new ArchiveException(e);
        } finally {
            try {
                os.flush();
                os.finish();
                os.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * Get size of files in Vector
     *
     * @param files target files
     *
     * @return size
     */
    public long getSize(Vector files) {
        long size = 0L;
        for (Object file : files) {
            File f = (File) file;
            if (f.isFile()) {
                size += f.length();
            } else {
                size += FileTools.getLength(f, true);
            }
        }
        return size;
    }

    /**
     * Recursively calculates the list of files.
     *
     * @param sDir
     *
     * @return vector of files
     */
    @SuppressWarnings("unchecked")
    public static Vector getList(String sDir) {

        Vector ret = new Vector();
        File[] files = new File(sDir).listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                ret.addAll(getList(file.getAbsolutePath()));
            } else {
                ret.add(file);
            }
        }
        return ret;
    }

    /**
     * Compresses list of files to specified output file.
     *
     * @param output the ZIP output file
     * @param files  the file list to be compressed
     * @throws ArchiveException if an error occurs
     */
    /*
    public void compress(File output, Vector files) throws ArchiveException {

        //Create the ZIP file
        ZipOutputStream out = null;
        try {
            out = new ZipOutputStream(new FileOutputStream(output));
        } catch (Exception e) {
            throw new ArchiveException(e.getMessage(), e);
        }

        //Create a buffer for reading the files
        byte[] buf = new byte[BUF_SIZE];
        long totalSize = this.getTotalSize(files);
        double compressed = 0;
        FileInputStream in = null;

        for (int i = 0; i < files.size(); i++) {
            try {
                File currentfile = (File) files.get(i);

                if (currentfile.getAbsolutePath().equals(output.getAbsolutePath())) {
                    continue;
                }

                this.baseDir = "";
                if (currentfile.isDirectory()) {
                    this.addDir(out, currentfile, this.baseDir, output);
                    continue;
                }
                in = new FileInputStream(currentfile);
                String entryName = this.baseDir.length() == 0 ?  currentfile.getName() : this.baseDir + File.separator + currentfile.getName();
                ZipEntry entry = new ZipEntry(entryName);

                out.putNextEntry(entry);

                // Transfer bytes from the file to the ZIP file
                int len = 0;
                while ((len = in.read(buf)) > 0 && this.isInterrupted() == false) {
                    out.write(buf, 0, len);
                    compressed += len;
                    this.fireCompressionProgressEvent(new CompressionEvent(this,
                                                                                                                                                                                                           compressed,
                                                                                                                                                                                                           (double)totalSize,
                                                                                                                                                                                                           ProgressEventMode.Compression));
                }

                this.fireCompressedEvent(new ArchiveCompressedEvent(this, currentfile));

                // Complete the entry
                out.closeEntry();
                in.close();
            } catch (Exception zipex) {
                throw new ArchiveException(zipex.getMessage(), zipex);
            }
        }

        if (out != null) {
                try {
                out.flush();
                out.finish();
                out.close();
            } catch (Exception e) {
                  // ignore
            }
        }

        if (in != null) {
                try {
                        in.close();
                } catch (Exception e) {
                        // ignore
                }
        }
    }
*/

    /**
     * Decompress all the files in given ZIP archive.
     *
     * @param file the ZIP file
     *
     * @throws ArchiveException if error occurs
     */
    public void decompress(File file)
            throws ArchiveException {
        File dir = new File(file.getParent());
        this.decompress(file, dir);
    }

    /**
     * @param entry a zip entry
     *
     * @return true if ZipEntry.getName() does not end with '/' or '\' else returns false
     */
    private boolean isNotDirectory(ZipEntry entry) {
        return !entry.getName().endsWith(String.valueOf(Zip.FORWARD_SLASH))
                || !entry.getName().endsWith(String.valueOf(Zip.BACK_SLASH));
    }

    /**
     * Decompress all the files in given ZIP archive.
     *
     * @param file   the ZIP file
     * @param output the output directory to decompress files to
     *
     * @throws ArchiveException if error occurs
     */
    public void decompress(File file, File output)
            throws ArchiveException {
        ZipInputStream zis = null;
        try {
            // Create a ZipInputStream to read the zip file
            BufferedOutputStream dest = null;
            FileInputStream fis = new FileInputStream(file);
            zis = new ZipInputStream(new BufferedInputStream(fis));

            // Loop over all of the entries in the zip file
            int count;
            byte[] data = new byte[BUF_SIZE];

            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                if (this.isNotDirectory(entry)) {
                    // Create the file name
                    String outputName = "";
                    String absolutePath = output.getAbsolutePath().replace(Zip.BACK_SLASH, Zip.FORWARD_SLASH);
                    String entryName = entry.getName().replace(Zip.BACK_SLASH, Zip.FORWARD_SLASH);

                    if (!output.isDirectory()) {
                        throw new ArchiveException("The file " + output.getName() + " is not directory.");
                    }

                    outputName = absolutePath + Zip.FORWARD_SLASH + entryName;

                    int index = outputName.lastIndexOf(Zip.FORWARD_SLASH);
                    String dirs = outputName.substring(0, index);

                    createSubDir(dirs);

                    // Write the file to the file system
                    if (index != outputName.length() - 1) {
                        FileOutputStream fos = new FileOutputStream(outputName);
                        dest = new BufferedOutputStream(fos, BUF_SIZE);
                        while ((count = zis.read(data, 0, BUF_SIZE)) != -1) {
                            dest.write(data, 0, count);
                            this.fireDecompressionProgressEvent(new DecompressionEvent(this, count, ProgressEventMode.Decompression));
                        }

                        dest.flush();
                        dest.close();
                        fos.flush();
                        fos.close();
                    }

                    this.fireDecompressedEvent(new ArchiveDecompressedEvent(this, new File(outputName)));
                }
            }
        } catch (Throwable e) {
            throw new ArchiveException(e);
        } finally {
            IOTools.close(zis);
        }
    }

    /**
     * Creates the subdirectorys in the path given.
     *
     * @param subDirs the directory path
     */
    private void createSubDir(String subDirs)
            throws IOException {
        File dir = new File(subDirs);
        if (dir.exists()) {
            return;
        }

        boolean created = dir.mkdirs();

        if (!created) {
            throw new IOException("Error creating directory.");
        }
    }

    /**
     * Adds listener.
     *
     * @param listener the listener
     *
     * @see Archive#addArchiveListener(ArchiveListener)
     */
    public void addArchiveListener(ArchiveListener listener) {
        this.listeners.add(listener);
    }

    /**
     * Removes listener.
     *
     * @param listener the listener
     *
     * @see Archive#removeArchiveListener(ArchiveListener)
     */
    public void removeArchiveListener(ArchiveListener listener) {
        this.listeners.remove(listener);
    }

    /**
     * Files Decompression event.
     *
     * @param event an event
     */
    protected void fireDecompressionProgressEvent(DecompressionEvent event) {
        for (ArchiveListener listener : listeners) {
            listener.decompressionProgress(event);
        }
    }

    /**
     * Fires CompressionEvent event.
     *
     * @param ev
     */
    protected void fireCompressionProgressEvent(CompressionEvent ev) {
        for (ArchiveListener listener : listeners) {
            listener.compressionProgress(ev);
        }
    }

    /**
     * Fires ArchiveCompressedEvent event.
     *
     * @param ev event to fire
     */
    protected void fireCompressedEvent(ArchiveCompressedEvent ev) {
        for (ArchiveListener listener : listeners) {
            listener.compressed(ev);
        }
    }

    /**
     * Fires ArchiveDecompressedEvent event.
     *
     * @param ev event to fire
     */
    protected void fireDecompressedEvent(ArchiveDecompressedEvent ev) {
        for (ArchiveListener listener : listeners) {
            listener.decompressed(ev);
        }
    }

    private File targetFor(File targetPath, String sourceName) {
        return targetPath.isDirectory() ? new File(targetPath, sourceName + ".zip") : targetPath;
    }
}
