/**
 * JOnAS
 * Copyright (C) 2010 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.audit.internal.logger.pool;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.audit.internal.logger.util.LogConfigurator;


/**
 * This class manage a pool of files.
 * Files are created by a Monolog logger.
 * Names of created files respect a pattern defined by the user.
 * @author Vincent Michaud
 */
public abstract class FileLogPool {

    /**
     * The directory of the pool.
     */
    private File mainDir;

    /**
     * The path of the pool directory.
     */
    private String mainPath;

    /**
     * The filename filter.
     */
    private FilenameFilter filter;

    /**
     * The pattern for detect files of the pool.
     */
    private Pattern pattern;

    /**
     * The begining text of fixed pattern.
     */
    private String beginFP;

    /**
     * The ending text of fixed pattern.
     */
    private String endFP;

    /**
     * The current log file name.
     */
    private String logFilename = null;

    /**
     * The current pattern of log file.
     */
    private String logFilenamePattern = null;

    /**
     * A log configurator.
     */
    private LogConfigurator lc = null;

    /**
     * The logger.
     */
    private Logger logger = null;

    /**
     * Determine if a file logger is used.
     */
    private boolean fileLogger;

    /**
     * The number of files in the pool.
     */
    private int numberFiles = 0;

    /**
     * The max number of files in the pool.
     */
    private int maxFiles = UNLIMITED;

    /**
     * The default name of the file handler.
     */
    private static final String FILE_HANDLER_NAME = "fileLogPoolHandler";

    /**
     * The default name of the file handler.
     */
    private static final String CONSOLE_HANDLER_NAME = "consoleLogPoolHandler";

    /**
     * The default level of the logger.
     */
    private static final String LOGGER_LEVEL = "INFO";

    /**
     * An unlimited number of files in the pool.
     */
    public static final int UNLIMITED = -1;

    /**
     * The group which identify the variable pattern.
     */
    private static final int PATTERN_GROUP = 1;

    /**
     * An interface to define an action on log files.
     */
    public interface LogFileAction {

        /**
         * Execute the action.
         * @param filename The name of the log file.
         */
        void execute(final String filename);
    }

    /**
     * Constructor.
     * @param dirPath The directory of the pool
     * @param className Name of the class representative to the log files pool
     * @param positionVariablePattern Index where the variable pattern is inserted
     * @param fixedPattern A fixed pattern, without the variable pattern
     * @param variablePattern A regular expression representing the variable pattern
     * @param msgPattern The pattern of the log message (standards are defined in Monolog documentation)
     * @param maxFiles Determine the maximum number of log files in the pool
     * @param fileLogger Determine if a file logger is used
     * @param consoleLogger Determine if a console logger is used
     * @throws IOException If the path of the pool is corresponding of a file path
     */
    public FileLogPool(final String dirPath, final String className, final String fixedPattern,
                       final String variablePattern, final int positionVariablePattern,
                       final String msgPattern, final int maxFiles, final boolean fileLogger,
                       final boolean consoleLogger) throws IOException {

        this.mainDir = new File(dirPath);
        if (this.mainDir.isFile()) {
            throw new IOException("Path of the file pool is not valid.");
        } else if (!mainDir.exists()) {
            mainDir.mkdirs();
        }
        this.fileLogger = fileLogger;
        this.beginFP = fixedPattern.substring(0, positionVariablePattern);
        this.endFP = fixedPattern.substring(positionVariablePattern);
        StringBuilder builder = new StringBuilder(this.beginFP);
        builder.append('(');
        builder.append(variablePattern);
        builder.append(')');
        builder.append(this.endFP);
        this.pattern = Pattern.compile(builder.toString());

        this.filter = new FilenameFilter() {
            public boolean accept(final File dir, final String name) {
                Matcher m = pattern.matcher(name);
                return m.matches();
            }
        };

        this.mainPath = this.mainDir.getCanonicalPath() + File.separator;
        this.logFilenamePattern = initLogFilenamePattern();
        builder = new StringBuilder(this.beginFP);
        builder.append(this.logFilenamePattern);
        builder.append(this.endFP);
        this.logFilename = builder.toString();
        this.setMaxFiles(maxFiles);

        if (consoleLogger || fileLogger) {
            this.lc = new LogConfigurator(className, getLoggerLevel(), false);
            if (fileLogger) {
                this.lc.setSimpleFileHandler(getFileHandlerName(), this.mainPath + this.logFilename,
                                             msgPattern, null, true);
            }
            if (consoleLogger) {
                this.lc.setConsoleHandler(getConsoleHandlerName(), LogConfigurator.SYSTEM_OUT, msgPattern, null);
            }
            this.logger = this.lc.apply();
        }
    }

