/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.common.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
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.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.modeshape.common.annotation.Immutable;

/**
 * A set of utilities for working with files and directories.
 */
@Immutable
public class FileUtil {

    private static FilenameFilter ACCEPT_ALL = new FilenameFilter() {

        @Override
        public boolean accept( File dir,
                               String name ) {
            return true;
        }
    };

    /**
     * Delete the file or directory at the supplied path. This method works on a directory that is not empty, unlike the
     * {@link File#delete()} method.
     * 
     * @param path the path to the file or directory that is to be deleted
     * @return true if the file or directory at the supplied path existed and was successfully deleted, or false otherwise
     */
    public static boolean delete( String path ) {
        if (path == null || path.trim().length() == 0) return false;
        return delete(new File(path));
    }

    /**
     * Delete the file or directory given by the supplied reference. This method works on a directory that is not empty, unlike
     * the {@link File#delete()} method.
     * 
     * @param fileOrDirectory the reference to the Java File object that is to be deleted
     * @return true if the supplied file or directory existed and was successfully deleted, or false otherwise
     */
    public static boolean delete( File fileOrDirectory ) {
        if (fileOrDirectory == null) return false;
        if (!fileOrDirectory.exists()) return false;

        // The file/directory exists, so if a directory delete all of the contents ...
        if (fileOrDirectory.isDirectory()) {
            File[] files = fileOrDirectory.listFiles();
            if (files != null) {
                for (File childFile : files) {
                    delete(childFile); // recursive call (good enough for now until we need something better)
                }
            }
            // Now an empty directory ...
        }
        // Whether this is a file or empty directory, just delete it ...
        return fileOrDirectory.delete();
    }

    /**
     * Copy the source file system structure into the supplied target location. If the source is a file, the destination will be
     * created as a file; if the source is a directory, the destination will be created as a directory.
     * 
     * @param sourceFileOrDirectory the file or directory whose contents are to be copied into the target location
     * @param destinationFileOrDirectory the location where the copy is to be placed; does not need to exist, but if it does its
     *        type must match that of <code>src</code>
     * @return the number of files (not directories) that were copied
     * @throws IllegalArgumentException if the <code>src</code> or <code>dest</code> references are null
     * @throws IOException
     */
    public static int copy( File sourceFileOrDirectory,
                            File destinationFileOrDirectory ) throws IOException {
        return copy(sourceFileOrDirectory, destinationFileOrDirectory, null);
    }

    /**
     * Copy the source file system structure into the supplied target location. If the source is a file, the destination will be
     * created as a file; if the source is a directory, the destination will be created as a directory.
     * 
     * @param sourceFileOrDirectory the file or directory whose contents are to be copied into the target location
     * @param destinationFileOrDirectory the location where the copy is to be placed; does not need to exist, but if it does its
     *        type must match that of <code>src</code>
     * @param exclusionFilter a filter that matches files or folders that should _not_ be copied; null indicates that all files
     *        and folders should be copied
     * @return the number of files (not directories) that were copied
     * @throws IllegalArgumentException if the <code>src</code> or <code>dest</code> references are null
     * @throws IOException
     */
    public static int copy( File sourceFileOrDirectory,
                            File destinationFileOrDirectory,
                            FilenameFilter exclusionFilter ) throws IOException {
        if (exclusionFilter == null) exclusionFilter = ACCEPT_ALL;
        int numberOfFilesCopied = 0;
        if (sourceFileOrDirectory.isDirectory()) {
            destinationFileOrDirectory.mkdirs();
            String list[] = sourceFileOrDirectory.list(exclusionFilter);

            for (int i = 0; i < list.length; i++) {
                String dest1 = destinationFileOrDirectory.getPath() + File.separator + list[i];
                String src1 = sourceFileOrDirectory.getPath() + File.separator + list[i];

                numberOfFilesCopied += copy(new File(src1), new File(dest1), exclusionFilter);
            }
        } else {
            InputStream fin = new FileInputStream(sourceFileOrDirectory);
            fin = new BufferedInputStream(fin);
            try {
                OutputStream fout = new FileOutputStream(destinationFileOrDirectory);
                fout = new BufferedOutputStream(fout);
                try {
                    int c;
                    while ((c = fin.read()) >= 0) {
                        fout.write(c);
                    }
                } finally {
                    fout.close();
                }
            } finally {
                fin.close();
            }
            numberOfFilesCopied++;
        }
        return numberOfFilesCopied;
    }

    /**
     * Utility to convert {@link File} to {@link URL}.
     * 
     * @param filePath the path of the file
     * @return the {@link URL} representation of the file.
     * @throws MalformedURLException
     * @throws IllegalArgumentException if the file path is null, empty or blank
     */
    public static URL convertFileToURL( String filePath ) throws MalformedURLException {
        CheckArg.isNotEmpty(filePath, "filePath");
        File file = new File(filePath.trim());
        return file.toURI().toURL();
    }

