/*
 * @(#)FileUtils.java
 *
 * Copyright (c) 2002-2003 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.file;

import com.jscape.util.IOTools;
import com.jscape.util.regex.JDKMatcherFactory;
import com.jscape.util.regex.Matcher;

import java.io.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;


/**
 * A set of utility methods for performing common file operations not easily handled in the Java API.
 */
public class FileUtils {

    private Set<FileUtilsListener> fileUtilsListeners = new CopyOnWriteArraySet<FileUtilsListener>();
    private volatile boolean verifyFileDate = false;
    private volatile int olderThanDays = 0;

    /**
     * Constructs a new FileUtils instance.
     */
    public FileUtils() {
    }

    /**
     * Gets contents of file.
     *
     * @param file the File to retrieve
     *
     * @return the contents of file
     *
     * @throws IOException if an I/O related error occurs
     */
    public byte[] getContents(File file)
            throws IOException {
        return IOTools.readFile(file);
    }

    /**
     * Adds FileUtilsEventListener.
     *
     * @param listener The new listener
     */
    public void addFileUtilsListener(FileUtilsListener listener) {
        fileUtilsListeners.add(listener);
    }

    /**
     * Appends a source file to the end of a destination file.
     *
     * @param src  the source file to read from
     * @param dest the destination file to append to
     *
     * @throws IOException if an I/O related error occurs
     */
    public void append(File src, File dest)
            throws IOException {
        if (src == null) {
            return;
        }

        if (src.isFile() && !dest.isDirectory()) {
            IOTools.writeFile(src, new FileOutputStream(dest, true));
        } else if (src.isDirectory()) {
            throw new IOException("Unable to append from source directory " + src.getAbsolutePath());
        } else if (dest.isDirectory()) {
            throw new IOException("Unable to append to destination directory " + dest.getAbsolutePath());
        }
    }

    /**
     * Appends specified data to the end of a destination file.
     *
     * @param src  the data to append
     * @param dest the destination file to append to
     *
     * @throws IOException if an I/O related error occurs
     */
    public void append(byte[] src, File dest)
            throws IOException {
        IOTools.writeFile(src, new FileOutputStream(dest, true));
    }

    /**
     * Appends specified data to the end of a destination file.
     *
     * @param src  the source data to append
     * @param off  the start offset in the data
     * @param len  the number of bytes to append
     * @param dest the destination file to append to
     *
     * @throws IOException if an I/O related error occurs
     */
    public void append(byte[] src, int off, int len, File dest)
            throws IOException {
        OutputStream out = new FileOutputStream(dest, true);

        try {
            out.write(src, off, len);
        } finally {
            IOTools.close(out);
        }
    }

    /**
     * Appends multiple files to a destination file.
     *
     * @param src  the files to read from
     * @param dest the destination file to append to
     *
     * @throws IOException if an I/O related error occurs
     */
    public void append(File[] src, File dest)
            throws IOException {
        for (File aSrc : src) {
            append(aSrc, dest);
        }
    }

    /**
     * Appends multiple files to a destination file.
     *
     * @param src  the files to read from
     * @param dest the destination file to append to
     *
     * @throws IOException if an I/O related error occurs
     */
    public void append(Enumeration src, File dest)
            throws IOException {
        while (src.hasMoreElements()) {
            append((File) src.nextElement(), dest);
        }
    }

    /**
     * Deletes the contents of one directory.
     *
     * @param dir The directory which will be deleted its contents.
     *
     * @throws IOException If a I/O error occurs
     */
    public void cleanDirectory(File dir)
            throws IOException {
        if ((dir != null) && dir.exists() && dir.isDirectory()) {
            String path = dir.getAbsolutePath();

            String[] contents = dir.list();

            if (contents == null) {
                return;
            }

            for (String file : contents) {
                delete(new File(dir, file));
            }
        }
    }

