/*
 * 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.util.*;
import java.text.SimpleDateFormat;
import java.security.*;
import java.io.*;

import org.osgi.framework.*;
import org.osgi.service.log.LogService;
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.
   **/
  private Hashtable logReaderServicies = new Hashtable();

  private LogConfig configuration;

  /**
   ** The constructor fetches the destination(s) of the log entries
   ** from the system properties.
   **/
  public LogReaderServiceFactory(BundleContext bc, LogConfig lc) {
    this.bc = bc;
    configuration = lc;
    history = new LogEntry[configuration.getMemSize()];
    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(LogConfig.MEM)){
      resetMemorySize(((Integer)newValue).intValue(), 
		      ((Integer)oldValue).intValue());
    } else if(name.equals(LogConfig.FILE)){
      resetFile((Boolean)newValue, (Boolean)oldValue);
    } else if(name.equals(LogConfig.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).*/
  private LogEntry[] history;
  /** The index in <code>history</code> where the next entry shall be
   ** inserted.
   **/
  private 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.
   */
  private 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;
    } else {
      throw  new NoSuchElementException();
    }
  }

}

