/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.io;

import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.util.Visitor;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.URL;
import java.text.NumberFormat;

/**
 * A utility class with some useful file and i/o related methods.
 * <P>
 * Versions exists take Input and OutputStreams as parameters, to
 * allow for copying streams (URL's etc.).
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author <A href="mailto:eirik.torske@iconmedialab.no">Eirik Torske</A>
 * @author last modified by $Author: haku $
 * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileUtil.java#3 $
 */
public final class FileUtil {
    // TODO: Be more cosequent using resolve() all places where File objects are involved
    // TODO: Parameter handling (allow null vs IllegalArgument)
    // TODO: Exception handling
    
    /**
     * The size of the buffer used for copying
     */
    public final static int BUF_SIZE = 1024;
    private static String TEMP_DIR = null;

    private final static FileSystem FS = FileSystem.get();

    public static void main(String[] pArgs) throws IOException {
        File file;
        if (pArgs[0].startsWith("file:")) {
            file = toFile(new URL(pArgs[0]));
            System.out.println(file);
        }
        else {
            file = new File(pArgs[0]);
            System.out.println(file.toURL());
        }

        System.out.println("Free space: " + getFreeSpace(file) + "/" + getTotalSpace(file) + " bytes");
    }

    /*
     * Method main for test only.
     *
    public static void main0(String[] pArgs) {
        if (pArgs.length != 2) {
            System.out.println("usage: java Copy in out");
            return;
        }
        try {
            if (!copy(pArgs[0], pArgs[1])) {
                System.out.println("Error copying");
            }
        }
        catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
    //*/

    // Avoid instances/constructor showing up in API doc
    private FileUtil() {}

    /**
     * Copies the fromFile to the toFile location. If toFile is a directory, a
     * new file is created in that directory, with the name of the fromFile.
     * If the toFile exists, the file will not be copied, unless owerWrite is
     * true.
     *
     * @param pFromFileName The name of the file to copy from
     * @param pToFileName   The name of the file to copy to
     * @return true if the file was copied successfully,
     *         false if the output file exists. In all other cases, an
     *         IOException is thrown, and the method does not return a value.
     * @throws IOException if an i/o error occurs during copy
     */
    public static boolean copy(String pFromFileName, String pToFileName) throws IOException {
        return copy(new File(pFromFileName), new File(pToFileName), false);
    }

    /**
     * Copies the fromFile to the toFile location. If toFile is a directory, a
     * new file is created in that directory, with the name of the fromFile.
     * If the toFile exists, the file will not be copied, unless owerWrite is
     * true.
     *
     * @param pFromFileName The name of the file to copy from
     * @param pToFileName   The name of the file to copy to
     * @param pOverWrite    Specifies if the toFile should be overwritten, if it
     *                      exists.
     * @return true if the file was copied successfully,
     *         false if the output file exists, and the owerWrite parameter is
     *         false. In all other cases, an
     *         IOException is thrown, and the method does not return a value.
     * @throws IOException if an i/o error occurs during copy
     */
    public static boolean copy(String pFromFileName, String pToFileName, boolean pOverWrite) throws IOException {
        return copy(new File(pFromFileName), new File(pToFileName), pOverWrite);
    }

    /**
     * Copies the fromFile to the toFile location. If toFile is a directory, a
     * new file is created in that directory, with the name of the fromFile.
     * If the toFile exists, the file will not be copied, unless owerWrite is
     * true.
     *
     * @param pFromFile The file to copy from
     * @param pToFile   The file to copy to
     * @return true if the file was copied successfully,
     *         false if the output file exists. In all other cases, an
     *         IOException is thrown, and the method does not return a value.
     * @throws IOException if an i/o error occurs during copy
     */
    public static boolean copy(File pFromFile, File pToFile) throws IOException {
        return copy(pFromFile, pToFile, false);
    }