    /**
     * Copies a source file or directory to a destination file or directory.
     *
     * @param src  the source file or directory to copy from
     * @param dest the destination file or directory to copy to
     *
     * @throws IOException if an I/O error occurs during copy.
     */
    public void copy(File src, File dest)
            throws IOException {
        // check if source exists
        if ((src != null) && src.exists()) {
            if (src.isFile() && !dest.isDirectory()) {
                copyFileToFile(src, dest);
            } else if (src.isFile() && dest.isDirectory()) {
                copyFileToDir(src, dest);
            } else if (src.isDirectory() && dest.isDirectory()) {
                copyDirToDir(src, dest);
            } else {
                throw new IOException("Cannot copy directory " + src.getAbsolutePath() + " to file " + dest.getAbsolutePath());
            }
        } else {
            throw new IOException("Source file " + (src != null ? src.getAbsolutePath() : "null") + " does not exist");
        }
    }

    /**
     * Copies a File[] of source files and or directories to a destination directory.
     *
     * @param src  the source file or directory to copy from
     * @param dest the destination directory to copy to
     *
     * @throws IOException if an I/O error occurs during copy.
     */
    public void copy(File[] src, File dest)
            throws IOException {
        if (src == null) {
            return;
        }

        for (File aSrc : src) {
            copy(aSrc, dest);
        }
    }

    /**
     * Copies an <code>Enumeration</code> of source files and or directories to a destination directory.
     *
     * @param src  an <code>Enumeration</code> of files and or directories
     * @param dest the destination directory to copy to
     *
     * @throws IOException if an I/O error occurs during copy.
     */
    public void copy(Enumeration src, File dest)
            throws IOException {
        while (src.hasMoreElements()) {
            copy((File) src.nextElement(), dest);
        }
    }

    /**
     * Copies a source directory to a destination directory.
     *
     * @param src  the source directory to copy
     * @param dest the destination directory
     * @param full Indicates if it will copy the directory itself and its contents
     *
     * @throws IOException if an I/O related error occurs
     */
    public void copyDirToDir(File src, File dest, boolean full)
            throws IOException {

        if (src != null && src.exists()
                && src.isDirectory()) {
            File dir = null;

            if (full) {
                dir = new File(dest, src.getName());
            } else {
                dir = dest;
            }

            // check if dest directory exists
            if (!directoryExists(dir)) {
                boolean created = dir.mkdirs();
                if (!created) {
                    throw new IOException("Error creating directory.");
                }

                copyFileAttributes(src, dir);
            }

            // copy all files in src to destination directory
            File[] files = src.listFiles();

            if (files != null) {
                for (int i = 0; i < files.length; ++i) {
                    File tmp = files[i];

                    if (tmp.isFile() && dir.isDirectory()) {
                        copyFileToDir(tmp, dir);
                    } else if (tmp.isDirectory() && dir.isDirectory()) {
                        copyDirToDir(tmp, dir, true);
                    } else {
                        throw new IOException("Cannot copy directory " + src.getAbsolutePath() + " to file " + dest.getAbsolutePath());
                    }

                }
            }
        }
    }

    /**
     * Deletes a file or directory from system.
     * If source is a directory then a recursive delete is performed.
     *
     * @param src the file or directory to delete
     *
     * @throws IOException if an I/O related error occurs
     */
    public void delete(File[] src)
            throws IOException {
        for (File aSrc : src) {
            delete(aSrc);
        }
    }

    /**
     * Delete files.
     *
     * @param dir   - the directory.
     * @param regex - regex expression.
     *
     * @throws Exception exception.
     */
    public void delete(File dir, String regex)
            throws Exception {
        boolean error = false;

        if (dir == null) {
            dir = new File(".");
        }

        // escape regular expression if needed

        regex = convertRegex(regex);

        File[] files = dir.listFiles();
        Matcher matcher = new JDKMatcherFactory().setCaseInsensitive().matcherFor(regex);

        if (files == null) {
            return;
        }

        for (File file : files) {
            if (file.isFile()) {
                if (matcher.matches(file.getName())) {
                    try {
                        delete(file);
                    } catch (IOException ioe) {
                        error = true;
                    }
                }
            }
        }

        if (error) {
            throw new IOException("Unable to delete all files from local directory " + dir.getAbsolutePath());
        }
    }

