/****************************************************************************
 * Copyright (c) 2010-2012, EBM WebSourcing - 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 University of California, Berkeley 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 REGENTS 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 REGENTS AND 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.ebmwebsourcing.easycommons.io;

import static java.io.File.separator;

import java.io.File;
import java.io.IOException;
import java.util.Random;
import java.util.regex.Pattern;

import org.apache.commons.lang.SystemUtils;

/**
 * @author Marc Jambert - EBM WebSourcing
 * @author Christophe DENEUX - Linagora
 */
public final class FileSystemHelper {

    /**
     * Error message of {@link #getRelativePath(File, File)} when the source
     * path is null
     */
    private final static String ILLEGAL_ARGUMENT_EXCEPTION_MSG_SRC_NULL = "The source path is null";

    /**
     * Error message of {@link #getRelativePath(File, File)} when the target
     * path is null
     */
    private final static String ILLEGAL_ARGUMENT_EXCEPTION_MSG_TARGET_NULL = "The target path is null";

    /**
     * Error message of {@link #getRelativePath(File, File)} when the source
     * directory is not an absolute directory.
     */
    private final static String ILLEGAL_ARGUMENT_EXCEPTION_MSG_SRC_NOT_ABSOLUTE = "The source is not an absolute directory";

    /**
     * Error message of {@link #getRelativePath(File, File)} when the source
     * directory is not a directory.
     */
    private final static String ILLEGAL_ARGUMENT_EXCEPTION_MSG_SRC_NOT_DIR = "The source is not an directory";

    /**
     * Error message of {@link #getRelativePath(File, File)} when the target
     * directory is not an absolute directory.
     */
    private final static String ILLEGAL_ARGUMENT_EXCEPTION_MSG_TARGET_NOT_ABSOLUTE = "The target is not an absolute path";

    private final static String FILE_SEPARATOR_AS_REGEXP = Pattern.quote(separator);

    private static final Random random = new Random();

    private FileSystemHelper() {
    }

    private static final File getSystemTempDir() {
        String tmpDirPath = System.getProperty("java.io.tmpdir");
        assert tmpDirPath != null;
        File tmpDir = new File(tmpDirPath);
        assert tmpDir != null;
        return tmpDir;
    }

    public static File createTempDir() throws IOException {
        return createTempDir(getSystemTempDir(), "tmp");
    }

    public static File createTempDir(String prefix) throws IOException {
        return createTempDir(getSystemTempDir(), prefix);
    }

    public static File createTempDir(File parentDir, String prefix) throws IOException {
        File tempDir = null;
        while (true) {
            tempDir = new File(parentDir, prefix + random.nextLong());
            if (!tempDir.exists())
                break;
        }
        if (!tempDir.mkdir()) {
            throw new IOException(String.format("Impossible to create temp dir '%s'",
                    tempDir.getAbsolutePath()));
        }
        tempDir.deleteOnExit();
        return tempDir;
    }

    /**
     * Cleans a directory without deleting it.
     * 
     * @param directory
     *            directory to clean
     * @throws IOException
     *             in case cleaning is unsuccessful
     */
    public static void cleanDirectory(File directory) throws IOException {
        assert directory != null;
        if (!directory.exists()) {
            String message = directory + " does not exist";
            throw new IOException(message);
        }
        if (!directory.isDirectory()) {
            String message = directory + " is not a directory";
            throw new IOException(message);
        }

        File[] files = directory.listFiles();
        if (files == null) { // null if security restricted
            throw new IOException("Failed to list contents of " + directory);
        }

        IOException exception = null;
        for (int i = 0; i < files.length; i++) {
            File file = files[i];
            try {
                forceDelete(file);
            } catch (IOException ioe) {
                exception = ioe;
            }
        }

        if (null != exception) {
            throw exception;
        }
    }

