/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009 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: LogBuffer.java 16794 2009-03-10 13:58:04Z danesa $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.jmbeans.log;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.ow2.jonas.lib.util.JonasObjectName;


/**
 * THis MBean receives and treats LogRecords contained in notifications
 * emitted by the Monolog JmxHandler. The treatement is aimed to provided
 * structured data for JonasAdmin.
 * @author waeselyf
 * @author adriana danes
 */
public class LogBuffer implements LogBufferMBean, MBeanRegistration {

    /**
     * The MBean constructor.
     * @param name the name used to construct the ObjectName
     */
    public LogBuffer(final String name) {
        this.name = name;
    }

    /**
     * @return a StringBuffer containing all the LogRecords in the recordSet
     * (isn't too big ?)
     */
    public StringBuffer fetchRecords() {
        TreeSet newset = fetchRecordsSet();
        int estimSz = newset.size() * estimLogRecordSize;
        StringBuffer buf = new StringBuffer(estimSz);
        Iterator it = newset.iterator();
        while (it.hasNext()) {
            appendRecord((LogRecord) it.next(), buf);
        }
        newset.clear();
        return buf;
    }
    /**
     * @return a StringBuffer containing all the LogRecords in the recordSet
     * (isn't too big ?)
     */
    private TreeSet fetchRecordsSet() {
        TreeSet myset = null;
        synchronized (recordSet) {
            myset = (TreeSet) recordSet.clone();
        }
        return myset;
    }

    private TreeSet fetchRecordsSet(final Long from, final Long to) {
        TreeSet myset = null;
        LogRecord recfrom = new LogRecord(Level.ALL, "from");
        recfrom.setMillis(from);
        recfrom.setSequenceNumber(0);
        LogRecord recto = new LogRecord(Level.ALL, "to");
        recto.setMillis(to);
        recto.setSequenceNumber(Long.MAX_VALUE);

        SortedSet sset = null;
        synchronized (recordSet) {
            if (from.longValue() == 0) {
                sset = recordSet.headSet(recto);
            } else if (to.longValue() == 0) {
                sset = recordSet.tailSet(recfrom);
            } else {
                sset = recordSet.subSet(recfrom, recto);
            }
            myset = (TreeSet) ((TreeSet) sset).clone();
        }
        return myset;

    }
    /**
     * @param from the LogRecord inferior time limit
     * @param to the LogRecord upper time limit
     * @return a StringBuffer containing the LogRecords from the recordSet
     * from, to : fetch records between from and to.
     * 0, to : fetch records anterior to to
     * from, 0 : fetch records posterior to from
     * 0, 0 : fetch all records
     */
    public StringBuffer fetchRecords(final Long from, final Long to) {

        StringBuffer buf = null;
        long tfrom = (from == null ? 0 : from.longValue());
        long tto = (to == null ? 0 : to.longValue());
        if (tfrom > tto) {
            return null;
        }
        if ((tfrom == 0) && (tto == 0)) {
            buf = fetchRecords();
        } else {
            TreeSet newset = fetchRecordsSet(from, to);
            int estimSz = newset.size() * estimLogRecordSize;
            buf = new StringBuffer(estimSz);
            Iterator it = newset.iterator();
            while (it.hasNext()) {
                appendRecord((LogRecord) it.next(), buf);
            }
            newset.clear();
        }
        return buf;
    }
    /**
     * @param from the LogRecord inferior time limit
     * @param to the LogRecord upper time limit
     * @param level ...
     * @return a StringBuffer containing the LogRecords from the recordSet
     * from, to : fetch records between from and to.
     * 0, to : fetch records anterior to to
     * from, 0 : fetch records posterior to from
     * 0, 0 : fetch all records
     */
    public StringBuffer fetchRecords(final Long from, final Long to, final int level) {
        StringBuffer buf = null;
        long tfrom = (from == null ? 0 : from.longValue());
        long tto = (to == null ? 0 : to.longValue());
        TreeSet newset = null;
        if (tfrom > tto) {
            return null;
        }
        if ((tfrom == 0) && (tto == 0)) {
            newset = fetchRecordsSet();
        } else {
            newset = fetchRecordsSet(from, to);
        }
        TreeSet newsetWithLevels = new TreeSet();
        Iterator it = newset.iterator();
        while (it.hasNext()) {
            LogRecord rec = (LogRecord) it.next();
            if (rec.getLevel().intValue() > level) {
                newsetWithLevels.add(rec);
            }
        }
        newset.clear();
        int estimSz = newsetWithLevels.size() * estimLogRecordSize;
        buf = new StringBuffer(estimSz);
        it = newsetWithLevels.iterator();
        while (it.hasNext()) {
            appendRecord((LogRecord) it.next(), buf);
        }
        newsetWithLevels.clear();
        return buf;
    }