    /**
     * Copies the fromFile to the toFile location. If toFile is a directory, a
     * new file is created in that directory, with the name of the fromFile.
     * If the toFile exists, the file will not be copied, unless owerWrite is
     * true.
     *
     * @param pFromFile  The file to copy from
     * @param pToFile    The file to copy to
     * @param pOverWrite Specifies if the toFile should be overwritten, if it
     *                   exists.
     * @return {@code true} if the file was copied successfully,
     *         {@code false} if the output file exists, and the
     *         {@code pOwerWrite} parameter is
     *         {@code false}. In all other cases, an
     *         {@code IOExceptio}n is thrown, and the method does not return.
     * @throws IOException if an i/o error occurs during copy
     */
    public static boolean copy(File pFromFile, File pToFile, boolean pOverWrite) throws IOException {
        // Copy all directory structure
        if (pFromFile.isDirectory()) {
            return copyDir(pFromFile, pToFile, pOverWrite);
        }

        // Check if destination is a directory
        if (pToFile.isDirectory()) {
            // Create a new file with same name as from
            pToFile = new File(pToFile, pFromFile.getName());
        }

        // Check if file exists, and return false if overWrite is false
        if (!pOverWrite && pToFile.exists()) {
            return false;
        }

        InputStream in = null;
        OutputStream out = null;

        try {
            // Use buffer size two times byte array, to avoid i/o bottleneck
            in = new FileInputStream(pFromFile);
            out = new FileOutputStream(pToFile);

            // Copy from inputStream to outputStream
            copy(in, out);
        }
        //Just pass any IOException on up the stack
        finally {
            close(in);
            close(out);
        }

        return true;  // If we got here, everything is probably okay.. ;-)
    }

    /**
     * Tries to close the given stream.
     * NOTE: If the stream cannot be closed, the IOException thrown is silently
     * ignored.
     * 
     * @param pInput the stream to close
     */
    public static void close(InputStream pInput) {
        try {
            if (pInput != null) {
                pInput.close();
            }
        }
        catch (IOException ignore) {
            // Non critical error
        }
    }

    /**
     * Tries to close the given stream.
     * NOTE: If the stream cannot be closed, the IOException thrown is silently
     * ignored.
     *
     * @param pOutput the stream to close
     */
    public static void close(OutputStream pOutput) {
        try {
            if (pOutput != null) {
                pOutput.close();
            }
        }
        catch (IOException ignore) {
            // Non critical error
        }
    }

    static void close(Reader pReader) {
        try {
            if (pReader != null) {
                pReader.close();
            }
        }
        catch (IOException ignore) {
            // Non critical error
        }
    }

    static void close(Writer pWriter) {
        try {
            if (pWriter != null) {
                pWriter.close();
            }
        }
        catch (IOException ignore) {
            // Non critical error
        }
    }

    /**
     * Copies a directory recursively. If the destination folder does not exist,
     * it is created
     *
     * @param pFrom the source directory
     * @param pTo the destination directory
     * @param pOverWrite {@code true} if we should allow overwrting existing files
     * @return {@code true} if all files were copied sucessfully
     * @throws IOException if {@code pTo} exists, and it not a directory,
     *          or if copying of any of the files in the folder fails
     */
    private static boolean copyDir(File pFrom, File pTo, boolean pOverWrite) throws IOException {
        if (pTo.exists() && !pTo.isDirectory()) {
            throw new IOException("A directory may only be copied to another directory, not to a file");
        }
        pTo.mkdirs();  // mkdir?
        boolean allOkay = true;
        File[] files = pFrom.listFiles();

        for (File file : files) {
            if (!copy(file, new File(pTo, file.getName()), pOverWrite)) {
                allOkay = false;
            }
        }
        return allOkay;
    }