    /**
     * Deletes a file. If file is a directory, delete it and all
     * sub-directories.
     * <p>
     * The difference between File.delete() and this method are:
     * <ul>
     * <li>A directory to be deleted does not have to be empty.</li>
     * <li>You get exceptions when a file or directory cannot be deleted.
     * (java.io.File methods returns a boolean)</li>
     * </ul>
     * 
     * @param file
     *            file or directory to delete, must not be <code>null</code>
     * @throws NullPointerException
     *             if the directory is <code>null</code>
     * @throws IOException
     *             in case deletion is unsuccessful
     */
    public static void forceDelete(File file) throws IOException {
        assert file != null;
        if (file.isDirectory()) {
            deleteDirectory(file);
        } else {
            if (!file.exists()) {
                return;
            }
            if (!file.delete()) {
                String message = "Unable to delete file: " + file;
                throw new IOException(message);
            }
        }
    }

    /**
     * Deletes a directory recursively.
     * 
     * @param directory
     *            directory to delete
     * @throws IOException
     *             in case deletion is unsuccessful
     */
    private static void deleteDirectory(File directory) throws IOException {
        if (!directory.exists()) {
            return;
        }

        cleanDirectory(directory);
        if (!directory.delete()) {
            String message = "Unable to delete directory " + directory + ".";
            throw new IOException(message);
        }
    }

    /**
     * Return relative path.
     * <p>
     * <b>WARNING:</b></br> Folder path must finished with the character:
     * {@link File.separator}
     * </p>
     * <ul>
     * Example:
     * <table border="1px">
     * <tr>
     * <td>source</td>
     * <td>target</td>
     * <td>relative path</td>
     * </tr>
     * <tr>
     * <td>/root/dir1/dir2/</td>
     * <td>/root/dir1/</td>
     * <td>../</td>
     * </tr>
     * <tr>
     * <td>/root/</td>
     * <td>/root/dir1/dir2/</td>
     * <td>dir1/dir2/</td>
     * </tr>
     * <tr>
     * <td>/root/dir1/dir2/</td>
     * <td>/root/test.xml</td>
     * <td>../../test.xml</td>
     * </tr>
     * </table>
     * 
     * 
     * @param source
     *            path of the source folder
     * @param target
     *            path of the target folder/file
     * @return the relative path
     * @deprecated Use {@link FileSystemHelper#getRelativePath(File, File)} that
     *             manages correctly non-relative paths and returns a correct
     *             value for relative sub-directories (<code>./dir1/dir2/</code>
     *             )
     */
    @Deprecated
    public static String getRelativePath(String source, String target) {
        // Determine if the target is a file
        String targetFile = null;
        if (!source.endsWith(separator)) {
            throw new IllegalArgumentException();
        }
        if (!target.endsWith(separator)) {
            int lastFolderIndex = target.lastIndexOf(separator) + 1;
            targetFile = target.substring(lastFolderIndex);
            target = target.substring(0, lastFolderIndex);
        }
        String[] sources = source.split(FILE_SEPARATOR_AS_REGEXP);
        String[] targets = target.split(FILE_SEPARATOR_AS_REGEXP);

        // Get the shortest of the two paths
        int length = sources.length < targets.length ? sources.length : targets.length;
        // Get the common part
        int common = 0;
        while (common < length) {
            if (sources[common].equals(targets[common])) {
                common++;
            } else {
                break;
            }
        }
        StringBuilder relativePathBuilder = new StringBuilder();
        for (int i = common; i < sources.length; i++) {
            relativePathBuilder.append(".." + separator);
        }

        for (int i = common; i < targets.length; i++) {
            relativePathBuilder.append(targets[i] + separator);
        }
        if (targetFile != null) {
            relativePathBuilder.append(targetFile);
        }
        return relativePathBuilder.toString();
    }

