// ============================================================================
// File:               $File$
//
// Project:
//
// Purpose:
//
// Author:             Rammi
//
// Copyright Notice:   (c) 1999-2006  Rammi (rammi@caff.de)
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Latest change:      $Date: 2006-02-01 13:48:49 +0100 (Mi, 01 Feb 2006) $
//
// History:	       $Log$
//=============================================================================

package de.caff.util.debug;

import java.text.DateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;

/**
 *  This class &quot;cooks&quot; debug messages. I.e. the messages are prepended
 *  with date and type information and extended with a line showing where
 *  the message was created. The message itself is inserted by a tab.
 *
 *  @see Debug
 *
 *  @author Rammi
 */
class DebugMessageCook
        implements AnyMessageDebugListener,
                   DebugConstants
{
  private static final boolean THROW_ASSERTION_EXCEPTION = true;
  private static final boolean STOP_ON_FATAL_ERRORS      = true;
  private static final boolean EMPTY_THROW_ASSERTION     = false;
  private static final int     EMPTY_FATAL_RETURN        = 0;

  private static final String  HEAD_TRACE     = "TRACE";
  private static final String  HEAD_MESSAGE   = "MESSAGE";
  private static final String  HEAD_WARNING   = "WARNING";
  private static final String  HEAD_ERROR     = "ERROR";
  private static final String  HEAD_FATAL     = "FATAL ERROR";
  private static final String  HEAD_LOG       = "LOGGING";
  private static final String  HEAD_ASSERTION = "ASSERTION FAILED";

  private LinkedList _list = new LinkedList();

  /**
   *  Data collection for listener.
   */
  static class ListenerData {
    CookedMessageDebugListener listener;
    boolean                    stopOnFatalErrors;
    boolean                    throwAssertionException;

    /**
     *  Construtor.
     *  Allows to set what happens to fatal errors and failed assertions.
     *  @param  listener           the listener waiting to be served by this cook
     *  @param  stopOnFatalErrors  stop program on fatal errors?
     *  @param  throwAssertionException throw exception on failed assertions?
     *  @exception NullPointerException if the listener is not set
     */
    ListenerData(CookedMessageDebugListener listener,
		 boolean stopOnFatalErrors,
		 boolean throwAssertionException) 
      throws NullPointerException
    {
      if (listener == null) {
	throw new NullPointerException();
      }
      this.listener                = listener;
      this.stopOnFatalErrors       = stopOnFatalErrors;
      this.throwAssertionException = throwAssertionException;
    }  
  }

  /**
   *  Add a listener. This one stops on fatal errors and throws an exception
   *  on failed assertions.
   *  @param  listener           the listener waiting to be served by this cook
   */
  public void addListener(CookedMessageDebugListener listener) {
    _list.add(new ListenerData(listener, 
			       STOP_ON_FATAL_ERRORS, 
			       THROW_ASSERTION_EXCEPTION));
  }
  
  /**
   *  Add a listener.
   *  @param  listener           the listener waiting to be served by this cook
   *  @param  stopOnFatalErrors  stop program on fatal errors?
   *  @param  throwAssertionException throw exception on failed assertions?
   */
  public void addListener(CookedMessageDebugListener listener,
			  boolean stopOnFatalErrors,
			  boolean throwAssertionException) {
    _list.add(new ListenerData(listener, stopOnFatalErrors, throwAssertionException));
  }

  /**
   *  Remove a listener.
   *  @param  listener   listener to be removed
   */
  public void removeListener(CookedMessageDebugListener listener) {
    for (Iterator it = _list.iterator();  it.hasNext();  ) {
      ListenerData data = (ListenerData)it.next();
      if (data.listener == listener) {
	it.remove();
	break;
      }
    }
  }
  
  /**
   *  Create formatted output. Prepend with date/category and append stack position.
   *  @param head    header
   *  @param msg     message
   *  @return formatted version of message
   */
  private static String getFormatted(String head, String msg) {
    StringBuffer compile = new StringBuffer();
    compile.append(DateFormat.getDateTimeInstance().format(new Date())).append('\t').append(head).append(":\n");
    int lastIndex = 0;
    int index     =  msg.indexOf('\n');
    while (index != -1) {
      compile.append('\t').append(msg.substring(lastIndex, index+1));
      lastIndex = index+1;
      index     =  msg.indexOf('\n', lastIndex);
    }
    compile.append('\t').append(msg.substring(lastIndex)).append('\n');

    return compile.toString();
  }

  /**
   *  Sends the cooked message to all listeners.
   *  @param msgType type of message
   *  @param cooked  cooked message
   *  @param pos     position
   */
  private void distribute(int msgType, String cooked, String pos) {
    for (Iterator it = _list.iterator();  it.hasNext();  ) {
      ((ListenerData)it.next()).listener.receiveCookedMessage(msgType, cooked, pos);
    }
  }

  /**
   *  Sends the cooked fatal message to all listeners.
   *  If at least one wants to exit the program this is returned.
   *  @param cooked  cooked message
   *  @param pos     postion
   *  @return <code>1</code>, if at least one listener wants to exit<br>
   *          <code>0</code> else
   */
  private int distributeFatal(String cooked, String pos) {
    boolean exit = false;
    for (Iterator it = _list.iterator();  it.hasNext();  ) {
      ListenerData data = (ListenerData)it.next();
      data.listener.receiveCookedMessage(FATAL, cooked, pos);
      if (!exit  &&  data.stopOnFatalErrors) {
	exit = true;
      }
    }
    return exit ? 1 : 0;
  }

  /**
   *  Sends the assertion message to all listeners.
   *  If at least one wants to throw an exception the program this is returned.
   *  @param cooked  cooked message
   *  @param pos     postion
   *  @return <code>true</code>, if at least one listener wants to throw<br>
   *          <code>false</code> else
   */
  private boolean distributeFailedAssertion(String cooked, String pos) {
    boolean dothrow = false;
    for (Iterator it = _list.iterator();  it.hasNext();  ) {
      ListenerData data = (ListenerData)it.next();
      data.listener.receiveCookedMessage(ASSERT, cooked, pos);
      if (!dothrow  &&  data.stopOnFatalErrors) {
	dothrow = true;
      }
    }
    return dothrow;
  }

  /**
   *  Receive a trace debug message, cook it and serve it to the listeners.
   *  @param msg  the raw message
   *  @param pos  postion
   */
  public void receiveTraceMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      distribute(TRACE, getFormatted(HEAD_TRACE, msg), pos);
    }
  }

  /**
   *  Receive a standard debug message, cook it and serve it to the listeners.
   *  @param msg  the raw message
   *  @param pos  postion
   */
  public void receiveStandardMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      distribute(MESSAGE, getFormatted(HEAD_MESSAGE, msg), pos);
    }
  }

  /**
   *  Receive a warning debug message, cook it and serve it to the listeners.
   *  @param msg  the raw message
   *  @param pos  postion
   */
  public void receiveWarningMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      distribute(WARNING, getFormatted(HEAD_WARNING, msg), pos);
    }
  }

  /**
   *  Receive an error debug message, cook it and serve it to the listeners.
   *  @param msg  the raw message
   *  @param pos  postion
   */
  public void receiveErrorMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      distribute(ERROR, getFormatted(HEAD_ERROR, msg), pos);
    }
  }

  /**
   *  Receive a logging message, cook it and serve it to the listeners.
   *  @param msg  the raw message
   *  @param pos  postion
   */
  public void receiveLogMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      distribute(LOG, getFormatted(HEAD_LOG, msg), pos);
    }
  }

  /**
   *  Receive a fatal error  message, cook it and serve it to the listeners.
   *  If the listener list is empty, <code>0</code> is returned.
   *  @param msg  the raw message
   *  @param pos  postion
   *  @return not equal to <code>0</code> for exit<br>
   *          else keep program running
   */
  public int receiveFatalMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      return distributeFatal(getFormatted(HEAD_FATAL, msg), pos);
    }
    else {
      return EMPTY_FATAL_RETURN;
    }
  }

  /**
   *  Receive a failed assertion message, cook it and serve it to the listeners.
   *  If the listener list is empty, <code>false</code> is returned.
   *  @param msg  the raw message
   *  @param pos  postion
   *  @return throw an assertion?
   */
  public boolean receiveFailedAssertionMessage(String msg, String pos) {
    if (!_list.isEmpty()) {
      return distributeFailedAssertion(getFormatted(HEAD_ASSERTION, msg), pos);
    }
    else {
      return EMPTY_THROW_ASSERTION;
    }
  }


  /**
   *  Get a position in default cooked format.
   *  @param  pos position string
   *  @return cooked version to be appended to a message
   */
  public static String cookedPosition(String pos) {
    return "\t[at "+pos+"]\n";
  }
  

}