    /**
     * Copies all data from one stream to another.
     * The data is copied from the fromStream to the toStream using buffered
     * streams for efficiency.
     *
     * @param pFrom The input srteam to copy from
     * @param pTo   The output stream to copy to
     * @return true. Otherwise, an
     *         IOException is thrown, and the method does not return a value.
     * @throws IOException if an i/o error occurs during copy
     * @throws IllegalArgumentException if either {@code pFrom} or {@code pTo} is
     *         {@code null}
     */
    public static boolean copy(InputStream pFrom, OutputStream pTo) throws IOException {
        Validate.notNull(pFrom, "from");
        Validate.notNull(pTo, "to");

        // TODO: Consider using file channels for faster copy where possible

        // Use buffer size two times byte array, to avoid i/o bottleneck
        // TODO: Consider letting the client decide as this is sometimes not a good thing!
        InputStream in = new BufferedInputStream(pFrom, BUF_SIZE * 2);
        OutputStream out = new BufferedOutputStream(pTo, BUF_SIZE * 2);

        byte[] buffer = new byte[BUF_SIZE];
        int count;

        while ((count = in.read(buffer)) != -1) {
            out.write(buffer, 0, count);
        }

        // Flush out stream, to write any remaining buffered data
        out.flush();

        return true;  // If we got here, everything is probably okay.. ;-)
    }

    /**
     * Gets the file (type) extension of the given file.
     * A file extension is the part of the filename, after the last occurence
     * of a period {@code '.'}.
     * If the filename contains no period, {@code null} is returned.
     *
     * @param pFileName the full filename with extension
     * @return the extension (type) of the file, or {@code null}
     */
    public static String getExtension(final String pFileName) {
        return getExtension0(getFilename(pFileName));
    }

    /**
     * Gets the file (type) extension of the given file.
     * A file extension is the part of the filename, after the last occurrence
     * of a period {@code '.'}.
     * If the filename contains no period, {@code null} is returned.
     *
     * @param pFile the file
     * @return the extension (type) of the file, or {@code null}
     */
    public static String getExtension(final File pFile) {
        return getExtension0(pFile.getName());
    }

    // NOTE: Assumes filename and no path
    private static String getExtension0(final String pFileName) {
        int index = pFileName.lastIndexOf('.');

        if (index >= 0) {
            return pFileName.substring(index + 1);
        }

        // No period found
        return null;
    }


    /**
     * Gets the file name of the given file, without the extension (type).
     * A file extension is the part of the filename, after the last occurence
     * of a period {@code '.'}.
     * If the filename contains no period, the complete file name is returned
     * (same as {@code pFileName}, if the string contains no path elements).
     *
     * @param pFileName the full filename with extension
     * @return the base name of the file
     */
    public static String getBasename(final String pFileName) {
        return getBasename0(getFilename(pFileName));
    }

    /**
     * Gets the file name of the given file, without the extension (type).
     * A file extension is the part of the filename, after the last occurence
     * of a period {@code '.'}.
     * If the filename contains no period, {@code pFile.getName()} is returned.
     *
     * @param pFile the file
     * @return the base name of the file
     */
    public static String getBasename(final File pFile) {
        return getBasename0(pFile.getName());
    }

    // NOTE: Assumes filename and no path
    public static String getBasename0(final String pFileName) {
        int index = pFileName.lastIndexOf('.');

        if (index >= 0) {
            return pFileName.substring(0, index);
        }

        // No period found
        return pFileName;
    }

    /**
     * Extracts the directory path without the filename, from a complete
     * filename path.
     *
     * @param pPath The full filename path.
     * @return the path without the filename.
     * @see File#getParent
     * @see #getFilename
     */
    public static String getDirectoryname(final String pPath) {
        return getDirectoryname(pPath, File.separatorChar);
    }

    /**
     * Extracts the directory path without the filename, from a complete
     * filename path.
     *
     * @param pPath The full filename path.
     * @param pSeparator the separator char used in {@code pPath}
     * @return the path without the filename.
     * @see File#getParent
     * @see #getFilename
     */
    public static String getDirectoryname(final String pPath, final char pSeparator) {
        int index = pPath.lastIndexOf(pSeparator);

        if (index < 0) {
            return "";  // Assume only filename
        }
        return pPath.substring(0, index);
    }

