/**
 * 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.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.ow2.util.auditreport.impl.GenericAuditReport;
import org.ow2.util.auditreport.impl.InvocationAuditReport;
import org.ow2.util.auditreport.impl.JaxwsAuditReport;
import org.ow2.util.auditreport.impl.JNDIAuditReport;
import org.ow2.util.auditreport.impl.WebInvocationAuditReport;
import org.ow2.util.auditreport.impl.util.ObjectEncoder;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * This class is a helper to deal with AuditReports.
 * Try to not use complex type for a good restitution through JMX.
 * @author Vincent Michaud
 */
public final class AuditLogReportHelper {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(AuditLogReportHelper.class);

    /**
     * The size of the buffer.
     */
    private static final int BUFFER_SIZE = 4096;

    /**
     * A string for indent values of fields.
     */
    public static final String INDENT1 = "    ";

    /**
     * A string for indent values of fields.
     */
    public static final String INDENT2 = "        ";


    private static final String BEFORE_DATE = "[";
    private static final String AFTER_DATE = "] ";
    private static final String BEFORE_TYPE = " : ";

    private static final String NEW_LINE = "\r\n";

    /**
     * The date format.
     */
    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yy/MM/dd HH:mm:ss");

    /**
     * The date pattern.
     */
    private static final String DATE_PATTERN = "[0-9]{2}/[0-1][0-9]/[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]";

    /**
     * The header pattern.
     */
    private static final String HEADER_PATTERN = "\\[(" + DATE_PATTERN + ")\\] ([a-zA-Z0-9.]+) : (.*)\r\n";

    /**
     * The log message pattern.
     */
    private static final Pattern MSG_PATTERN = Pattern.compile(HEADER_PATTERN + "((.+\r\n)*)(\r\n)+");

    /**
     * Fields pattern.
     */
    private static final Pattern FIELDS_PATTERN = Pattern.compile("[ ]*(([\\w]+) = )?(.*)\r\n(\r\n$)?");

    /**
     * The calendar used for calculate the date of the timestamp.
     */
    private static final Calendar CALENDAR = new GregorianCalendar();

    /**
     * An interface for get the result of encodeLogMessages().
     */
    public interface EncodeResult {

        /**
         * Get encoded audit reports.
         * @return An array of encoded string. A value of the array can be null if the class
         *         of the message is not found in the classpath
         */
        String[] getEncodedAuditReports();

        /**
         * Get timestamp of audit reports.
         * @return An array of timestamp
         */
        long[] getAuditReportsTimestamp();
    }

    /**
     * The private constructor for utility class.
     */
    private AuditLogReportHelper() {
    }

    /**
     * Decode an audit report to an formatted and displayable message.
     * @param encodedAuditReport The encoded audit report
     * @param typeMessage Type of the audit report
     * @return A formatted message
     */
    public static synchronized GenericAuditReport decodeAuditReport(final String typeMessage, final String encodedAuditReport) {

        List<String> paramNames = new LinkedList<String>();
        List paramValues = new LinkedList();
        String className = ObjectEncoder.getClassName(encodedAuditReport);


        GenericAuditReport report = null;
        if ((InvocationAuditReport.class.getName()).equals(typeMessage)) {
            InvocationAuditReport reportTmp = ObjectEncoder.decodeToDisplayable(encodedAuditReport, InvocationAuditReport.class,
                    paramNames, paramValues);
            report = reportTmp;
        } else if (WebInvocationAuditReport.class.getName().equals(typeMessage)) {
            WebInvocationAuditReport reportTmp = ObjectEncoder.decodeToDisplayable(encodedAuditReport, WebInvocationAuditReport.class,
                    paramNames, paramValues);
            report = reportTmp;
        } else if ("JNDI".equals(typeMessage)) {
            JNDIAuditReport reportTmp = ObjectEncoder.decodeToDisplayable(encodedAuditReport, JNDIAuditReport.class,
                    paramNames, paramValues);
            report = reportTmp;
        } else if (JaxwsAuditReport.class.getName().equals(typeMessage)) {
            report = ObjectEncoder.decodeToDisplayable(encodedAuditReport,
                                                       JaxwsAuditReport.class,
                                                       paramNames,
                                                       paramValues);
        } else {
            report = ObjectEncoder.decodeToDisplayable(encodedAuditReport, GenericAuditReport.class,
                                                                    paramNames, paramValues);
        }
        return report;

    }