    /**
     * Define the maximum number of files int the pool.
     * @param maxFiles The maximum number, or FileLogPool.UNLIMITED
     */
    public void setMaxFiles(final int maxFiles) {
        this.maxFiles = maxFiles;
        listLogFilenames();
    }

    /**
     * Update the variable 'numberFiles'.
     * @param logFiles The list of log files in the pool
     */
    private void updateFileNumber(final Object[] logFiles) {
        this.numberFiles = logFiles.length;
        if (this.maxFiles > 0 && this.maxFiles < this.numberFiles) {
            int totalDeleted = this.numberFiles - this.maxFiles;
            this.numberFiles = this.maxFiles;
            if (logFiles instanceof String[]) {
                for (int i = 0; totalDeleted > 0; i++) {
                    deleteFile((String) logFiles[i]);
                }
            } else if (logFiles instanceof File[]) {
                for (int i = 0; totalDeleted > 0; i++) {
                    if (((File) logFiles[i]).delete()) {
                        this.numberFiles--;
                    }
                }
            }
        }
    }

    /**
     * Delete a log file.
     * @param filename The name of the file
     */
    private void deleteFile(final String filename) {
        File file = new File(this.mainDir, filename);
        if (file.delete()) {
            this.numberFiles--;
        }
    }

    /**
     * Find a file with the specified motif.
     * @param motif The motif
     * @return The file found, or null if nothing is found
     */
    public File findFile(final String motif) {
        StringBuilder builder = new StringBuilder(this.beginFP);
        builder.append(motif);
        builder.append(this.endFP);

        File file = new File(this.mainDir, builder.toString());
        if (file.exists()) {
            return file;
        }
        return null;
    }

    /**
     * Get the main directory.
     * @return The main directory
     */
    public File getMainDirectory() {
        return this.mainDir;
    }

    /**
     * Find the name of the file with the specified motif.
     * @param motif The motif
     * @return The name of the file
     */
    public String findFileName(final String motif) {
        StringBuilder builder = new StringBuilder(this.beginFP);
        builder.append(motif);
        builder.append(this.endFP);
        return builder.toString();
    }

    /**
     * Count the number of log files in the pool.
     * @return The number of log files in the pool
     */
    public int countLogFiles() {
        String[] children = this.mainDir.list(this.filter);
        if (children != null) {
            return children.length;
        }
        return 0;
    }

    /**
     * List all valid files of the pool.
     * @return Sorted list of filenames, or null if not
     *         any files are found in the log pool
     */
    public String[] listLogFilenames() {
        String[] children = this.mainDir.list(this.filter);
        updateFileNumber(children);
        int i, length = children.length;
        String str;
        if (length > 0) {
            for (int j = 1; j < length; j++) {
                i = j - 1;
                if (compareFilename(children[i], children[j]) > 0) {
                    str = children[j];
                    do {
                        children[i + 1] = children[i];
                        i--;
                    } while (i > -1 && compareFilename(children[i], str) > 0);
                    children[i + 1] = str;
                }
            }
            return children;
        }
        return null;
    }