    /**
     * Deletes a file or directory from system.
     * If source is a directory then a recursive delete is performed.
     *
     * @param src the file or directory to delete
     *
     * @throws IOException if an I/O related error occurs
     */
    public void delete(File src)
            throws IOException {
        if ((src != null) && src.exists()) {
            String path = src.getAbsolutePath();

            // delete file
            if (src.isFile()) {
                if (!this.verifyFileDate || couldBeDeleted(src, this.olderThanDays)) {
                    // delete file
                    if (src.delete()) {
                        // fire deleted event
                        fireFileDeleted(path);
                    } else {
                        throw new IOException("Unable to delete file " + path);
                    }
                }
            } else {
                // delete all files in directory
                File[] files = src.listFiles();

                if (files != null) {
                    for (int i = 0; i < files.length; ++i) {
                        delete(files[i]);
                    }
                }

                // fire deleted event
                if (!this.verifyFileDate || src.listFiles().length == 0) {
                    if (src.delete()) {
                        fireFileDeleted(path);
                    } else {
                        throw new IOException("Unable to delete file " + path);
                    }
                }
            }
        }
    }

    /**
     * Moves a source file or directory to a destination file or directory.
     *
     * @param src  the source file or directory to move
     * @param dest the destination file or directory to move to
     *
     * @throws IOException if an I/O error occurs during move.
     */
    public void move(File src, File dest)
            throws IOException {
        File[] f = { src };
        move(f, dest);
    }

    /**
     * Moves a File[] of source files and or directories to a destination directory.
     *
     * @param src  the source files and or directories to move
     * @param dest the destination directory to move to
     *
     * @throws IOException if an I/O error occurs during move.
     */
    public void move(File[] src, File dest)
            throws IOException {
        if (src != null) {
            // copy files
            copy(src, dest);

            // delete files
            delete(src);
        } else {
            throw new IOException("Unable to perform move source array is null");
        }
    }

    /**
     * Moves an <code>Enumeration</code> of source files and or directories to a destination directory.
     *
     * @param src  an <code>Enumeration</code> of files and or directories
     * @param dest the destination directory to copy to
     *
     * @throws IOException if an I/O error occurs during copy.
     */
    @SuppressWarnings("unchecked")
    public void move(Enumeration src, File dest)
            throws IOException {
        Vector files = new Vector();

        while (src.hasMoreElements()) {
            files.add(src.nextElement());
        }

        File[] arr = new File[files.size()];

        for (int i = 0; i < files.size(); i++) {
            arr[i] = (File) files.get(i);
        }

        //File[] arr = (File[]) files.toArray();
        move(arr, dest);
    }

    /**
     * Removes specified FileUtilsEventListener.
     *
     * @param listener Listener will be removed
     *
     * @see com.jscape.file.FileUtilsListener
     */
    public void removeFileUtilsListener(FileUtilsListener listener) {
        fileUtilsListeners.remove(listener);
    }

    /**
     * Sets the older days which will be used to delete the files.
     *
     * @param olderThanDays The number of older days.
     */
    public void setOlderThanDays(final int olderThanDays) {
        this.olderThanDays = olderThanDays;
    }

    /**
     * Sets the verifile date indicator.
     *
     * @param verifyFileDate The verify file date when files will be deleted.
     */
    public void setVerifyFileDate(final boolean verifyFileDate) {
        this.verifyFileDate = verifyFileDate;
    }

    /**
     * Fires <code>FileCopiedEvent</code>.
     * This event is fired upon copying a file or directory.
     *
     * @param src  the source file or directory copied
     * @param dest the destination of the source file or directory
     *
     * @see FileCopiedEvent
     */
    protected void fireFileCopied(final File src, final File dest) {
        FileCopiedEvent event = new FileCopiedEvent(this, src.getAbsolutePath(), dest.getAbsolutePath());

        for (FileUtilsListener listener : fileUtilsListeners) {
            listener.fileCopied(event);
        }
    }

    /**
     * Fires <code>FileDeletedEvent</code>.
     * This event is fired upon deleting a file or directory.
     *
     * @param src the absolute path to the file or directory deleted.
     *
     * @see FileDeletedEvent
     */
    protected void fireFileDeleted(final String src) {
        FileDeletedEvent event = new FileDeletedEvent(this, src);
        for (FileUtilsListener listener : fileUtilsListeners) {
            listener.fileDeleted(event);
        }
    }