    /**
     * Decode an audit report to an formatted and displayable message.
     * @param encodedAuditReport The encoded audit report
     * @param typeMessage Type of the audit report
     * @return A formatted message
     */
    public static synchronized String getLogdecodeAuditReport(final String typeMessage, final String encodedAuditReport) {

        List<String> paramNames = new LinkedList<String>();
        List paramValues = new LinkedList();
        String className = ObjectEncoder.getClassName(encodedAuditReport);


        GenericAuditReport report = null;
        if ((InvocationAuditReport.class.getName()).equals(typeMessage)) {
            InvocationAuditReport reportTmp = ObjectEncoder.decodeToDisplayable(encodedAuditReport, InvocationAuditReport.class,
                    paramNames, paramValues);
            report = reportTmp;
        } else if ("JNDI".equals(typeMessage)) {
            JNDIAuditReport reportTmp = ObjectEncoder.decodeToDisplayable(encodedAuditReport, JNDIAuditReport.class,
                    paramNames, paramValues);
            report = reportTmp;
        } else if (JaxwsAuditReport.class.getName().equals(typeMessage)) {
            report = ObjectEncoder.decodeToDisplayable(encodedAuditReport,
                                                       JaxwsAuditReport.class,
                                                       paramNames,
                                                       paramValues);
        } else {
            report = ObjectEncoder.decodeToDisplayable(encodedAuditReport, GenericAuditReport.class,
                                                                    paramNames, paramValues);
        }


        CALENDAR.setTimeInMillis(report.getRequestTimeStamp());
        String date = DATE_FORMAT.format(CALENDAR.getTime());
        StringBuilder builder = new StringBuilder(BEFORE_DATE);
        builder.append(date);
        builder.append(AFTER_DATE);
        builder.append(className);
        builder.append(BEFORE_TYPE);
        builder.append(typeMessage);
        builder.append(NEW_LINE);

        Iterator<String> itName = paramNames.iterator();
        Iterator<String> itValue = paramValues.iterator();
        String name;
        Object value;

        if (paramNames.size() == paramValues.size()) {
            while (itName.hasNext()) {
                name = itName.next();
                value = itValue.next();
                builder.append(INDENT1);
                builder.append(name);
                builder.append(" = ");

                if (value instanceof List) {
                    builder.append(NEW_LINE);
                    for (String str : (List<String>) value) {
                        builder.append(INDENT2);
                        builder.append(str);
                        builder.append(NEW_LINE);
                    }
                } else {
                    builder.append(value);
                    builder.append(NEW_LINE);
                }
            }
        }

        builder.append('\r');      // Not appended "\r\n" cause of a strange bug...
        return builder.toString();
    }

    /**
     * Get audits reports from a string.
     * @param logMessages The string
     * @return A list of audit reports
     */
    private static LinkedList<AuditReport> getAuditReports(final String logMessages) {
        LinkedList<AuditReport> auditReports = new LinkedList<AuditReport>();
        Matcher m = MSG_PATTERN.matcher(logMessages);
        AuditReport auditReport;

        while (m.find()) {
            auditReport = new AuditReport(m.group(1), m.group(2), m.group(3), m.group(4));
            auditReports.add(auditReport);
        }
        return auditReports;
    }

    /**
     * Get audits reports from a log file.
     * @param logFile The log file
     * @return A list of audit reports
     */
    private static LinkedList<AuditReport> getAuditReports(final File logFile) {
        StringBuilder builder = new StringBuilder();
        try {
            char[] buffer = new char[BUFFER_SIZE];
            BufferedReader reader = new BufferedReader(new FileReader(logFile));

            try {
                int length = reader.read(buffer);
                while (length > 0) {
                    builder.append(buffer, 0, length);
                    length = reader.read(buffer);
                }
            } finally {
                reader.close();
            }

        } catch (Exception ex) {
            return null;
        }
        return getAuditReports(builder.toString());
    }

