/*
 * Copyright (c) 2003, KNOPFLERFISH project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * - Neither the name of the KNOPFLERFISH project nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.knopflerfish.bundle.log;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.log.LogEntry;

/**
 * * A LogReaderServiceFactory implements the log functionality. * * I.e., it
 * keeps log entries in memory and initiates callbacks to * LogListeners. It
 * does not keep track of subscribers, this is * delegated to
 * LogReaderServiceImpl. * * It uses an instance of FileLog to write log entryes
 * to file if * that configuration option has been enabled. It also prints log *
 * entries on System.out if desired. * *
 * 
 * @author Gatespace AB *
 * @version $Revision: 1.3 $ *
 */
public final class LogReaderServiceFactory implements ServiceFactory {
    /** Handle to the framework. */
    BundleContext bc;

    /**
     * A FileLog that writes log entries to file. * (Accessed from the
     * LogConfigCommandGroup).
     */
    FileLog fileLog;

    /**
     * The logReaderServicies table maps LogReaderServiceImpl object to * the
     * bundle that owns the instance. * * The key is an instance of
     * LogReaderServiceImpl and the value the * Bundle object for the bundle
     * that uses the service.
     */
    Hashtable logReaderServicies = new Hashtable();

    LogConfigImpl configuration;

    /**
     * * The constructor fetches the destination(s) of the log entries * from
     * the system properties.
     */
    public LogReaderServiceFactory(BundleContext bc, LogConfigImpl lc) {
        this.bc = bc;
        configuration = lc;
        history = new LogEntry[configuration.getMemorySize()];
        historyInsertionPoint = 0;
        try {
            if (configuration.getFile()) {
                fileLog = new FileLog(bc, configuration);
            }
        } catch (Exception e) {
        }
        configuration.init(this);
    }

    /**
     * * The stop method is called by the bundle activator when the log * bundle
     * is stoped. If a <code>fileLog</code> is active terminate * it.
     */
    void stop() {
        if (fileLog != null) {
            synchronized (fileLog) {
                fileLog.stop();
                fileLog = null;
            }
        }
        configuration.stop();
    }

    /* Methods called when configuration is changed. */

    /**
     * * Reset number of log entries that are kept in memory. *
     * 
     * @param size
     *            the new maximum number of log entries in memory.
     */
    void resetMemorySize(int size, int memorySize) {
        if (size <= 0) {
            size = 1;
        }
        synchronized (history) {
            LogEntry[] new_history = new LogEntry[size];
            int leftSize = historyInsertionPoint;
            // Copy all entries to the left of the insertion point in
            // history to end of the new_history.
            if (leftSize > 0) {
                // There are entries to the left
                if (leftSize > size) {
                    // To many entries; ignore oldest (leftmost)
                    System.arraycopy(history, leftSize - size + 1, new_history,
                            1, size - 1);
                } else {
                    // Copy all entries to the left
                    System.arraycopy(history, 0, new_history, size - leftSize,
                            leftSize);
                    // Are there more entries to the right?
                    int remaindingSize = size - leftSize;
                    int remaindingEntries = memorySize - leftSize;
                    if (remaindingSize > remaindingEntries) {
                        // Copy all entries to the right
                        System.arraycopy(history, leftSize, new_history,
                                remaindingSize - remaindingEntries,
                                remaindingEntries);
                    } else {
                        // Too many entries; ignore oldest (leftmost)
                        System.arraycopy(history, memorySize - remaindingSize,
                                new_history, 0, remaindingSize);
                    }
                }
            } else {
                // Copy the last size entries from history
                int s = (size > memorySize) ? memorySize : size;
                int fromPos = (size > memorySize) ? 0 : memorySize - s;
                System.arraycopy(history, fromPos, new_history, size - s, s);
            }
            history = new_history;
            historyInsertionPoint = 0;
        }
    }

    private void resetFile(Boolean newValue, Boolean oldValue) {
        if (newValue.booleanValue() && fileLog == null) {
            fileLog = new FileLog(bc, configuration);
            if (oldValue == null) {
                synchronized (fileLog) {
                    synchronized (history) {
                        fileLog.saveMemEntries(new ArrayEnumeration(history,
                                historyInsertionPoint));
                    }
                }
            }
        } else if (!(newValue.booleanValue()) && (fileLog != null)) {
            synchronized (fileLog) {
                fileLog.stop();
                fileLog = null;
            }
        }
    }

