/*******************************************************************************
 * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
 ******************************************************************************/
package fr.osug.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * File utility methods : Several utility methods : finds a file in the class path (jar), open files
 * for read or write operation and close file
 */
public final class FileUtils {

    /** class logger */
    private static Logger log = LoggerFactory.getLogger(FileUtils.class.getName());

    /** default read buffer capacity : DEFAULT_READ_BUFFER_SIZE = 8K */
    private static final int DEFAULT_READ_BUFFER_SIZE = 8 * 1024;
    /** default write buffer capacity : DEFAULT_WRITE_BUFFER_SIZE = 8K */
    private static final int DEFAULT_WRITE_BUFFER_SIZE = 8 * 1024;
    /** File encoding use UTF-8 */
    public static final String FILE_ENCODING = "UTF-8";

    /**
     * Forbidden FileUtils constructor
     */
    private FileUtils() {
        /* no-op */
    }

    public static File createDirectories(final String path) throws IOException {
        final File dir = new File(path).getCanonicalFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir;
    }

    /**
     * Find a file in the current classloader (application class Loader)
     *
     * @param fileName file name only no path included
     * @return URL to the file or null
     */
    public static URL getResource(final String fileName) {
        // Find properties in the classpath
        final URL url = FileUtils.class.getClassLoader().getResource(fileName);

        if (url == null) {
            throw new RuntimeException("Unable to find the file in classpath : " + fileName);
        }

        if (log.isInfoEnabled()) {
            log.info("FileUtils.getSystemFileInputStream : URL : " + url);
        }

        return url;
    }

    /**
     * Find a file in the current classloader (application class Loader)
     *
     * @param fileName file name only no path included
     * @return InputStream or RuntimeException if not found
     * @throws RuntimeException if not found
     */
    public static InputStream getSystemFileInputStream(final String fileName) {
        final URL url = getResource(fileName);

        try {
            return url.openStream();
        } catch (final IOException ioe) {
            throw new RuntimeException("Failure when loading file in classpath : " + fileName, ioe);
        }
    }

    /**
     * Close an inputStream
     *
     * @param in inputStream to close
     */
    public static void closeStream(final InputStream in) {
        if (in != null) {
            try {
                in.close();
            } catch (final IOException ioe) {
                log.error("FileUtils.closeStream : io close failure : ", ioe);
            }
        }
    }

    /**
     * Close an outputStream
     *
     * @param out outputStream to close
     */
    public static void closeStream(final OutputStream out) {
        if (out != null) {
            try {
                out.close();
            } catch (final IOException ioe) {
                log.error("FileUtils.closeStream : io close failure : ", ioe);
            }
        }
    }

    /**
     * Returns an exisiting File for the given path
     *
     * @param file file path
     * @return File or null
     */
    private static File getExistingFile(final File file) {
        if (file != null) {
            if (file.exists()) {
                return file;
            }
        }
        return null;
    }

    /**
     * Returns an exisiting File for the given path
     *
     * @param path file path
     * @return File or null
     */
    private static File getExistingFile(final String path) {
        if (path != null && !path.isEmpty()) {
            return getExistingFile(new File(path));
        }
        return null;
    }

    /**
     * Returns an existing directory for the given path
     *
     * @param path directory path
     * @return directory or null
     */
    public static File getDirectory(final String path) {
        final File dir = getExistingFile(path);

        if (dir != null && dir.isDirectory()) {
            return dir;
        }
        return null;
    }

    /**
     * Returns an existing directory for the given path
     *
     * @param path directory path
     * @return directory or null
     */
    public static File getRequiredDirectory(final String path) throws IOException {
        final File dir = getDirectory(path);

        if (dir != null) {
            return dir;
        }
        throw new FileNotFoundException("Invalid directory '" + ((dir == null) ? null : dir.getAbsolutePath()) + "' !");
    }

    /**
     * Returns an exisiting File for the given path
     *
     * @param path file path
     * @return File or null
     */
    public static File getFile(final String path) {
        final File file = getExistingFile(path);

        if (file != null && file.isFile()) {
            return file;
        }
        return null;
    }