    /**
     * Extracts the filename of a complete filename path.
     *
     * @param pPath The full filename path.
     * @return the extracted filename.
     * @see File#getName
     * @see #getDirectoryname
     */
    public static String getFilename(final String pPath) {
        return getFilename(pPath, File.separatorChar);
    }

    /**
     * Extracts the filename of a complete filename path.
     *
     * @param pPath      The full filename path.
     * @param pSeparator The file separator.
     * @return the extracted filename.
     * @see File#getName
     * @see #getDirectoryname
     */
    public static String getFilename(final String pPath, final char pSeparator) {
        int index = pPath.lastIndexOf(pSeparator);

        if (index < 0) {
            return pPath; // Assume only filename
        }

        return pPath.substring(index + 1);
    }


    /**
     * Tests if a file or directory has no content.
     * A file is empty if it has a length of 0L. A non-existing file is also
     * considered empty.
     * A directory is considered empty if it contains no files.
     *
     * @param pFile The file to test
     * @return {@code true} if the file is empty, otherwise
     *         {@code false}.
     */
    public static boolean isEmpty(File pFile) {
        if (pFile.isDirectory()) {
            return (pFile.list().length == 0);
        }
        return (pFile.length() == 0);
    }

    /**
     * Gets the default temp directory for the system as a File.
     *
     * @return a {@code File}, representing the default temp directory.
     * @see File#createTempFile
     */
    public static File getTempDirFile() {
        return new File(getTempDir());
    }

    /**
     * Gets the default temp directory for the system.
     *
     * @return a {@code String}, representing the path to the default temp
     *         directory.
     * @see File#createTempFile
     */
    public static String getTempDir() {
        synchronized (FileUtil.class) {
            if (TEMP_DIR == null) {
                // Get the 'java.io.tmpdir' property
                String tmpDir = System.getProperty("java.io.tmpdir");

                if (StringUtil.isEmpty(tmpDir)) {
                    // Stupid fallback...
                    // TODO: Delegate to FileSystem?
                    if (new File("/temp").exists()) {
                        tmpDir = "/temp";  // Windows
                    }
                    else {
                        tmpDir = "/tmp";   // Unix
                    }
                }
                TEMP_DIR = tmpDir;
            }
        }
        return TEMP_DIR;
    }

    /**
     * Gets the contents of the given file, as a byte array.
     *
     * @param pFilename the name of the file to get content from
     * @return the content of the file as a byte array.
     * @throws IOException if the read operation fails
     */
    public static byte[] read(String pFilename) throws IOException {
        return read(new File(pFilename));
    }

    /**
     * Gets the contents of the given file, as a byte array.
     *
     * @param pFile the file to get content from
     * @return the content of the file as a byte array.
     * @throws IOException if the read operation fails
     */
    public static byte[] read(File pFile) throws IOException {
        // Custom implementation, as we know the size of a file
        if (!pFile.exists()) {
            throw new FileNotFoundException(pFile.toString());
        }

        byte[] bytes = new byte[(int) pFile.length()];
        InputStream in = null;

        try {
            // Use buffer size two times byte array, to avoid i/o bottleneck
            in = new BufferedInputStream(new FileInputStream(pFile), BUF_SIZE * 2);

            int off = 0;
            int len;
            while ((len = in.read(bytes, off, in.available())) != -1 && (off < bytes.length)) {
                off += len;
                //              System.out.println("read:" + len);
            }
        }
        // Just pass any IOException on up the stack
        finally {
            close(in);
        }

        return bytes;
    }

    /**
     * Reads all data from the input stream to a byte array.
     *
     * @param pInput The input stream to read from
     * @return The content of the stream as a byte array.
     * @throws IOException if an i/o error occurs during read.
     */
    public static byte[] read(InputStream pInput) throws IOException {
        // Create byte array
        ByteArrayOutputStream bytes = new FastByteArrayOutputStream(BUF_SIZE);

        // Copy from stream to byte array
        copy(pInput, bytes);

        return bytes.toByteArray();
    }
    
