//;-*- mode: java -*-
/*
                        ACM-SL Commons

    Copyright (C) 2002-today  Jose San Leandro Armendariz
                              chous@acm-sl.org

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.

    You should have received a copy of the GNU General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-307  USA

    Thanks to ACM S.L. for distributing this library under the LGPL license.
    Contact info: jose.sanleandro@acm-sl.com

 ******************************************************************************
 *
 * Filename: FileUtils.java
 *
 * Author: Jose San Leandro Armendariz
 *
 * Description: Provides some useful methods when working with files.
 *
 */
package org.acmsl.commons.utils.io;

/*
 * Importing some ACM-SL classes.
 */
import org.acmsl.commons.Literals;
import org.acmsl.commons.patterns.Singleton;
import org.acmsl.commons.patterns.Utils;
import org.acmsl.commons.regexpplugin.Helper;
import org.acmsl.commons.regexpplugin.MalformedPatternException;
import org.acmsl.commons.regexpplugin.RegexpEngine;
import org.acmsl.commons.regexpplugin.RegexpEngineNotFoundException;
import org.acmsl.commons.regexpplugin.RegexpManager;
import org.acmsl.commons.regexpplugin.RegexpPluginMisconfiguredException;

/*
 * Importing some JDK1.3 classes.
 */
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.nio.channels.Channel;
import java.nio.charset.Charset;

/*
 * Importing Apache Commons-Logging classes.
 */
import org.apache.commons.logging.LogFactory;

/*
 * Importing JetBrains annotations.
 */
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Provides some useful methods when working with files.
 * @author <a href="mailto:chous@acm-sl.org"
           >Jose San Leandro Armendariz</a>
 */