    /**
     * Extract fields information from an audit report.
     * @param report The audit report
     * @param fieldNames Names of fields extracted
     * @param fieldValues Values of fields extracted. Values can be
     *                    a string or a list of string (in case of array,
     *                    or complex objects)
     */
    private static void extractDisplayable(final AuditReport report, final List<String> fieldNames,
                                          final List fieldValues) {
        Matcher m = FIELDS_PATTERN.matcher(report.message);
        String fieldName, fieldValue, lastFieldName, lastFieldValue;
        List<String> values = null;

        if (m.find()) {
            lastFieldName = m.group(2);
            lastFieldValue = m.group(3);

            while (m.find()) {
                fieldName = m.group(2);
                fieldValue = m.group(3);

                if (fieldName != null) {
                    fieldNames.add(lastFieldName);
                    if (values == null) {
                        fieldValues.add(lastFieldValue);
                    } else {
                        fieldValues.add(values);
                        values = null;
                    }
                    lastFieldName = fieldName;
                    lastFieldValue = fieldValue;
                } else {
                    if (values == null) {
                        values = new LinkedList<String>();
                        if (lastFieldValue.length() != 0) {
                            values.add(lastFieldValue);
                        }
                    }
                    if (fieldValue.length() != 0) {
                        values.add(fieldValue);
                    }
                }
            }

            fieldNames.add(lastFieldName);
            if (values == null) {
                fieldValues.add(lastFieldValue);
            } else {
                fieldValues.add(values);
            }
        }
    }

    /**
     * Encode audit reports contained in the specified log file.
     * @param logFile The log file
     * @param maxLogMessages The maximum number of encoded messages (0 for unlimited)
     * @return Two arrays: one of encoded strings and the other of timestamps.
     *         A value of the encoded strings array can be null if the class
     *         of the message is not found in the classpath
     */
    public static EncodeResult encodeLogMessages(final File logFile, final int maxLogMessages) {
        LinkedList<AuditReport> auditReports = getAuditReports(logFile);
        if (auditReports != null) {
            AuditReport report;
            int nbAudits = auditReports.size(), size = nbAudits;
            ListIterator<AuditReport> it = auditReports.listIterator(nbAudits);

            if (maxLogMessages > 0 && maxLogMessages < nbAudits) {
                size = maxLogMessages;
            }

            String[] result = new String[size];
            long[] timestamps = new long[size];
            LinkedList<String> fieldNames;
            LinkedList fieldValues;
            Class classReport;

            for (int i = size - 1; i >= 0; i--) {
                try {
                    fieldNames = new LinkedList<String>();
                    fieldValues = new LinkedList();
                    report = it.previous();
                    extractDisplayable(report, fieldNames, fieldValues);
                    classReport = Class.forName(report.className);
                    result[i] = ObjectEncoder.encodeFromDisplayable(classReport, fieldNames, fieldValues);
                    timestamps[i] = report.getTimestamp();
                } catch (ClassNotFoundException ex) {
                    result[i] = null;
                    timestamps[i] = 0L;
                    ex.printStackTrace();
                }
            }
            return new CodingResult(result, timestamps);
        }
        return null;
    }


    /**
     * A class representing an audit report.
     */
    static class AuditReport {

        /**
         * The date of the audit report.
         */
        private String date;

        /**
         * The class name of the audit report.
         */
        private String className;

        /**
         * The type of the audit report.
         */
        private String type;

        /**
         * The message of the audit report.
         */
        private String message;

        /**
         * Constructor.
         * @param date The date of the audit report
         * @param className The class name of the audit report
         * @param type The type of the audit report
         * @param message The message of the audit report
         */
        public AuditReport(final String date, final String className, final String type, final String message) {
            this.date = date;
            this.className = className;
            this.type = type;
            this.message = message;
        }

        /**
         * Get the timestamp of the audit report.
         * @return The timestamp
         */
        public long getTimestamp() {
            long result;
            try {
                Date d = DATE_FORMAT.parse(this.date);
                result = d.getTime();
            } catch (ParseException ex) {
                return -1;
            }
            return result;
        }
    }


        /**
     * A class for get the result of encodeLogMessages().
     */
    static class CodingResult implements EncodeResult {

        /**
         * Encoded audit reports.
         */
        private String[] auditReports;

        /**
         * Timestamps.
         */
        private long[] timestamps;

        /**
         * Constructor.
         * @param auditReports Encoded audit reports
         * @param timestamps Timestamps
         */
        public CodingResult(final String[] auditReports, final long[] timestamps) {
            this.auditReports = auditReports;
            this.timestamps = timestamps;
        }

        /**
         * Get encoded audit reports.
         * @return An array of encoded string. A value of the array can be null if the class
         *         of the message is not found in the classpath
         */
        public String[] getEncodedAuditReports() {
            return this.auditReports;
        }

        /**
         * Get timestamp of audit reports.
         * @return An array of timestamp
         */
        public long[] getAuditReportsTimestamp() {
            return this.timestamps;
        }
    }

}