    /**
     * Returns an exisiting File for the given path
     *
     * @param file file path
     * @return File or null
     */
    public static File getFile(final File file) {
        final File eFile = getExistingFile(file);

        if (eFile != null) {
            return file;
        }
        return null;
    }

    /**
     * Returns an existing File for the given path
     *
     * @param file file path
     * @return File or null
     */
    public static File getRequiredFile(final File file) throws IOException {
        final File eFile = getFile(file);

        if (eFile != null) {
            return eFile;
        }
        throw new FileNotFoundException("Invalid file '" + ((file == null) ? null : file.getAbsolutePath()) + "' !");
    }

    /**
     * Returns an existing File for the given path
     *
     * @param path file path
     * @return File or null
     */
    public static File getRequiredFile(final String path) throws IOException {
        final File file = getFile(path);

        if (file != null) {
            return file;
        }
        throw new FileNotFoundException("Invalid file '" + ((file == null) ? null : file.getAbsolutePath()) + "' !");
    }

    // writers :  
    /**
     * Returns a Writer for the given file path and use the default writer buffer capacity
     *
     * @param absoluteFilePath absolute file path
     * @return Writer (buffered) or null
     */
    public static Writer openFile(final String absoluteFilePath) {
        return openFile(absoluteFilePath, DEFAULT_WRITE_BUFFER_SIZE);
    }

    /**
     * Returns a Writer for the given file path and use the given buffer capacity
     *
     * @param absoluteFilePath absolute file path
     * @param bufferSize write buffer capacity
     * @return Writer (buffered) or null
     */
    public static Writer openFile(final String absoluteFilePath, final int bufferSize) {
        if (absoluteFilePath != null && !absoluteFilePath.isEmpty()) {
            return openFile(new File(absoluteFilePath), bufferSize);
        }

        return null;
    }

    /**
     * Returns a Writer for the given file and use the default writer buffer capacity
     *
     * @param file file to write
     * @return Writer (buffered) or null
     */
    public static Writer openFile(final File file) {
        return openFile(file, DEFAULT_WRITE_BUFFER_SIZE);
    }

    /**
     * Returns a Writer for the given file and use the given buffer capacity
     *
     * @param file file to write
     * @param bufferSize write buffer capacity
     * @return Writer (buffered) or null
     */
    public static Writer openFile(final File file, final int bufferSize) {
        try {
            return new BufferedWriter(new FileWriter(file), bufferSize);
        } catch (final IOException ioe) {
            log.error("FileUtils.openFile : io failure : ", ioe);
        }

        return null;
    }

    /**
     * Returns a Reader for the given file
     *
     * @param file file to read
     * @return Reader or null
     */
    public static Reader reader(final File file) throws IOException {
        return new InputStreamReader(new FileInputStream(file), FILE_ENCODING);
    }

    /**
     * Close the given writer
     *
     * @param w writer to close
     * @return null
     */
    public static Writer closeFile(final Writer w) {
        if (w != null) {
            try {
                w.close();
            } catch (final IOException ioe) {
                log.error("FileUtils.closeFile : io close failure : ", ioe);
            }
        }

        return null;
    }

    /**
     * Get the file name part without extension
     *
     * @param file file as File
     * @return the file name part without extension or null
     */
    public static String getFileNameWithoutExtension(final File file) {
        if (file != null) {
            return getFileNameWithoutExtension(file.getName());
        }
        return null;
    }

    /**
     * Get the file name part without extension
     *
     * @param fileName file name as String
     * @return the file name part without extension or null
     */
    public static String getFileNameWithoutExtension(final String fileName) {
        if (fileName != null) {
            final int pos = fileName.lastIndexOf('.');
            if (pos == -1) {
                return fileName;
            }
            if (pos > 0) {
                return fileName.substring(0, pos);
            }
        }
        return null;
    }

