/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.log4j.chainsaw;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.event.EventListenerList;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.LogManager;
import org.apache.log4j.helpers.Constants;
import org.apache.log4j.net.SocketReceiver;
import org.apache.log4j.rule.ExpressionRule;
import org.apache.log4j.rule.Rule;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.LoggerRepositoryEx;
import org.apache.log4j.spi.LoggingEventFieldResolver;

/**
 * A handler class that either extends a particular appender hierarchy or can be
 * bound into the Log4j appender framework, and queues events, to be later
 * dispatched to registered/interested parties.
 * 
 * @author Scott Deboy &lt;sdeboy@apache.org&gt;
 * @author Paul Smith &lt;psmith@apache.org&gt;
 * 
 */
public class ChainsawAppenderHandler extends AppenderSkeleton {
  private static final String DEFAULT_IDENTIFIER = "Unknown";
  private final Object mutex = new Object();
  private int sleepInterval = 1000;
  private EventListenerList listenerList = new EventListenerList();
  private double dataRate = 0.0;
  private String identifierExpression;
  private final LoggingEventFieldResolver resolver = LoggingEventFieldResolver
      .getInstance();
  private PropertyChangeSupport propertySupport = new PropertyChangeSupport(
      this);
  private Map customExpressionRules = new HashMap();

  /**
   * NOTE: This variable needs to be physically located LAST, because
   * of the initialization sequence, the WorkQueue constructor starts a thread 
   * which ends up needing some reference to fields created in ChainsawAppenderHandler (outer instance)
   * which may not have been created yet.  Becomes a race condition, and therefore
   * this field initialization should be kept last.
   */
  private WorkQueue worker = new WorkQueue();
  
  public ChainsawAppenderHandler(final ChainsawAppender appender) {
    super(true);
    appender.setAppender(this);
  }

  public ChainsawAppenderHandler() {
    super(true);
  }

  public void setIdentifierExpression(String identifierExpression) {
    synchronized (mutex) {
      this.identifierExpression = identifierExpression;
      mutex.notify();
    }
  }

  public String getIdentifierExpression() {
    return identifierExpression;
  }

  public void addCustomEventBatchListener(String identifier,
      EventBatchListener l) throws IllegalArgumentException {
    customExpressionRules.put(identifier, ExpressionRule.getRule(identifier));
    listenerList.add(EventBatchListener.class, l);
  }

  public void addEventBatchListener(EventBatchListener l) {
    listenerList.add(EventBatchListener.class, l);
  }

  public void removeEventBatchListener(EventBatchListener l) {
    listenerList.remove(EventBatchListener.class, l);
  }

  public void append(LoggingEvent event) {
    worker.enqueue(event);
  }

  public void close() {}

  public boolean requiresLayout() {
    return false;
  }

  public int getQueueInterval() {
    return sleepInterval;
  }

  public void setQueueInterval(int interval) {
    sleepInterval = interval;
  }

  /**
   * Determines an appropriate title for the Tab for the Tab Pane by locating a
   * the hostname property
   * 
   * @param e
   * @return identifier
   */
  String getTabIdentifier(LoggingEvent e) {
    String ident = resolver.applyFields(identifierExpression, e);
    return ((ident != null) ? ident : DEFAULT_IDENTIFIER);
  }

  /**
   * A little test bed
   * 
   * @param args
   */
  public static void main(String[] args) throws InterruptedException {
    ChainsawAppenderHandler handler = new ChainsawAppenderHandler();
    handler.addEventBatchListener(new EventBatchListener() {
      public String getInterestedIdentifier() {
        return null;
      }

      public void receiveEventBatch(String identifier, List events) {
        System.out.println("received " + events.size());
      }
    });
    LogManager.getRootLogger().addAppender(handler);
    SocketReceiver receiver = new SocketReceiver(4445);
    ((LoggerRepositoryEx) LogManager.getLoggerRepository()).getPluginRegistry().addPlugin(receiver);
    receiver.activateOptions();
    Thread.sleep(60000);
  }

  /**
   * Exposes the current Data rate calculated. This is periodically updated by
   * an internal Thread as is the number of events that have been processed, and
   * dispatched to all listeners since the last sample period divided by the
   * number of seconds since the last sample period.
   * 
   * This method fires a PropertyChange event so listeners can monitor the rate
   * 
   * @return double # of events processed per second
   */
  public double getDataRate() {
    return dataRate;
  }

  /**
   * @param dataRate
   */
  void setDataRate(double dataRate) {
    double oldValue = this.dataRate;
    this.dataRate = dataRate;
    propertySupport.firePropertyChange("dataRate", new Double(oldValue),
        new Double(this.dataRate));
  }

  /**
   * @param listener
   */
  public synchronized void addPropertyChangeListener(
      PropertyChangeListener listener) {
    propertySupport.addPropertyChangeListener(listener);
  }