    /**
     * Writes the contents from a byte array to an output stream.
     *
     * @param pOutput   The output stream to write to
     * @param pData The byte array to write
     * @return {@code true}, otherwise an IOException is thrown.
     * @throws IOException if an i/o error occurs during write.
     */
    public static boolean write(OutputStream pOutput, byte[] pData) throws IOException {
        // Write data
        pOutput.write(pData);

        // If we got here, all is okay
        return true;
    }

    /**
     * Writes the contents from a byte array to a file.
     *
     * @param pFile The file to write to
     * @param pData The byte array to write
     * @return {@code true}, otherwise an IOException is thrown.
     * @throws IOException if an i/o error occurs during write.
     */
    public static boolean write(File pFile, byte[] pData) throws IOException {
        boolean success = false;
        OutputStream out = null;

        try {
            out = new BufferedOutputStream(new FileOutputStream(pFile));
            success = write(out, pData);
        }
        finally {
            close(out);
        }
        return success;
    }

    /**
     * Writes the contents from a byte array to a file.
     *
     * @param pFilename The name of the file to write to
     * @param pData     The byte array to write
     * @return {@code true}, otherwise an IOException is thrown.
     * @throws IOException if an i/o error occurs during write.
     */
    public static boolean write(String pFilename, byte[] pData) throws IOException {
        return write(new File(pFilename), pData);
    }

    /**
     * Deletes the specified file.
     *
     * @param pFile  The file to delete
     * @param pForce Forces delete, even if the parameter is a directory, and
     *               is not empty. Be careful!
     * @return {@code true}, if the file existed and was deleted.
     * @throws IOException if an i/o error occurs during delete.
     */
    public static boolean delete(final File pFile, final boolean pForce) throws IOException {
        if (pForce && pFile.isDirectory()) {
            return deleteDir(pFile);
        }
        return pFile.exists() && pFile.delete();
    }

    /**
     * Deletes a directory recursively.
     *
     * @param pFile the file to delete
     * @return {@code true} if the file was deleted sucessfully
     * @throws IOException if an i/o error occurs during delete.
     */
    private static boolean deleteDir(final File pFile) throws IOException {
        // Recusively delete all files/subfolders
        // Deletes the files using visitor pattern, to avoid allocating
        // a file array, which may throw OutOfMemoryExceptions for
        // large directories/in low memory situations
        class DeleteFilesVisitor implements Visitor<File> {
            private int failedCount = 0;
            private IOException exception = null;

            public void visit(final File pFile) {
                try {
                    if (!delete(pFile, true)) {
                        failedCount++;
                    }
                }
                catch (IOException e) {
                    failedCount++;
                    if (exception == null) {
                        exception = e;
                    }
                }
            }

            boolean succeeded() throws IOException {
                if (exception != null) {
                    throw exception;
                }
                return failedCount == 0;
            }
        }
        DeleteFilesVisitor fileDeleter = new DeleteFilesVisitor();
        visitFiles(pFile, null, fileDeleter);

        // If any of the deletes above failed, this will fail (or return false)
        return fileDeleter.succeeded() && pFile.delete();
    }

    /**
     * Deletes the specified file.
     *
     * @param pFilename The name of file to delete
     * @param pForce    Forces delete, even if the parameter is a directory, and
     *                  is not empty. Careful!
     * @return {@code true}, if the file existed and was deleted.
     * @throws java.io.IOException if deletion fails
     */
    public static boolean delete(String pFilename, boolean pForce) throws IOException {
        return delete(new File(pFilename), pForce);
    }

    /**
     * Deletes the specified file.
     *
     * @param pFile The file to delete
     * @return {@code true}, if the file existed and was deleted.
     * @throws java.io.IOException if deletion fails
     */
    public static boolean delete(File pFile) throws IOException {
        return delete(pFile, false);
    }