    /**
     * Return relative path.
     * <ul>
     * Example:
     * <table border="1px">
     * <tr>
     * <td><code>sourcePath</code></td>
     * <td><code>targetPath</code></td>
     * <td>relative path</td>
     * </tr>
     * <tr>
     * <td>/root/dir1/dir2/</td>
     * <td>/root/dir1/</td>
     * <td>../</td>
     * </tr>
     * <tr>
     * <td>/root/</td>
     * <td>/root/dir1/dir2/</td>
     * <td>./dir1/dir2/</td>
     * </tr>
     * <tr>
     * <td>/root/dir1/dir2/</td>
     * <td>/root/test.xml</td>
     * <td>../../test.xml</td>
     * </tr>
     * <tr>
     * <td>/root/</td>
     * <td>/notrelative/dir1/</td>
     * <td>/notrelative/dir1/</td>
     * </tr>
     * <tr>
     * <td>/root/dir1</td>
     * <td>/notrelative/test.xml</td>
     * <td>/notrelative/test.xml</td>
     * </tr>
     * </table>
     * 
     * 
     * @param sourcePath
     *            path of the source folder. <b>MUST</b> be an absolute directory. Not null.
     * @param targetPath
     *            path of the target folder/file to relativize against <code>sourcePath</code>. <b>MUST</b> be an
     *            absolute {@link File}. Not null
     * @return the relative path, or <code>targetPath</code> if paths are not relative
     * @throws IllegalArgumentException
     *             if parameters are invalid
     */
    public static String getRelativePath(final File sourcePath, final File targetPath) {
        // Check that the source path is not null
        if (sourcePath == null) {
            throw new IllegalArgumentException(ILLEGAL_ARGUMENT_EXCEPTION_MSG_SRC_NULL);
        }

        // Check that the target path is not null
        if (targetPath == null) {
            throw new IllegalArgumentException(ILLEGAL_ARGUMENT_EXCEPTION_MSG_TARGET_NULL);
        }

        // Check that the source path is an absolute path
        if (!sourcePath.isAbsolute()) {
            throw new IllegalArgumentException(ILLEGAL_ARGUMENT_EXCEPTION_MSG_SRC_NOT_ABSOLUTE);
        }

        // Check that the source path is a directory
        if (!sourcePath.isDirectory()) {
            throw new IllegalArgumentException(ILLEGAL_ARGUMENT_EXCEPTION_MSG_SRC_NOT_DIR);
        }

        // Check that the target path is an absolute path
        if (!targetPath.isAbsolute()) {
            throw new IllegalArgumentException(ILLEGAL_ARGUMENT_EXCEPTION_MSG_TARGET_NOT_ABSOLUTE);
        }

        final String source;
        final String target;
        final String sourceAbsolutePath = sourcePath.getAbsolutePath();
        final String targetAbsolutePath = targetPath.getAbsolutePath();
        if (SystemUtils.IS_OS_WINDOWS) {
            // On Windows, relative paths are on the same drive.
            // If the drive letters of the absolute paths are not the same, the paths will be not relative
            final String sourceDrive = sourceAbsolutePath.substring(0, 2);
            final String targetDrive = targetAbsolutePath.substring(0, 2);
            if (targetDrive.equals(sourceDrive)) {
                // Drive letters are the same
                source = sourceAbsolutePath.substring(2);
                target = targetAbsolutePath.substring(2);
            } else {
                // Drive letters are different
                return targetPath.getAbsolutePath();
            }
        } else {
            source = sourceAbsolutePath.substring(1);
            target = targetAbsolutePath.substring(1);
        }

        final String[] sources = source.split(FILE_SEPARATOR_AS_REGEXP);
        final String[] targets = target.split(FILE_SEPARATOR_AS_REGEXP);

        // Get the shortest of the two paths
        final int length = sources.length < targets.length ? sources.length : targets.length;
        // Get the common part
        int common = 0;
        while (common < length) {
            if (sources[common].equals(targets[common])) {
                common++;
            } else {
                break;
            }
        }
        if (common > 0) {
            final StringBuilder relativePathBuilder = new StringBuilder();
            if (common == sources.length) {
                // target is a subdir of source
                relativePathBuilder.append(".").append(separator);
            }
            for (int i = common; i < sources.length; i++) {
                relativePathBuilder.append("..").append(separator);
            }

            for (int i = common; i < targets.length; i++) {
                relativePathBuilder.append(targets[i]);
                if (i < targets.length - 1) {
                    relativePathBuilder.append(separator);
                }
            }
            return relativePathBuilder.toString();
        } else {
            return targetPath.getAbsolutePath();
        }
    }
}