  /**
   * @param propertyName
   * @param listener
   */
  public synchronized void addPropertyChangeListener(String propertyName,
      PropertyChangeListener listener) {
    propertySupport.addPropertyChangeListener(propertyName, listener);
  }

  /**
   * @param listener
   */
  public synchronized void removePropertyChangeListener(
      PropertyChangeListener listener) {
    propertySupport.removePropertyChangeListener(listener);
  }

  /**
   * @param propertyName
   * @param listener
   */
  public synchronized void removePropertyChangeListener(String propertyName,
      PropertyChangeListener listener) {
    propertySupport.removePropertyChangeListener(propertyName, listener);
  }

  /**
   * Queue of Events are placed in here, which are picked up by an asychronous
   * thread. The WorkerThread looks for events once a second and processes all
   * events accumulated during that time..
   */
  class WorkQueue {
    final ArrayList queue = new ArrayList();
    Thread workerThread;

    protected WorkQueue() {
      workerThread = new WorkerThread();
      workerThread.start();
    }

    public final void enqueue(LoggingEvent event) {
      synchronized (mutex) {
        queue.add(event);
        mutex.notify();
      }
    }

    public final void stop() {
      synchronized (mutex) {
        workerThread.interrupt();
      }
    }

    /**
     * The worker thread converts each queued event to a vector and forwards the
     * vector on to the UI.
     */
    private class WorkerThread extends Thread {
      public WorkerThread() {
        super("Chainsaw-WorkerThread");
        setDaemon(true);
        setPriority(Thread.NORM_PRIORITY - 1);
      }

      public void run() {
        List innerList = new ArrayList();
        while (true) {
          long timeStart = System.currentTimeMillis();
          synchronized (mutex) {
            try {
              while ((queue.size() == 0) || (identifierExpression == null)) {
                setDataRate(0);
                mutex.wait();
              }
              if (queue.size() > 0) {
                innerList.addAll(queue);
                queue.clear();
              }
            }
            catch (InterruptedException ie) {}
          }
          int size = innerList.size();
          if (size > 0) {
            Iterator iter = innerList.iterator();
            ChainsawEventBatch eventBatch = new ChainsawEventBatch();
            while (iter.hasNext()) {
              LoggingEvent e = (LoggingEvent) iter.next();
              // attempt to set the host name (without port), from
              // remoteSourceInfo
              // if 'hostname' property not provided
              if (e.getProperty(Constants.HOSTNAME_KEY) == null) {
                String remoteHost = e
                    .getProperty(ChainsawConstants.LOG4J_REMOTEHOST_KEY);
                if (remoteHost != null) {
                  int colonIndex = remoteHost.indexOf(":");
                  if (colonIndex == -1) {
                    colonIndex = remoteHost.length();
                  }
                  e.setProperty(Constants.HOSTNAME_KEY, remoteHost.substring(0,
                      colonIndex));
                }
              }
              for (Iterator itery = customExpressionRules.entrySet().iterator(); itery
                  .hasNext();) {
                Map.Entry entry = (Map.Entry) itery.next();
                Rule rule = (Rule) entry.getValue();
                if (rule.evaluate(e, null)) {
                  eventBatch.addEvent((String) entry.getKey(), e);
                }
              }
              eventBatch.addEvent(getTabIdentifier(e), e);
            }
            dispatchEventBatch(eventBatch);
            innerList.clear();
          }
          if (getQueueInterval() > 1000) {
            try {
              synchronized (this) {
                wait(getQueueInterval());
              }
            }
            catch (InterruptedException ie) {}
          } else {
            Thread.yield();
          }
          if (size == 0) {
            setDataRate(0.0);
          } else {
            long timeEnd = System.currentTimeMillis();
            long diffInSeconds = (timeEnd - timeStart) / 1000;
            double rate = (((double) size) / diffInSeconds);
            setDataRate(rate);
          }
        }
      }

      /**
       * Dispatches the event batches contents to all the interested parties by
       * iterating over each identifier and dispatching the
       * ChainsawEventBatchEntry object to each listener that is interested.
       * 
       * @param eventBatch
       */
      private void dispatchEventBatch(ChainsawEventBatch eventBatch) {
        EventBatchListener[] listeners = (EventBatchListener[]) listenerList
            .getListeners(EventBatchListener.class);
        for (Iterator iter = eventBatch.identifierIterator(); iter.hasNext();) {
          String identifier = (String) iter.next();
          List eventList = null;
          for (int i = 0; i < listeners.length; i++) {
            EventBatchListener listener = listeners[i];
            if ((listener.getInterestedIdentifier() == null)
                || listener.getInterestedIdentifier().equals(identifier)) {
              if (eventList == null) {
                eventList = eventBatch.entrySet(identifier);
              }
              listener.receiveEventBatch(identifier, eventList);
            }
          }
          eventList = null;
        }
      }
    }
  }
}