    /**
     * List all valid files of the pool.
     * @return The list of file, sorted by file name or null
     *         if not any files are found in the log pool
     */
    public File[] listLogFiles() {
        File[] array = this.mainDir.listFiles(this.filter);
        updateFileNumber(array);
        int i, length = array.length;
        String filename;
        File file;

        if (length > 0) {
            for (int j = 1; j < length; j++) {
                i = j - 1;
                file = array[j];
                filename = file.getName();
                if (compareFilename(array[i].getName(), filename) > 0) {
                    do {
                        array[i + 1] = array[i];
                        i--;
                    } while (i > -1 && compareFilename(array[i].getName(), filename) > 0);
                    array[i + 1] = file;
                }
            }
            return array;
        }
        return null;
    }

    /**
     * List all filenames which are older or more recent than the specified filename.
     * @param filename The filename which others are compared
     * @param action The action executed on log files
     * @param old If true, files older than the specified filename are listed
     *        otherwise, files more recent than the specified filename are listed
     * @param inclusive Determine if the specified filename is included in the result
     */
    protected void listLogFiles(final String filename, final LogFileAction action,
                                final boolean old, final boolean inclusive) {
        String[] children = listLogFilenames();
        if (children != null) {
            int i, index = -1, compareResult = 1;
            if (old ^ inclusive) {
                compareResult = 0;
            }

            for (i = 0; i < children.length; i++) {
                if (compareFilename(children[i], filename) >= compareResult) {
                    index = i;
                    break;
                }
            }

            if (index != -1) {
                if (old) {
                    for (i = 0; i < index; i++) {
                        action.execute(children[i]);
                    }
                } else {
                    int length = children.length - index;
                    for (i = 0; i < length; i++) {
                        action.execute(children[i + index]);
                    }
                }
            }
        }
    }

    /**
     * List all filenames which are older or more recent than the specified filename.
     * @param filename The filename which others are compared
     * @param action The action executed on log files
     * @param old If true, files older than the specified filename are listed
     *        otherwise, files more recent than the specified filename are listed
     * @param inclusive Determine if the specified filename is included in the result
     * @return The list of file, sorted by file name, or null if not any files are
     *         found in the log pool
     */
    protected File[] listLogFiles(final String filename, final boolean old, final boolean inclusive) {
        String[] children = listLogFilenames();
        if (children != null) {
            File[] result = null;
            int i, index = -1, compareResult = 1;
            if (old ^ inclusive) {
                compareResult = 0;
            }

            for (i = 0; i < children.length; i++) {
                if (compareFilename(children[i], filename) >= compareResult) {
                    index = i;
                    break;
                }
            }

            if (index != -1) {
                if (old) {
                    if (index > 0) {
                        result = new File[index];
                        for (i = 0; i < index; i++) {
                            result[i] = new File(this.mainDir, children[i]);
                        }
                    }
                } else {
                    int length = children.length - index;
                    if (length > 0) {
                        result = new File[length];
                        for (i = 0; i < length; i++) {
                            result[i] = new File(this.mainDir, children[i + index]);
                        }
                    }
                }
                return result;
            }
        }
        return null;
    }

    /**
     * Clean all files in the log pool.
     */
    public void cleanLogPool() {
        String[] children = this.mainDir.list(this.filter);
        for (String child : children) {
            deleteFile(child);
        }
    }



    /**
     * Clean the log pool by removing files.
     * @param nbFiles Number of files to remove
     * @param old If true, old files are removed,
     *        otherwise, recent files are removed
     */
    public void cleanLogPool(final int nbFiles, final boolean old) {
        if (nbFiles > 0) {
            String[] children = listLogFilenames();
            if (children != null) {
                int number = nbFiles;
                if (number > children.length) {
                    number = children.length;
                }
                if (old) {
                    for (int i = 0; i < number; i++) {
                        deleteFile(children[i]);
                    }
                } else {
                    int end = children.length - number;
                    for (int i = children.length - 1; i >= end; i--) {
                        deleteFile(children[i]);
                    }
                }
            }
        }
    }