    /**
     * Determines the size (in bytes) of the file or directory at the given path.
     *
     * @param filePath the path of the file; may not be {@code null}
     * @return the size in bytes of the file or the total computed size of the folder. If the given path is not a valid file or
     * folder, this will return 0.
     * @throws IOException if anything unexpected fails.
     */
    public static long size(String filePath) throws IOException {
        CheckArg.isNotEmpty(filePath, "filePath");
        File file = new File(filePath);
        if (!file.exists()) {
            return 0;
        }
        if (file.isFile()) {
            return file.length();
        }
        final AtomicLong size = new AtomicLong();
        Files.walkFileTree(Paths.get(filePath), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) {
                size.addAndGet(attrs.size());
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed( Path file, IOException exc ) {
                return FileVisitResult.CONTINUE;
            }
        });
        return size.get();
    }

    /**
     * Unzip archive to the specified destination.
     * 
     * @param zipFile zip archive
     * @param dest directory where archive will be uncompressed
     * @throws IOException 
     */
    public static void unzip(InputStream zipFile, String dest) throws IOException {
        byte[] buffer = new byte[1024];

        //create output directory is not exists
        File folder = new File(dest);

        if (folder.exists()) {
            FileUtil.delete(folder);
        }

        folder.mkdir();

        //get the zip file content
        ZipInputStream zis = new ZipInputStream(zipFile);
        //get the zipped file list entry
        ZipEntry ze = zis.getNextEntry();
        File parent = new File(dest);
        while (ze != null) {
            String fileName = ze.getName();
            if (ze.isDirectory()) {
                File newFolder = new File(parent, fileName);
                newFolder.mkdir();
            } else {
                File newFile = new File(parent, fileName);
                try (FileOutputStream fos = new FileOutputStream(newFile)) {
                    int len;
                    while ((len = zis.read(buffer)) > 0) {
                        fos.write(buffer, 0, len);
                    }
                }
            }
            ze = zis.getNextEntry();
        }

        zis.closeEntry();
        zis.close();
    }
    
    /**
     * Compresses directory into zip archive.
     * 
     * @param dirName the path to the directory
     * @param nameZipFile archive name.
     * @throws IOException 
     */
    public static void zipDir(String dirName, String nameZipFile) throws IOException {
        ZipOutputStream zip = null;
        FileOutputStream fW = null;
        fW = new FileOutputStream(nameZipFile);
        zip = new ZipOutputStream(fW);
        addFolderToZip("", dirName, zip);
        zip.close();
        fW.close();
    }

    /**
     * Adds folder to the archive.
     * 
     * @param path path to the folder
     * @param srcFolder folder name
     * @param zip zip archive
     * @throws IOException 
     */
    public static void addFolderToZip(String path, String srcFolder, ZipOutputStream zip) throws IOException {
        File folder = new File(srcFolder);
        if (folder.list().length == 0) {
            addFileToZip(path, srcFolder, zip, true);
        } else {
            for (String fileName : folder.list()) {
                if (path.equals("")) {
                    addFileToZip(folder.getName(), srcFolder + "/" + fileName, zip, false);
                } else {
                    addFileToZip(path + "/" + folder.getName(), srcFolder + "/" + fileName, zip, false);
                }
            }
        }
    }

    /**
     * Appends file to the archive.
     * 
     * @param path path to the file
     * @param srcFile file name
     * @param zip archive
     * @param flag
     * @throws IOException 
     */
    public static void addFileToZip(String path, String srcFile, ZipOutputStream zip, boolean flag) throws IOException {
        File folder = new File(srcFile);
        if (flag) {
            zip.putNextEntry(new ZipEntry(path + "/" + folder.getName() + "/"));
        } else {
            if (folder.isDirectory()) {
                addFolderToZip(path, srcFile, zip);
            } else {
                byte[] buf = new byte[1024];
                int len;
                FileInputStream in = new FileInputStream(srcFile);
                zip.putNextEntry(new ZipEntry(path + "/" + folder.getName()));
                while ((len = in.read(buf)) > 0) {
                    zip.write(buf, 0, len);
                }
            }
        }
    }

    /**
     * Returns the extension of a file, including the dot.
     * 
     * @param filename the name of a file, may be not be null
     * @return the file extension or an empty string if the extension cannot be determined.
     */
    public static String getExtension(final String filename) {
        Objects.requireNonNull(filename, "filename cannot be null");
        int lastDotIdx = filename.lastIndexOf(".");
        return lastDotIdx >= 0 ? filename.substring(lastDotIdx) : "";
    }
    
    private FileUtil() {
    }

}