    /**
     * Deletes the specified file.
     *
     * @param pFilename The name of file to delete
     * @return {@code true}, if the file existed and was deleted.
     * @throws java.io.IOException if deletion fails
     */
    public static boolean delete(String pFilename) throws IOException {
        return delete(new File(pFilename), false);
    }

    /**
     * Renames the specified file.
     * If the destination is a directory (and the source is not), the source
     * file is simply moved to the destination directory.
     *
     * @param pFrom      The file to rename
     * @param pTo        The new file
     * @param pOverWrite Specifies if the tofile should be overwritten, if it
     *                   exists
     * @return {@code true}, if the file was renamed.
     *
     * @throws FileNotFoundException if {@code pFrom} does not exist.
     */
    public static boolean rename(File pFrom, File pTo, boolean pOverWrite) throws IOException {
        if (!pFrom.exists()) {
            throw new FileNotFoundException(pFrom.getAbsolutePath());
        }

        if (pFrom.isFile() && pTo.isDirectory()) {
            pTo = new File(pTo, pFrom.getName());
        }
        return (pOverWrite || !pTo.exists()) && pFrom.renameTo(pTo);

    }

    /**
     * Renames the specified file, if the destination does not exist.
     * If the destination is a directory (and the source is not), the source
     * file is simply moved to the destination directory.
     *
     * @param pFrom The file to rename
     * @param pTo   The new file
     * @return {@code true}, if the file was renamed.
     * @throws java.io.IOException if rename fails
     */
    public static boolean rename(File pFrom, File pTo) throws IOException {
        return rename(pFrom, pTo, false);
    }

    /**
     * Renames the specified file.
     * If the destination is a directory (and the source is not), the source
     * file is simply moved to the destination directory.
     *
     * @param pFrom      The file to rename
     * @param pTo        The new name of the file
     * @param pOverWrite Specifies if the tofile should be overwritten, if it
     *                   exists
     * @return {@code true}, if the file was renamed.
     * @throws java.io.IOException if rename fails
     */
    public static boolean rename(File pFrom, String pTo, boolean pOverWrite) throws IOException {
        return rename(pFrom, new File(pTo), pOverWrite);
    }

    /**
     * Renames the specified file, if the destination does not exist.
     * If the destination is a directory (and the source is not), the source
     * file is simply moved to the destination directory.
     *
     * @param pFrom The file to rename
     * @param pTo   The new name of the file
     * @return {@code true}, if the file was renamed.
     * @throws java.io.IOException if rename fails
     */
    public static boolean rename(File pFrom, String pTo) throws IOException {
        return rename(pFrom, new File(pTo), false);
    }

    /**
     * Renames the specified file.
     * If the destination is a directory (and the source is not), the source
     * file is simply moved to the destination directory.
     *
     * @param pFrom      The name of the file to rename
     * @param pTo        The new name of the file
     * @param pOverWrite Specifies if the tofile should be overwritten, if it
     *                   exists
     * @return {@code true}, if the file was renamed.
     * @throws java.io.IOException if rename fails
     */
    public static boolean rename(String pFrom, String pTo, boolean pOverWrite) throws IOException {
        return rename(new File(pFrom), new File(pTo), pOverWrite);
    }

    /**
     * Renames the specified file, if the destination does not exist.
     * If the destination is a directory (and the source is not), the source
     * file is simply moved to the destination directory.
     *
     * @param pFrom The name of the file to rename
     * @param pTo   The new name of the file
     * @return {@code true}, if the file was renamed.
     * @throws java.io.IOException if rename fails
     */
    public static boolean rename(String pFrom, String pTo) throws IOException {
        return rename(new File(pFrom), new File(pTo), false);
    }

    /**
     * Lists all files (and directories) in a specific folder.
     *
     * @param pFolder The folder to list
     * @return a list of {@code java.io.File} objects.
     * @throws FileNotFoundException if {@code pFolder} is not a readable file
     */
    public static File[] list(final String pFolder) throws FileNotFoundException {
        return list(pFolder, null);
    }