public class FileUtils
    implements  Utils,
                Singleton
{
    /**
     * Retrieves the {@link File} associated to given {@link FileInputStream}, if possible.
     * @param inputStream the input stream.
     * @return the file.
     */
    @Nullable
    public File retrieveFile(@NotNull final FileInputStream inputStream)
    {
        @Nullable final File result;

        @Nullable String t_strPath = null;

        try
        {
            @Nullable final Field field = inputStream.getClass().getDeclaredField("path");
            field.setAccessible(true);
            t_strPath = (String) field.get(inputStream);
        }
        catch (@NotNull final Throwable noField)
        {
            System.err.println(noField.getMessage());
        }

        if (t_strPath == null)
        {
            result = null;
        }
        else
        {
            result = new File(t_strPath);
        }

        return result;
    }

    /**
     * Retrieves the file name from an absolute path.
     * @param filePath the path.
     * @return just the file name (no folder information).
     */
    @NotNull
    public String getFileName(@NotNull final String filePath)
    {
        @NotNull final String result;

        final int lastSeparatorPosition = filePath.lastIndexOf(File.separator);

        if (lastSeparatorPosition > -1)
        {
            if (lastSeparatorPosition < filePath.length() - 1)
            {
                result = filePath.substring(lastSeparatorPosition + 1);
            }
            else
            {
                result = "";
            }
        }
        else
        {
            result = filePath;
        }

        return result;
    }

    /**
     * Strips the last extension.
     * @param filePath the file path.
     * @return the file path, after removing the last extension.
     */
    @NotNull
    public String stripExtension(@NotNull final String filePath)
    {
        @NotNull final String result;

        final int lastDotPosition = filePath.lastIndexOf(".");

        if (lastDotPosition > -1)
        {
            result = filePath.substring(0, lastDotPosition);
        }
        else
        {
            result = filePath;
        }

        return result;
    }

    /**
     * Removes all extensions from a given file name.
     * @param filePath the path.
     * @return the file path, with no extensions.
     */
    @NotNull
    public String stripExtensions(@NotNull final String filePath)
    {
        @NotNull final String result;

        @NotNull String aux = filePath;
        @NotNull String aux2;

        do
        {
            aux2 = aux;
            aux = stripExtension(aux);
        }
        while (!aux.equals(aux2));

        result = aux;

        return result;
    }

    /**
     * Singleton implemented to avoid the double-checked locking.
     */
    private static class FileUtilsSingletonContainer
    {
        /**
         * The actual singleton.
         */
        @NotNull
        public static final FileUtils SINGLETON = new FileUtils();
    }

    /**
     * Retrieves a FileUtils instance.
     * @return such instance.
     */
    @NotNull
    public static FileUtils getInstance()
    {
        return FileUtilsSingletonContainer.SINGLETON;
    }

    /**
     * Protected constructor to avoid accidental instantiation.
     */
    protected FileUtils() {}

    /**
     * Reads a file pointed by given path, and returns its contents.
     * @param filePath the path to the file to be read.
     * @param charset the {@link Charset}.
     * @return the contents of the file.
     * @throws FileNotFoundException if the file is not found.
     * @throws SecurityException if the operation is forbidden because of
     * security manager settings.
     * @throws IOException if some I/O exception occurs.
     */
    @NotNull
    public String readFile(@NotNull final String filePath, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        return readFile(new File(filePath), charset);
    }

    /**
     * Reads a file and returns its contents.
     * @param file the file to be read.
     * @param charset the {@link Charset}.
     * @return the contents of the file.
     * @throws FileNotFoundException if the file is not found.
     * @throws SecurityException if the operation is forbidden because of
     * security manager settings.
     * @throws IOException if some I/O exception occurs.
     */
    @NotNull
    public char[] readFileContents(@NotNull final File file, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        @NotNull final char[] result;

        @Nullable FileInputStream t_fisFileStream = null;

        @Nullable InputStreamReader t_isFileReader = null;

            /*
             * To read file's contents it's better to use BufferedReader class.
             */
        @Nullable BufferedReader t_frPageBufferedReader = null;

        try
        {
            /*
             * Instantiate a FileReader object to read file's contents.
             */
            t_fisFileStream = new FileInputStream(file);

            t_isFileReader = new InputStreamReader(t_fisFileStream, charset);

            /*
             * To read file's contents it's better to use BufferedReader class.
             */
            t_frPageBufferedReader = new BufferedReader(t_isFileReader);

            if  (file.length() > Integer.MAX_VALUE)
            {
                throw
                    new IOException(
                        "File too large (" + file.length() + " bytes)");
            }

            /*
             * Next, I find out the necessary size of the array where file's
             * contents will be copied into.
             */
            result = new char[(int) file.length()];

            /*
             * Now I actually read the file, and fill the array.
             */
            t_frPageBufferedReader.read(
                result,
                0,
                result.length);
        }
        finally
        {
            if (t_frPageBufferedReader != null)
            {
                try
                {
                    t_frPageBufferedReader.close();
                }
                catch (final IOException cannotCloseStream)
                {
                    LogFactory.getLog(FileUtils.class).warn(
                        "Cannot close file", cannotCloseStream);
                }
            }
            if (t_isFileReader != null)
            {
                try
                {
                    t_isFileReader.close();
                }
                catch (final IOException cannotCloseStream)
                {
                    LogFactory.getLog(FileUtils.class).warn(
                        "Cannot close file", cannotCloseStream);
                }
            }
            if (t_fisFileStream != null)
            {
                try
                {
                    t_fisFileStream.close();
                }
                catch (final IOException cannotCloseStream)
                {
                    LogFactory.getLog(FileUtils.class).warn(
                        "Cannot close file", cannotCloseStream);
                }
            }
        }
        return result;
    }

    /**
     * Reads a file and returns its contents.
     * @param file the file to be read.
     * @param charset the {@link Charset}.
     * @return the contents of the file.
     * @throws FileNotFoundException if the file is not found.
     * @throws SecurityException if the operation is forbidden because of
     * security manager settings.
     * @throws IOException if some I/O exception occurs.
     */
    @NotNull
    public String readFile(@NotNull final File file, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        /*
         * we can use a String constructor that exactly
         * fits our needs.
         */
        return new String(readFileContents(file, charset));
    }

    /**
     * Reads a file by its path and returns its contents, if possible. If some
     * exception occurs, it's ignored, and returns an empty String. This method
     * is used to avoid declaring try/catch blocks in client code.
     * @param filePath the path to the file to be read.
     * @param charset the {@link Charset}.
     * @return the contents of the file, or empty if reading cannot be
     * accomplished.
     */
    @SuppressWarnings("unused")
    @NotNull
    public String readFileIfPossible(@NotNull final String filePath, @NotNull final Charset charset)
    {
        String result = null;

        try
        {
            result = readFile(filePath, charset);
        }
        catch  (final FileNotFoundException fileNotFoundException)
        {
            /*
            * We have chosen not to notify of exceptions, so this
            * block of code is only descriptive.
            */
            LogFactory.getLog(FileUtils.class).trace(
                "Cannot read file " + filePath,
                fileNotFoundException);
        }
        catch  (final SecurityException securityException)
        {
            /*
            * We have chosen not to notify of exceptions, so this
            * block of code is only descriptive.
            */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot read file " + filePath,
                securityException);
        }
        catch  (final IOException ioException)
        {
            /*
            * We have chosen not to notify of exceptions, so this
            * block of code is only descriptive.
            */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot read file " + filePath,
                ioException);
        }

        if (result == null)
        {
            result = "";
        }

        return result;
    }

    /**
     * Reads a file and returns its contents, if possible. If some exception
     * occurs, it's ignored, and returns an empty String. This method is used
     * to avoid declaring try/catch blocks in client code.
     * @param file the file to be read.
     * @param charset the {@link Charset}.
     * @return the contents of the file, or empty if reading cannot be
     * accomplished.
     */
    @SuppressWarnings("unused")
    @Nullable
    public String readFileIfPossible(@NotNull final File file, @NotNull final Charset charset)
    {
        @Nullable String result = null;

        try
        {
            result = readFile(file, charset);
        }
        catch  (final FileNotFoundException fileNotFoundException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).trace(
                "Cannot read file " + file,
                fileNotFoundException);
        }
        catch  (final SecurityException securityException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot read file " + file,
                securityException);
        }
        catch  (final IOException ioException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot read file " + file,
                ioException);
        }

        return result;
    }

    /**
     * Tries to read from given stream.
     * @param inputStream the stream to read from.
     * @return the contents read.
     * @throws IOException if the input stream could not be read or closed.
     */
    @NotNull
    public String read(@NotNull final InputStream inputStream)
        throws  IOException
    {
        @NotNull final BufferedInputStream bufferedInputStream;

        if  (inputStream instanceof BufferedInputStream)
        {
            bufferedInputStream = (BufferedInputStream) inputStream;
        }
        else
        {
            bufferedInputStream = new BufferedInputStream(inputStream);
        }

        return read(bufferedInputStream);
    }

    /**
     * Tries to read from given stream.
     * @param bufferedInputStream the stream to read from.
     * @return the contents read.
     * @throws IOException if the input stream could not be read or closed.
     */
    @NotNull
    public String read(@NotNull final BufferedInputStream bufferedInputStream)
        throws  IOException
    {
        @NotNull final StringWriter t_swResult = new StringWriter();

        @NotNull final PrintWriter t_Writer = new PrintWriter(t_swResult);

        int t_iReadChar = bufferedInputStream.read();

        while  (t_iReadChar != -1)
        {
            t_Writer.print((char) t_iReadChar);

            t_iReadChar = bufferedInputStream.read();
        }

        bufferedInputStream.close();

        t_Writer.close();

        return t_swResult.toString();
    }

    /**
     * Saves the contents to a file.
     * @param filePath the path of the file.
     * @param contents the text to save.
     * @param charset the file charset to use.
     * @return <code>true</code> if the process is successfully accomplished.
     */
    @SuppressWarnings("unused")
    public boolean writeFileIfPossible(
        @NotNull final String filePath, @NotNull final String contents, @NotNull final Charset charset)
    {
        return writeFileIfPossible(new File(filePath), contents, charset);
    }

    /**
     * Saves the contents to a file.
     * @param file the file to be overwritten.
     * @param contents the text to save.
     * @param charset the file charset to use.
     * @return <code>true</code> if the process is successfully accomplished.
     */
    public boolean writeFileIfPossible(
        @NotNull final File file, @NotNull final String contents, @NotNull final Charset charset)
    {
        boolean result = false;

        try
        {
            writeFile(file, contents, charset);
            result = true;
        }
        catch  (final FileNotFoundException fileNotFoundException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot write file " + file,
                fileNotFoundException);
        }
        catch  (final SecurityException securityException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot write file " + file,
                securityException);
        }
        catch  (final IOException ioException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot write file " + file,
                ioException);
        }

        return result;
    }

    /**
     * Writes a file referred by given path, with given contents.
     * @param filePath the path of the file.
     * @param contents the text to write.
     * @param charset the file charset to use.
     * @throws FileNotFoundException if the file is not found.
     * @throws SecurityException if the security manager forbids this
     * operation.
     * @throws IOException if any other I/O error occurs.
     */
    @SuppressWarnings("unused")
    public void writeFile(
        @NotNull final String filePath, @NotNull final String contents, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        writeFile(new File(filePath), contents, charset);
    }

    /**
     * Writes a file with given contents.
     * @param file the file to write.
     * @param contents the text to write.
     * @param charset the charset to use.
     * @throws SecurityException if the security manager forbids this
     * operation.
     * @throws IOException if any other I/O error occurs.
     */
    public void writeFile(
        @NotNull final File file, @NotNull final String contents, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        @Nullable FileOutputStream t_fosFileStream = null;

        @Nullable OutputStreamWriter t_osFileWriter = null;

        @Nullable final PrintWriter t_pwWriter;

        try
        {
            t_fosFileStream = new FileOutputStream(file);

            t_osFileWriter = new OutputStreamWriter(t_fosFileStream, charset);

            t_pwWriter = new PrintWriter(t_osFileWriter);

            t_pwWriter.println(contents);

            t_pwWriter.close();
        }
        finally
        {
            if (t_fosFileStream != null)
            {
                try
                {
                    t_fosFileStream.close();
                }
                catch (@NotNull final IOException cannotClose)
                {
                    LogFactory.getLog(FileUtils.class).error(
                        "Cannot close file", cannotClose);
                }
            }
            if (t_osFileWriter != null)
            {
                try
                {
                    t_osFileWriter.close();
                }
                catch (@NotNull final IOException cannotClose)
                {
                    LogFactory.getLog(FileUtils.class).error(
                        "Cannot close file", cannotClose);
                }
            }
        }
    }

    /**
     * Copies the contents of a file to another.
     * @param original the content to copy.
     * @param destination the file to be overwritten.
     * @param charset the {@link Charset}.
     * @throws SecurityException if the security manager forbids this
     * operation.
     * @throws IOException if any other I/O error occurs.
     */
    public void copy(@NotNull final File original, @NotNull final File destination, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        writeFile(destination, readFile(original, charset), charset);
    }

    /**
     * Copies the contents of a file (referred by given path) to another.
     * @param originalPath the path of the file to copy.
     * @param destinationPath the path of the file to be overwritten.
     * @param charset the {@link Charset}.
     * @return <code>true</code> if the operation ends up successfully.
     */
    @SuppressWarnings("unused")
    public boolean copyIfPossible(
        @NotNull final String originalPath, @NotNull final String destinationPath, @NotNull final Charset charset)
    {
        return
            copyIfPossible(new File(originalPath), new File(destinationPath), charset);
    }

    /**
     * Copies the contents of a file to another.
     * @param original the content to copy.
     * @param destination the file to be overwritten.
     * @param charset the {@link Charset}.
     * @return <code>true</code> if the operation ends up successfully.
     */
    public boolean copyIfPossible(
        @NotNull final File original, @NotNull final File destination, @NotNull final Charset charset)
    {
        boolean result = false;

        try
        {
            copy(original, destination, charset);

            result = true;
        }
        catch  (final FileNotFoundException fileNotFoundException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot copy file " + original + " to " + destination,
                fileNotFoundException);
        }
        catch  (final SecurityException securityException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot copy file " + original + " to " + destination,
                securityException);
        }
        catch  (final IOException ioException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot copy file " + original + " to " + destination,
                ioException);
        }

        return result;
    }

    /**
     * Moves a file.
     * @param originalFile the file to move.
     * @param destinationFile the new file.
     * @param charset the {@link Charset}.
     * @throws FileNotFoundException if the file is not found.
     * @throws SecurityException if the security manager forbids this
     * operation.
     * @throws IOException if any other I/O error occurs.
     */
    public void move(
        @NotNull final File originalFile, @NotNull final File destinationFile, @NotNull final Charset charset)
        throws  SecurityException,
                IOException
    {
        copy(originalFile, destinationFile, charset);

        if (!originalFile.delete())
        {
            throw new IOException("Cannot delete " + originalFile.getAbsolutePath());
        }
    }

    /**
     * Moves a file, if possible.
     * @param originalFile the file to move.
     * @param destinationFile the new file.
     * @param charset the {@link Charset}.
     * @return <code>true</code> if the operation ends up successfully.
     */
    public boolean moveIfPossible(
        @NotNull final File originalFile, @NotNull final File destinationFile, @NotNull final Charset charset)
    {
        boolean result = false;

        try
        {
            move(originalFile, destinationFile, charset);
            result = true;
        }
        catch  (final FileNotFoundException fileNotFoundException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot move file " + originalFile + " to " + destinationFile,
                fileNotFoundException);
        }
        catch  (final SecurityException securityException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot move file " + originalFile + " to " + destinationFile,
                securityException);
        }
        catch  (final IOException ioException)
        {
            /*
             * We have chosen not to notify of exceptions, so this
             * block of code is only descriptive.
             */
            LogFactory.getLog(FileUtils.class).info(
                "Cannot move file " + originalFile + " to " + destinationFile,
                ioException);
        }

        return result;
    }

    /**
     * Moves a file from one path to another, if possible.
     * @param filePath the path of the file to move.
     * @param destinationPath the new file's path.
     * @param charset the {@link Charset}.
     * @return <code>true</code> if the operation ends up successfully.
     */
    @SuppressWarnings("unused")
    public boolean moveIfPossible(
        @NotNull final String filePath, @NotNull final String destinationPath, @NotNull final Charset charset)
    {
        return
            moveIfPossible(
                new File(filePath),
                new File(destinationPath),
                charset);
    }

    /**
     * Translates given package name to a relative path.
     * @param packageName the package name.
     * @return the path.
     */
    @Nullable
    public String packageToPath(@NotNull final String packageName)
    {
        String result = null;

        try
        {
            @Nullable final Helper t_Helper = createHelper(RegexpManager.getInstance());

            if (t_Helper != null)
            {
                result =
                    t_Helper.replaceAll(packageName, "\\.", File.separator);
            }
        }
        catch  (final MalformedPatternException malformedPatternException)
        {
            /*
             * This exception is about pattern mismatch. In my opinion,
             * it's an error that should be detected at compile time,
             * but regexp API design cannot provide this functionality,
             * since all patterns are defined with strings, and therefore
             * they escape all compiler checks.
             */
            LogFactory.getLog(FileUtils.class).warn(
                Literals.MALFORMED_STATIC_PATTERNS_ARE_FATAL_CODING_ERRORS,
                malformedPatternException);
        }
        catch  (final RegexpEngineNotFoundException regengNotFoundException)
        {
            /*
             * This exception is thrown only if no regexp library is available
             * at runtime. Not only this one, but any method provided by this
             * class that use regexps will not work.
             */
            LogFactory.getLog(FileUtils.class).fatal(
                Literals.CANNOT_FIND_ANY_REGEXP_ENGINE,
                regengNotFoundException);
        }
        catch  (final RegexpPluginMisconfiguredException
                      regexpPluginMisconfiguredException)
        {
            /*
             * This exception is thrown if RegexpPlugin cannot be configured
             * properly at runtime. Not only this one, but any method provided
             * by thisclass that use regexps will not work.
             */
            LogFactory.getLog(FileUtils.class).fatal(
                "Cannot configure RegexpPlugin.",
                regexpPluginMisconfiguredException);
        }

        return result;
    }

    /**
     * Creates the helper.
     * @return the regexp helper.
     */
    @Nullable
    protected static synchronized Helper createHelper()
    {
        return createHelper(RegexpManager.getInstance());
    }

    /**
     * Creates the helper.
     * @param regexpManager the RegexpManager instance.
     * @return the regexp helper.
     */
    @Nullable
    protected static synchronized Helper createHelper(
        @NotNull final RegexpManager regexpManager)
    {
        return createHelper(regexpManager.getEngine());
    }

    /**
     * Creates the helper.
     * @param regexpEngine the RegexpEngine instance.
     * @return the regexp helper.
     */
    @Nullable
    protected static synchronized Helper createHelper(
        @NotNull final RegexpEngine regexpEngine)
    {
        return regexpEngine.createHelper();
    }
}