    /**
     * Clean the log pool all files which are before
     * or after a specific log. The specific log is
     * not removed.
     * @param filename The name of the specific file
     * @param old If true, old files are removed,
     *        otherwise, recent files are removed
     */
    protected void cleanLogPool(final String filename, final boolean old) {
        if (filename != null) {
            String[] children = listLogFilenames();
            if (children != null) {
                if (old) {
                    for (int i = 0; i < children.length && compareFilename(children[i], filename) < 0; i++) {
                        deleteFile(children[i]);
                    }
                } else {
                    for (int i = children.length - 1; i >= 0 && compareFilename(children[i], filename) > 0; i--) {
                        deleteFile(children[i]);
                    }
                }
            }
        }
    }

    /**
     * Remove a specific log file.
     * @param filename The name of the file
     */
    protected void removeLogFile(final String filename) {
        if (this.filter.accept(this.mainDir, filename)) {
            deleteFile(filename);
        }
    }

    /**
     * Get the logger. The logger has a file handler.
     * If the pattern of the file has not changed, the logger is the
     * same. Otherwise, a new logger is created.
     * @return The logger
     */
    public Logger getLogger() {

        String newPattern = this.getLogFilenamePattern();
        if (this.fileLogger && newPattern != null && !newPattern.equals(this.logFilenamePattern)) {
            StringBuilder builder = new StringBuilder(this.beginFP);
            builder.append(newPattern);
            builder.append(this.endFP);
            this.logFilenamePattern = newPattern;
            this.logFilename = builder.toString();
            this.lc.changeHandlerOutput(getFileHandlerName(), this.mainPath + this.logFilename);
            this.logger = this.lc.apply();

            if (this.numberFiles >= this.maxFiles) {
                String[] children = listLogFilenames();
                new File(this.mainDir, children[0]).delete();
            } else {
                this.numberFiles++;
            }
        }
        return this.logger;
    }

    /**
     * Get the variable pattern of a file name.
     * @param filename The file name
     * @return The pattern if t is found, null otherwise
     */
    public String getPattern(final String filename) {
        Matcher m = this.pattern.matcher(filename);
        if (m.find()) {
            return m.group(PATTERN_GROUP);
        }
        return null;
    }

    /**
     * Determine the missing pattern of the log file name.
     * @return The pattern of the log file name. If the return is null,
     *         the old pattern is kept.
     */
    protected abstract String getLogFilenamePattern();

    /**
     * Compare two filename.
     * @param filename1 The first filename
     * @param filename2 The second filename
     * @return The value 0 if the second filename is equal to the first;
     *         a value less than 0 if the first filename is less than second;
     *         and a value greater than 0 if first pattern is greater than the
     *         second filename
     */
    protected abstract int compareFilename(final String filename1, final String filename2);


    /**
     * Determine the first pattern of the log file name.
     * @return The default pattern
     * @throws IOException If the initial pattern is invalid
     */
    protected String initLogFilenamePattern() throws IOException {
        String initPattern = getLogFilenamePattern();
        if (initPattern == null || initPattern.length() == 0) {
            throw new IOException("Initial pattern of the log file name is invalid.");
        }
        return initPattern;
    }

    /**
     * Get the log configurator.
     * @return The log configurator
     */
    protected LogConfigurator getLogConfigurator() {
        return this.lc;
    }

    /**
     * Get the logger level.
     * @return The logger level
     */
    protected String getLoggerLevel() {
        return LOGGER_LEVEL;
    }

    /**
     * Get the file handler name.
     * @return The file handler name
     */
    protected String getFileHandlerName() {
        return FILE_HANDLER_NAME;
    }

    /**
     * Get the console handler name.
     * @return The console handler name
     */
    protected String getConsoleHandlerName() {
        return CONSOLE_HANDLER_NAME;
    }

    /**
     * Get the filter accepting files in the pool.
     * @return The filter
     */
    protected FilenameFilter getFilenameFilter() {
        return this.filter;
    }

    /**
     * Get the pattern of accepted files in the pool.
     * @return The pattern
     */
    protected Pattern getPoolFilePattern() {
        return this.pattern;
    }
}