    /**
     * Lists all files (and directories) in a specific folder which are
     * embraced by the wildcard filename mask provided.
     *
     * @param pFolder       The folder to list
     * @param pFilenameMask The wildcard filename mask
     * @return a list of {@code java.io.File} objects.
     * @see File#listFiles(FilenameFilter)
     * @throws FileNotFoundException if {@code pFolder} is not a readable file
     */
    public static File[] list(final String pFolder, final String pFilenameMask) throws FileNotFoundException {
        if (StringUtil.isEmpty(pFolder)) {
            return null;
        }

        File folder = resolve(pFolder);
        if (!(/*folder.exists() &&*/folder.isDirectory() && folder.canRead())) {
            // NOTE: exists is implicitly called by isDirectory
            throw new FileNotFoundException("\"" + pFolder + "\" is not a directory or is not readable.");
        }

        if (StringUtil.isEmpty(pFilenameMask)) {
            return folder.listFiles();
        }

        // TODO: Rewrite to use regexp

        FilenameFilter filter = new FilenameMaskFilter(pFilenameMask);
        return folder.listFiles(filter);
    }

    /**
     * Creates a {@code File} based on the path part of the URL, for
     * file-protocol ({@code file:}) based URLs.
     *
     * @param pURL the {@code file:} URL
     * @return a new {@code File} object representing the URL
     *
     * @throws NullPointerException if {@code pURL} is {@code null}
     * @throws IllegalArgumentException if {@code pURL} is
     * not a file-protocol URL.
     *
     * @see java.io.File#toURI()
     * @see java.io.File#File(java.net.URI)
     */
    public static File toFile(URL pURL) {
        if (pURL == null) {
            throw new NullPointerException("URL == null");
        }

        // NOTE: Precondition tests below is based on the File(URI) constructor,
        //   and is most likely overkill...
        // NOTE: A URI is absolute iff it has a scheme component
        //   As the scheme has to be "file", this is implicitly tested below
        // NOTE: A URI is opaque iff it is absolute and it's shceme-specific
        //   part does not begin with a '/', see below
        if (!"file".equals(pURL.getProtocol())) {
            // URL protocol => URI scheme
            throw new IllegalArgumentException("URL scheme is not \"file\"");
        }
        if (pURL.getAuthority() != null) {
            throw new IllegalArgumentException("URL has an authority component");
        }
        if (pURL.getRef() != null) {
            // URL ref (anchor) => URI fragment
            throw new IllegalArgumentException("URI has a fragment component");
        }
        if (pURL.getQuery() != null) {
            throw new IllegalArgumentException("URL has a query component");
        }
        String path = pURL.getPath();
        if (!path.startsWith("/")) {
            // A URL should never be able to represent an opaque URI, test anyway
            throw new IllegalArgumentException("URI is not hierarchical");
        }
        if (path.isEmpty()) {
            throw new IllegalArgumentException("URI path component is empty");
        }

        // Convert separator, doesn't seem to be neccessary on Windows/Unix,
        // but do it anyway to be compatible...
        if (File.separatorChar != '/') {
            path = path.replace('/', File.separatorChar);
        }

        return resolve(path);
    }

    public static File resolve(String pPath) {
        return Win32File.wrap(new File(pPath));
    }

    public static File resolve(File pPath) {
        return Win32File.wrap(pPath);
    }

    public static File resolve(File pParent, String pChild) {
        return Win32File.wrap(new File(pParent, pChild));
    }

    public static File[] resolve(File[] pPaths) {
        return Win32File.wrap(pPaths);
    }

    // TODO: Handle SecurityManagers in a deterministic way
    // TODO: Exception handling
    // TODO: What happens if the file does not exist?
    public static long getFreeSpace(final File pPath) {
        // NOTE: Allow null, to get space in current/system volume
        File path = pPath != null ? pPath : new File(".");

        Long space = getSpace16("getFreeSpace", path);
        if (space != null) {
            return space;
        }

        return FS.getFreeSpace(path);
    }