    /**
     * Append a LogRecord to a StringBuffer being constructed by getRecent()
     * or FetchRecords() method
     * @param record LogRecord to append
     * @param buf  StringBuffer being constructed
     */
    private void appendRecord(final LogRecord record, final StringBuffer buf) {
        buf.append(record.getLevel().getName());
        buf.append("; ");
        /*
        buf.append(record.getMillis());
        buf.append(";");
        buf.append(record.getSequenceNumber());
        buf.append(";");
        buf.append(record.getSourceClassName());
        buf.append(";");
        buf.append(record.getSourceMethodName());
        buf.append(";");
        */
        buf.append(dateFormat.format(new Date(record.getMillis())));
        buf.append("; ");
        buf.append(record.getMessage());
        buf.append("\n");
        Throwable th = record.getThrown();
        if (th != null) {
            appendThrowable(th, buf);
        }
    }

    private void appendThrowable(Throwable th, final StringBuffer buf) {

        buf.append(th.getClass());
        buf.append(": ");
        buf.append(th.getMessage());
        StackTraceElement[] stack = th.getStackTrace();
        for (int i = 0; i < stack.length; i++) {
            buf.append("\n\t");
            buf.append(stack[i].toString());
        }
        buf.append("\n");

        th = th.getCause();
        if (th != null) {
            buf.append("Caused by:\n");
            appendThrowable(th, buf);
        }
    }

    /**
     * @return the capacity of the recordSet
     */
    public int getCapacity() {
        return capacity;
    }

    /**
     * @return number of received LogRecords having INFO level
     */
    public long getInfoCount() {
        return infoCount;
    }

    /**
     * Increment counter of received LogRecords having INFO level
     */
    protected void incInfoCount() {
        infoCount++;
    }
    /**
     * Not used currently. It could be used to filter out LogRecord notifications having
     * their level inferior to this level.
     * @return the minimum level which is treated by the LogBuffer
     */
    public int getLevel() {
        return level;
    }

    /**
     * @return the counter of received LogRecors
     */
    public long getRecordCount() {

        return recordCount;
    }

    /**
     * Increment counter of received LogRecors
     */
    protected void incRecordCount() {
        recordCount++;
    }

    /**
     * @return the number of received LogRecords having SEVERE level
     */
    public long getSevereCount() {
        return severeCount;
    }

    /**
     * Increment counter of received LogRecords having SEVERE level
     */
    protected void incSevereCount() {
        severeCount++;
    }

    /**
     * @return the number of received LogRecords having WARNING level
     */
    public long getWarningCount() {
        return warningCount;
    }

    /**
     * Increment counter of received LogRecords having WARNING level
     */
    protected void incWarningCount() {
        warningCount++;
    }

    /**
     * @return the number of received LogRecords having level different
     * of INFO, WARNING, SEVERE
     */
    public long getOtherCount() {
        return otherCount;
    }

    /**
     * Increment counter of received LogRecords having level different
     * of INFO, WARNING, SEVERE
     */
    protected void incOtherCount() {
        otherCount++;
    }

    /**
     * @return the date in millis of the most recently received LogRecord
     */
    public long getLatestDate() {
        return latestDate;
    }

    /**
     * @param records the capacity of the recordSet
     */
    public void setCapacity(final int records) {

        capacity = records;
    }

    /*
     * (non-Javadoc)
     *
     * @see bull.jonas.mbean.LogBufferMBean#setLevel(int)
     */
    public void setLevel(final int level) {

        this.level = level;
    }

    /**
     * @return recent LogRecord appended into a String
     */
    public String getRecent() {

        LogRecord[] lra;
        synchronized (recentLock) {
            lra = new LogRecord[recent.size()];
            lra = (LogRecord[]) recent.toArray(lra);
        }
        int estimSz = recentCapacity * estimLogRecordSize;
        StringBuffer buf = new StringBuffer(estimSz);

        for (int i = 0; i < lra.length; i++) {
            appendRecord(lra[i], buf);
        }
        return buf.toString();
    }

    /**
     * Construct ObjectName of the log service MBean (LogManagement MBean's name)
     */
    private void locateLogSource() {
        if (logManagementOn == null) {
            ObjectName on = null;
            if (domain != null) {
                on = JonasObjectName.logService(domain);
            } else {
                on = JonasObjectName.logService();
            }
            Set son = mbeanServer.queryNames(on, null);
            if (!son.isEmpty()) {
                logManagementOn = (ObjectName) son.iterator().next();
            }
        }
    }

    /**
     * @return the domain name
     */
    private String getDomain() {
        return domain;
    }

