package textbender.g.io; // Copyright 2004-2007, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Textbender Software"), to deal in the Textbender Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Textbender Software, and to permit persons to whom the Textbender Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Textbender Software. THE TEXTBENDER SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE TEXTBENDER SOFTWARE OR THE USE OR OTHER DEALINGS IN THE TEXTBENDER SOFTWARE.

import java.io.*;
import java.util.regex.*;
import textbender.g.lang.*;
import textbender.g.util.logging.*;


/** File utilities.
  */
public @ThreadSafe final class FileX
{

    private FileX() {}



    /** Pattern to split a filename (or path) into two groups: body and dot-extension.
      * The body group (1) may include separators '/'.
      * It will be neither empty nor null.
      * <p>
      *     The extension group (2) will either include the preceding dot '.',
      *     or it will be empty. It will not be null.
      *     </p>
      */
    public static final Pattern BODY_DOTX_PATTERN = Pattern.compile( "^(.+?)((?:\\.[^.]*)?)$" );



    /** @param target pathname to create as copy (will be overwritten if it already exists)
      * @param source file or directory to copy
      *     (directory contents are recursively copied using {@link #copyTo(File,File) copyTo})
      */
    public static void copyAs( File target, File source ) throws IOException
    {
        copyAs( target, source, FileFilterX.TRANSPARENT );
    }



    /** Copies a file to a new file named as specified.
      * No checking is done to prevent self-copying.
      *
      *     @param target pathname to create as copy (will be overwritten if it already exists)
      *     @param source file or directory to copy
      *         (directory contents are recursively copied
      *         using {@link #copyTo(File,File,FileFilter) copyTo})
      *     @param fileFilter to use, if the source is a directory
      *         (filter is applied to contents of directory,
      *         and recursively to contents of any sub-directories that themselves pass the filter)
      */
    public static void copyAs( File target, File source, FileFilter fileFilter )

        throws IOException
    {
        if( source.isDirectory() )
        {
          // First the directory itself.
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            if( target.isDirectory() ) // clean it out, in case not empty:
            {
                if( !deleteRecursiveFrom(target) ) throw new IOException( "unable to delete directory contents of " + target );
            }
            else if( !target.mkdir() ) throw new IOException( "unable to create directory " + target );

          // Then its contents, if any.
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            File[] subSourceArray = source.listFiles( fileFilter );
            for( int i=0; i<subSourceArray.length; ++i )
            {
                copyTo( target, subSourceArray[i] );
            }
        }
     // else if( source.isFile() )
        else // file (perhaps not a normal one, though)
        {
            InputStream input = new BufferedInputStream( new FileInputStream( source ));
            try
            {
                OutputStream output = new BufferedOutputStream( new FileOutputStream( target ));
                try
                {
                    for( ;; ) { int i = input.read(); if( i == -1 ) break; output.write( i ); }
                }
                finally{ output.close(); }
            }
            finally{ input.close(); }
        }
    }



    /** @param targetDirectory in which to make copy (must already exist)
      * @param source file or directory to copy (directories are recursively copied)
      *     overwriting any existing file or directory of the same name
      */
    public static void copyTo( File targetDirectory, File source ) throws IOException
    {
        copyTo( targetDirectory, source, FileFilterX.TRANSPARENT );
    }



    /** @param targetDirectory in which to make copy (must already exist)
      * @param source file or directory to copy (directories are recursively copied)
      *     overwriting any existing file or directory of the same name
      * @param fileFilter to use, if the source is a directory
      *     (filter is applied to contents of directory,
      *     and recursively to contents of any sub-directories that themselves pass the filter)
      */
    public static void copyTo( File targetDirectory, File source, FileFilter fileFilter )

        throws IOException
    {
        File target = new File( targetDirectory, source.getName() );
        copyAs( target, source, fileFilter );
    }



    /** Atomically creates a new, empty directory
      * if and only if the directory does not yet exist.
      * Similar to File.createNewFile(), but creates a directory.
      * Also creates any necessary parent directories.
      *
      *     @param directory to create if it does not already exist
      *     @return true if the directory was created; false if it already existed
      *
      *     @throws IOException if the directory did not exist, and could not be created
      */
    public static boolean createNewDirectory( File directory ) throws IOException // 'ensureDirectory' would be better, except 'createNewDirectory' is consistent with File's 'createNewFile'
    {
        if( directory.isDirectory() ) return false;

        if( directory.mkdirs() )  return true;

        if( directory.isDirectory() ) return false; // checking once again, in case failure was caused by another thread that created the directory

        throw new IOException( "unable to create directory: " + directory );
    }



    /** As per File.delete() except it will work with non-empty directories.
      *
      *     @param fileOrDirectory to delete
      *     @return true if entirely deleted; false otherwise
      *
      *     @throws SecurityException if the application does not have permission to delete the fileOrDirectory
      */
    public static boolean deleteRecursive( File fileOrDirectory )
    {
        if( fileOrDirectory.isFile() ) return fileOrDirectory.delete();
        else return deleteRecursiveFrom(fileOrDirectory) && fileOrDirectory.delete();
    }



    /** Same as {@link #deleteRecursive(File) deleteRecursive()},
      * except it works only on the contents of the specified directory.
      *
      *     @param directory whose contents to delete
      *     @return true if deleted; false otherwise
      *
      *     @throws SecurityException if the application does not have permission to delete the fileOrDirectory
      */
    public static boolean deleteRecursiveFrom( File directory )
    {
        boolean deletedAll = true; // till proven false
        for( File file : directory.listFiles() ) deletedAll = deletedAll && deleteRecursive( file );
        return deletedAll;
    }



    /** Tests whether or not the file system is case sensitive,
      * and returns true iff it is.
      */
    public static boolean testsCaseSensitive()
    {
        File lower = new File( "a" );
        File upper = new File( "A" );
        return !lower.equals( upper );
    }



    /** Tries hard to rename a file, despite bug 6213298.
      */
    public static boolean renameFrom( File oldFile, File newFile ) // Was renameTo(oldFile,newFile) which is confusing. Corrected by renaming method, instead of swapping parameters, to avoid silent breakage of old code.
    {
        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6213298
        // Submitter's note follows.
        //
        // HACK - I should just be able to call renameTo() here and return its
        // result. In fact, I used to do just that and this method always worked
        // fine. Now with this new version of Java (1.5.0), rename (and other
        // file methods) sometimes don't work on the first try. This is because
        // file objects that have been closed are hung onto, pending garbage
        // collection. By suggesting garbage collection, the next time, the
        // renameTo() usually (but not always) works.

        if( oldFile.renameTo( newFile )) return true;

        System.gc();
        ThreadX.trySleep( 50/*ms*/ );
        if( oldFile.renameTo( newFile )) return true;

        System.gc();
        ThreadX.trySleep( 450/*ms*/ );
        if( oldFile.renameTo( newFile )) return true;

        System.gc();
        ThreadX.trySleep( 1500/*ms*/ );
        if( oldFile.renameTo( newFile )) return true;

        return false;
    }



    /** First tries renameFrom, failing that it makes a copy.
      * Does not delete the old file.
      *
      *     @return true if it had to resort to copying; false if the rename worked
      */
    public static boolean renameFromDefaultsToCopy( File oldFile, File newFile ) throws IOException
    {
        if( renameFrom( oldFile, newFile )) return false;

        ThreadX.trySleep( 500/*ms*/ );
        LoggerX.i(FileX.class).fine( "rename failed, defaulting to copy: " + oldFile );
        copyAs( newFile, oldFile );
        return true;
    }



 // /** @return Safe name for a file, based on originalName supplied. This implementation checks
 //   *     content only (not length). If all characters are recognized, then the originalName
 //   *     reference is returned. Otherwise returns a copy of the originalName with any
 //   *     unrecognized characters converted to underscores. Recognizes only alphanumeric
 //   *     characters and underscores. All others, including the period '.' character, are
 //   *     converted to underscores.
 //   */ @Deprecated // in favour of ~.io.FileNameEncoder
 // public static String safeName( String originalName )
 // {
 //     char[] array = originalName.toCharArray();
 //     boolean arrayChanged = false; // till proven otherwise
 //     for( int i=0; i<array.length; ++i )
 //     {
 //         char c = array[i];
 //         if(  c!='_' && !Character.isLetterOrDigit(c) )
 //         {
 //             array[i] = '_';
 //             arrayChanged = true;
 //         }
 //     }
 //     if( arrayChanged )
 //         return new String( array );
 //     else
 //         return originalName;
 // }



    /** Returns a new instance of file system's temporary directory,
      * as defined by System property 'java.io.tmpdir'.
      */
    public static File tempDirectory()
    {
        return new File( System.getProperty( "java.io.tmpdir" ));
    }



}