    /* Method called by the LogConfig to indicate chage of properties. */
    void configChange(String propName, Object oldValue, Object newValue) {
        String name = propName;
        if (name.equals(LogConfigImpl.MEM)) {
            resetMemorySize(((Integer) newValue).intValue(),
                    ((Integer) oldValue).intValue());
        } else if (name.equals(LogConfigImpl.FILE)) {
            resetFile((Boolean) newValue, (Boolean) oldValue);
        } else if (name.equals(LogConfigImpl.GEN) && fileLog != null) {
            synchronized (fileLog) {
                fileLog.resetGenerations(((Integer) newValue).intValue(),
                        ((Integer) oldValue).intValue());
            }
        }
    }

    /**
     * Each Bundle gets its own LogReaderServiceImpl, and the
     * LogReaderServiceFactory keeps track of the created LogReaderServices.
     */
    public Object getService(Bundle bc, ServiceRegistration sd) {
        LogReaderServiceImpl lrsi = new LogReaderServiceImpl(this);
        logReaderServicies.put(lrsi, bc);
        return lrsi;
    }

    public void ungetService(Bundle bc, ServiceRegistration sd, Object s) {
        logReaderServicies.remove(s);
    }

    /*
     * This is the code that implements the log functionality! It keeps a short
     * history of the log in memory, this is the part that is returned when
     * getLog is called.
     */

    /** The history list (an array used as a circular list). */
    LogEntry[] history;

    /**
     * The index in <code>history</code> where the next entry shall be *
     * inserted.
     */
    int historyInsertionPoint = 0;

    /*
     * Return an enumeration of the historyLength last entries in the log.
     */
    public Enumeration getLog() {
        return new ArrayEnumeration(history, historyInsertionPoint);
    }

    /**
     * * Returns the filter level for a specific bundle. *
     * 
     * @param bundel
     *            the bundle to get the filter level for.
     */
    public int getLogLevel(final Bundle bundle) {
        // The synchronized block below is needed to work around a bug in
        // JDK1.2.2 on Linux (with green threads).
        synchronized (configuration) {
            Integer res = (Integer) AccessController
                    .doPrivileged(new PrivilegedAction() {
                        public Object run() {
                            return new Integer(getFilterLevel(bundle));
                        }
                    });
            return res.intValue();
        }
    }

    /**
     * * A new log entry has arrived. If its numeric level is less than * or
     * equal to the filter level then output it and store it in the * memory
     * log. All LogReaderServiceImpl is notified for all new * logEntries, i.e.,
     * the logFilter is not used for this. The * LogReaderService will notify
     * the LogListeners. *
     * 
     * @param le
     *            The new LogEntry
     */
    protected synchronized void log(final LogEntryImpl le) {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                if (le.getLevel() <= getFilterLevel(le.getBundle())) {
                    if (fileLog != null) {
                        synchronized (fileLog) {
                            fileLog.logged(le);
                        }
                    }
                    if (configuration.getOut()) {
                        System.out.println(le);
                    }
                    synchronized (history) {
                        history[historyInsertionPoint] = le;
                        historyInsertionPoint++;
                        if (historyInsertionPoint == history.length) {
                            historyInsertionPoint = 0;
                        }
                    }
                }
                for (Enumeration e = logReaderServicies.keys(); e
                        .hasMoreElements();) {
                    try {
                        ((LogReaderServiceImpl) e.nextElement()).callback(le);
                    } catch (Exception ce) {
                        // TBD Log error?
                    }
                }
                return null;
            }
        });
    }

    /**
     * Get current filter level given a bundle. *
     * 
     * @return filter level.
     */
    int getFilterLevel(Bundle b) {
        return (b != null) ? configuration.getLevel(b) : configuration
                .getFilter();
    }
}

/*
 * Auxiliary class used to create a Enumeration from an array. The array is
 * cloned to keep the array consistent even if the contents of the orginal array
 * is changed.
 * 
 * The elements in the array are not cloned, but they are not changed in the log
 * anyway.
 */

class ArrayEnumeration implements Enumeration {

    private Object[] array;

    private int pos;

    private int endPos;

    private boolean more = true;

    public ArrayEnumeration(Object[] a, int start) {
        this.array = (Object[]) a.clone();
        endPos = start;
        pos = start;
        nextPos();
    }

    /**
     * Decrement i until array[i] is non-null or there are no more * elements.
     * Sets <code>more</code> to false when the last element * has been
     * reached.
     */
    private void nextPos() {
        do {
            pos = (pos == 0) ? array.length - 1 : pos - 1;
        } while (array[pos] == null && pos != endPos);
        more = array[pos] != null;
    }

    public boolean hasMoreElements() {
        return more;
    }

    public Object nextElement() {
        if (more) {
            Object o = array[pos];
            if (pos == endPos) {
                more = false;
            } else {
                nextPos();
            }
            return o;
        }
        throw new NoSuchElementException();
    }

}