    public static long getUsableSpace(final File pPath) {
        // NOTE: Allow null, to get space in current/system volume
        File path = pPath != null ? pPath : new File(".");

        Long space = getSpace16("getUsableSpace", path);
        if (space != null) {
            return space;
        }

        return getTotalSpace(path);
    }

    // TODO: FixMe for Windows, before making it public...
    public static long getTotalSpace(final File pPath) {
        // NOTE: Allow null, to get space in current/system volume
        File path = pPath != null ? pPath : new File(".");

        Long space = getSpace16("getTotalSpace", path);
        if (space != null) {
            return space;
        }

        return FS.getTotalSpace(path);
    }

    private static Long getSpace16(final String pMethodName, final File pPath) {
        try {
            Method freeSpace = File.class.getMethod(pMethodName);
            return (Long) freeSpace.invoke(pPath);
        }
        catch (NoSuchMethodException ignore) {}
        catch (IllegalAccessException ignore) {}
        catch (InvocationTargetException e) {
            Throwable throwable = e.getTargetException();
            if (throwable instanceof SecurityException) {
                throw (SecurityException) throwable;
            }
            throw new UndeclaredThrowableException(throwable);
        }

        return null;
    }

    /**
     * Formats the given number to a human readable format.
     * Kind of like {@code df -h}.
     *
     * @param pSizeInBytes the size in byte
     * @return a human readable string representation
     */
    public static String toHumanReadableSize(final long pSizeInBytes) {
        // TODO: Rewrite to use String.format?
        if (pSizeInBytes < 1024L) {
            return pSizeInBytes + " Bytes";
        }
        else if (pSizeInBytes < (1024L << 10)) {
            return getSizeFormat().format(pSizeInBytes / (double) (1024L)) + " KB";
        }
        else if (pSizeInBytes < (1024L << 20)) {
            return getSizeFormat().format(pSizeInBytes / (double) (1024L << 10)) + " MB";
        }
        else if (pSizeInBytes < (1024L << 30)) {
            return getSizeFormat().format(pSizeInBytes / (double) (1024L << 20)) + " GB";
        }
        else  if (pSizeInBytes < (1024L << 40)) {
            return getSizeFormat().format(pSizeInBytes / (double) (1024L << 30)) + " TB";
        }
        else {
            return getSizeFormat().format(pSizeInBytes / (double) (1024L << 40)) + " PB";
        }
    }

    // NumberFormat is not thread-safe, so we stick to thread-confined instances
    private static ThreadLocal<NumberFormat> sNumberFormat = new ThreadLocal<NumberFormat>() {
        protected NumberFormat initialValue() {
            NumberFormat format = NumberFormat.getNumberInstance();
            // TODO: Consider making this locale/platform specific, OR a method parameter...
//            format.setMaximumFractionDigits(2);
            format.setMaximumFractionDigits(0);
            return format;
        }
    };

    private static NumberFormat getSizeFormat() {
        return sNumberFormat.get();
    }

    /**
     * Visits all files in {@code pDirectory}. Optionally filtered through a {@link FileFilter}.
     *
     * @param pDirectory the directory to visit files in
     * @param pFilter the filter, may be {@code null}, meaning all files will be visited
     * @param pVisitor the visitor
     *
     * @throws IllegalArgumentException if either {@code pDirectory} or {@code pVisitor} are {@code null}
     *
     * @see com.twelvemonkeys.util.Visitor
     */
    @SuppressWarnings({"ResultOfMethodCallIgnored"})
    public static void visitFiles(final File pDirectory, final FileFilter pFilter, final Visitor<File> pVisitor) {
        Validate.notNull(pDirectory, "directory");
        Validate.notNull(pVisitor, "visitor");

        pDirectory.listFiles(new FileFilter() {
            public boolean accept(final File pFile) {
                if (pFilter == null || pFilter.accept(pFile)) {
                    pVisitor.visit(pFile);
                }

                return false;
            }
        });
    }
}