    /**
     * Fires <code>FileRenamedEvent</code>.
     * This event is fired upon renaming a file or directory.
     *
     * @param src  the absolute path to the original file or directory
     * @param dest the absolute path to the renamed file or directory
     *
     * @see FileRenamedEvent
     */
    protected void fireFileRenamed(final String src, final String dest) {
        FileRenamedEvent event = new FileRenamedEvent(this, src, dest);
        for (FileUtilsListener listener : fileUtilsListeners) {
            listener.fileRenamed(event);
        }
    }

    /**
     * Convert asterisks.
     *
     * @param src - source String
     *
     * @return converted String
     */
    private static String convertAsterisks(final String src) {
        StringTokenizer st = new StringTokenizer(src, "*", true);

        if (st.hasMoreElements()) {
            StringBuilder buffer = new StringBuilder();

            while (st.hasMoreTokens()) {
                String str = st.nextToken();

                if (str.equals("*")) {
                    buffer.append(".*");
                } else {
                    buffer.append(str);
                }
            }

            return buffer.toString();
        } else {
            return src;
        }
    }

    /**
     * Convert periods.
     *
     * @param src convert periods.
     *
     * @return the converted String
     */
    private static String convertPeriods(final String src) {
        StringTokenizer st = new StringTokenizer(src, ".", true);

        if (st.hasMoreElements()) {
            StringBuilder buffer = new StringBuilder();

            while (st.hasMoreTokens()) {
                String str = st.nextToken();

                if (str.equals(".")) {
                    buffer.append("\\.");
                } else {
                    buffer.append(str);
                }
            }

            return buffer.toString();
        } else {
            return src;
        }
    }

    /**
     * Convert regex.
     *
     * @param src String
     *
     * @return the converted String.
     */
    private static String convertRegex(final String src) {
        return convertAsterisks(convertPeriods(src));
    }

    /**
     * Copies a source directory to a destination directory.
     *
     * @param src  the source directory to copy
     * @param dest the destination directory
     *
     * @throws IOException if an I/O related error occurs
     */
    private void copyDirToDir(final File src, final File dest)
            throws IOException {
        copyDirToDir(src, dest, true);
    }

    /**
     * Copies files attributes from source file to destination file.
     * Attributes include last modified date.
     *
     * @param src  the source file
     * @param dest the destination file
     *
     * @throws IOException if an I/O related error occurs
     */
    private void copyFileAttributes(final File src, final File dest)
            throws IOException {
        boolean success = dest.setLastModified(src.lastModified());

        if (!success) {
            throw new IOException("Error setting file last modified date.");
        }
    }

    /**
     * Copies source file to destination directory.
     *
     * @param src the source file
     * @param dir the destination directory
     *
     * @throws IOException if an I/O related error occurs
     */
    private void copyFileToDir(final File src, final File dir)
            throws IOException {
        File dest = new File(dir, src.getName());
        copyFileToFile(src, dest);
    }

    /**
     * Copies source file to destination file.
     *
     * @param src  the source file
     * @param dest the destination file
     *
     * @throws IOException if an I/O related error occurs
     */
    private void copyFileToFile(final File src, final File dest)
            throws IOException {
        // check if file exists and can be overwritten
        if (dest.exists() && !dest.canWrite()) {
            throw new IOException("Cannot overwrite destination file " + dest.getAbsolutePath());
        }

        IOTools.copy(src, dest);
        copyFileAttributes(src, dest);

        // fire copy event
        fireFileCopied(src, dest);
    }

    /**
     * Checks if a directory exists.
     *
     * @param dir the directory to check
     *
     * @return true if directory exists, false otherwise
     */
    private boolean directoryExists(final File dir) {
        return dir.exists() && dir.isDirectory();
    }

    /**
     * Verifies if a file could be deleted. One file could be deleted
     * if the date file is older than the date allowed.
     *
     * @param file File date will be verified
     * @param days Max older days.
     *
     * @return <code>true</code> if the date file is older than the max file
     *         allowed.
     */
    private boolean couldBeDeleted(final File file, final int days) {
        // File date
        long lastModified = file.lastModified();
        Calendar fileCalendar = Calendar.getInstance();
        fileCalendar.setTime(new Date(lastModified));

        // Max date allowed
        Calendar mostAllowed = Calendar.getInstance();
        mostAllowed.add(Calendar.DAY_OF_YEAR, -days);

        // Verifies if max allowed is before than file date
        return !mostAllowed.before(fileCalendar);
    }
}