    /**
     * After registering, add LogNotificationListener as listener for notifications
     * emitted by the LogManagement MBean
     * @param arg0 Indicates whether or not the MBean has been successfully registered
     */
    public void postRegister(Boolean arg0) {
        if (!arg0) {
            return;
        }
        listener = new LogNotificationListener(this);
        try {
            mbeanServer.addNotificationListener(logManagementOn, listener, null, null);
        } catch (InstanceNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        RecordFeeder feeder = new RecordFeeder(queue, recordSet);
        feeder.setMaxRecords(capacity);
        feederThread = new Thread(feeder, "LogRecorder");
        feederThread.setDaemon(true);
        feederThread.start();
    }

    /**
     * Allows getting a refernce to the MBean server, getting the domain name,
     * and if necessary, constructing an ObjectName
     * @param mbs refernce to the MBean
     * @param on ObjectName provided by the creator of the MBean
     * @return this MBean's ObjectName
     * @throws MalformedObjectNameException could not create ObjectName
     */
    public ObjectName preRegister(final MBeanServer mbs, ObjectName on) throws MalformedObjectNameException {
        this.mbeanServer = mbs;
        if (on != null) {
            domain = on.getDomain();
        }
        locateLogSource();
        if (on == null) {
            on = ObjectName.getInstance(getDomain() + ":type=LogBuffer,name="
                    + name);
        }
        return on;
    }

    /**
     * Before deregistering, remove LogNotificationListener listener
     * @throws Exception pb. when deregistering
     */
    public void preDeregister() throws Exception {
        mbeanServer.removeNotificationListener(logManagementOn, listener);
        feederThread.interrupt();
        feederThread = null;
    }

    /**
     * Nothing to do after deregisteing
     */
    public void postDeregister() {

    }
    /**
     * Add a LogRecord to the recent recors list
     * @param record LogRecord to be add
     */
    protected void addToRecentLog(final LogRecord record) {

        synchronized (recentLock) {
            if (recent.size() == recentCapacity) {
                recent.remove(0);
            }
            recent.add(record);
        }
    }

    /**
     * Add a LogRecord to the queue
     * @param record LogRecord to be add
     */
    protected void addToLogRecordsQueue(final LogRecord record) {
        synchronized (queue) {
            queue.add(record);
        }
    }

    private int level = WARNING;

    private long recordGauge = 0;

    private long recordCount = 0;

    private long infoCount = 0;

    private long warningCount = 0;

    private long severeCount = 0;

    private long otherCount = 0;

    private ArrayList recent = new ArrayList();
    /**
     * Data of millies of the oldest LogRecord
     */
    private long oldestDate = 0;

    /**
     * Data in millis of the most recent LogRecord
     */
    private long latestDate = 0;
    /**
     * Used to synchronize access to the recent list
     */
    private Object recentLock = new Object();

    /**
     * MBeanServer where this MBean is registered.
     * Used to add, remove NotificationListener
     */
    private MBeanServer mbeanServer = null;

    /**
     * LogManagement MBean's ObjectName
     */
    private ObjectName logManagementOn = null;

    /**
     * Used to be printed by the viewer (JonasAdmin)
     */
    private SimpleDateFormat dateFormat = new SimpleDateFormat(
            "yyyy/MM/dd-HH:mm:ss-SSS");

    /**
     * NotificationListener object that treats notifications emitted by
     * the JOnAS log service MBean (in fact by the JMX handler)
     */
    private LogNotificationListener listener = null;

    /**
     * If the LogBuffer MBean's Objectname is not provided at registration time
     */
    private String name = "log";

    /**
     * The domain name
     */
    private String domain = null;

    /**
     * Holds LogRecords that are used as source by the RecordFeeder
     */
    private LinkedList queue = new LinkedList();

    /**
     * the capacity of the recordSet
     */
    private int capacity = 100000;

    /**
     * Estimated number of characters in a LogRecord to be printed out.
     * These characters may contain the message and the stack trace in the associated
     * Throwable object
     */
    private int estimLogRecordSize = 1000;

    /**
     * Big LogRecords set allowing to search LogRecords on the basis of date or order criteria
     */
    private TreeSet recordSet = new TreeSet(new LogRecordComparator());

    /**
     * Thread executed by the RecordFeeder
     */
    private Thread feederThread = null;
    /**
     * Capacity of the recent LogRecord list
     */
    private int recentCapacity = 10;
    /**
     * @return current capacity of the recent LogRecord list
     */
    public int getRecentCapacity() {
        return recentCapacity;
    }
    /**
     * @param recentCapacity capacity of the recent LogRecord list
     */
    public void setRecentCapacity(final int recentCapacity) {
        this.recentCapacity = recentCapacity;
    }


    protected void setLatestDate(final long latestDate) {
        this.latestDate = latestDate;
    }

    public long getOldestDate() {
        return oldestDate;
    }

    public void setOldestDate(final long oldestDate) {
        this.oldestDate = oldestDate;
    }

}