    /**
     * Read a text file from the given file
     *
     * @param file local file
     * @return text file content
     *
     * @throws IOException if an I/O exception occurred
     */
    public static String readFile(final File file) throws IOException {
        return readStream(new FileInputStream(file), (int) file.length());
    }

    /**
     * Read a text file from the given input stream into a string
     *
     * @param inputStream stream to load
     * @return text file content
     *
     * @throws IOException if an I/O exception occurred
     */
    public static String readStream(final InputStream inputStream) throws IOException {
        return readStream(inputStream, DEFAULT_READ_BUFFER_SIZE);
    }

    /**
     * Read a text file from the given input stream into a string
     *
     * @param inputStream stream to load
     * @param bufferCapacity initial buffer capacity (chars)
     * @return text file content
     *
     * @throws IOException if an I/O exception occurred
     */
    public static String readStream(final InputStream inputStream, final int bufferCapacity) throws IOException {

        String result = null;
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(inputStream, FILE_ENCODING));

            // Use one string buffer with the best guessed initial capacity:
            final StringBuilder sb = new StringBuilder(bufferCapacity);

            // Use a char buffer to consume reader using DEFAULT_BUFFER_CAPACITY:
            final char[] cbuf = new char[DEFAULT_READ_BUFFER_SIZE];

            int len;
            while ((len = reader.read(cbuf)) > 0) {
                sb.append(cbuf, 0, len);
            }

            result = sb.toString();

        } finally {
            closeFile(reader);
        }
        return result;
    }

    /**
     * Close the given reader
     *
     * @param r reader to close
     * @return null
     */
    public static Reader closeFile(final Reader r) {
        if (r != null) {
            try {
                r.close();
            } catch (final IOException ioe) {
                log.error("FileUtils.closeFile : io close failure : ", ioe);
            }
        }

        return null;
    }

    /**
     * Copy file
     *
     * @param src source file
     * @param dst destination file
     * @throws IOException if an I/O exception occurred
     * @throws FileNotFoundException if input file is not found
     */
    public static void copy(final File src, final File dst) throws IOException, FileNotFoundException {
        final InputStream in = new BufferedInputStream(new FileInputStream(src), DEFAULT_READ_BUFFER_SIZE);

        saveStream(in, dst);
    }

    /**
     * Save the given input stream as file.
     *
     * @param in input stream to save as file
     * @param dst destination file
     * @throws IOException if an I/O exception occurred
     */
    public static void saveStream(final InputStream in, final File dst) throws IOException {
        final OutputStream out = new BufferedOutputStream(new FileOutputStream(dst), DEFAULT_WRITE_BUFFER_SIZE);

        // Transfer bytes from in to out
        try {
            final byte[] buf = new byte[DEFAULT_READ_BUFFER_SIZE];

            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        } finally {
            closeStream(in);
            closeStream(out);
        }
    }

    public static long computeChecksum(final InputStream in) throws IOException {

        final ChecksumOutputStream cs = new ChecksumOutputStream();

        final byte[] buf = new byte[DEFAULT_READ_BUFFER_SIZE];

        int len;
        while ((len = in.read(buf)) > 0) {
            cs.write(buf, 0, len);
        }
        return cs.getChecksum(); // flush and close streams
    }

    public static String MD5(final InputStream in) throws IOException {
        try {
            final MessageDigest md = MessageDigest.getInstance("MD5");

            final byte[] buf = new byte[DEFAULT_READ_BUFFER_SIZE];

            int len;
            while ((len = in.read(buf)) > 0) {
                md.update(buf, 0, len);
            }
            final byte[] array = md.digest();

            final StringBuilder sb = new StringBuilder(32);
            for (int i = 0; i < array.length; ++i) {
                final String num = Integer.toHexString(array[i] & 0xFF); // max 2 digits
                if (num.length() == 1) {
                    sb.append('0');
                }
                sb.append(num);
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException nsae) {
            log.error("Unsupported algorithm", nsae);
        }
        return null;
    }
}