package org.testng.log4testng;


import org.testng.Assert;
import org.testng.collections.Maps;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

/**
 * TestNG support logging via a custom logging framework similar to
 * <a href="http://logging.apache.org/log4j"> Log4j</a>. To control logging, add a
 * resource named "log4testng.properties" to your classpath. The logging levels are
 * TRACE, DEBUG, INFO, WARN, ERROR and FATAL.
 * The Logging framework has the following characteristics:
 *
 * <ul>
 * <li>All logging is done using System.out (for levels &lt; ERROR) or System.err. There
 * is no way to specify Appenders.</li>
 * <li>There is no way to control logging programmatically.</li>
 * <li>The log4testng.properties resource is searched in the classpath on the first
 * call to the logging API. If it is not present, logging defaults to the WARN
 * level.</li>
 * </ul>
 *
 * The property file contains lines in the following format:
 *
 * <pre><code>
 * # log4testng will log its own behavior (generally used for debugging this package only).
 * log4testng.debug=true
 *
 * # Specifies the root Loggers logging level. Will log DEBUG level and above
 * log4testng.rootLogger=DEBUG
 *
 * # The org.testng.reporters.EmailableReporter Logger will log TRACE level and above
 * log4testng.logger.org.testng.reporters.EmailableReporter=TRACE
 *
 * # All Logger in packages below org.testng will log WARN level and above
 * log4testng.logger.org.testng=WARN
 * </code></pre>
 *
 * In your source files you will typically instantiate and use loggers this ways:
 * <pre><code>
 * import org.testng.log4testng.Logger;
 *
 * class ThisClass {
 *     private static final Logger LOGGER = Logger.getLogger(ThisClass.class);
 *
 *     ...
 *     LOGGER.debug("entering myMethod()");
 *     ...
 *     LOGGER.warn("unknown file: " + filename);
 *     ...
 *     LOGGER.error("Unexpected error", exception);
 * </code></pre>
 */
public class Logger {

  // Attribute an hierarchical integer value to all levels.
  private static int i= 0;
  private static final int TRACE= i++;
  private static final int DEBUG= i++;
  private static final int INFO= i++;
  private static final int WARN= i++;
  private static final int ERROR= i++;
  private static final int FATAL= i++;
  private static final int LEVEL_COUNT= i;

  /** Standard prefix of all property names in log4testng.properties. */
  private static final String PREFIX= "log4testng.";

  /** Standard prefix of all logger names in log4testng.properties. */
  private static final String LOGGER_PREFIX= PREFIX + "logger.";

  /** Root logger name in log4testng.properties. */
  private static final String ROOT_LOGGER= PREFIX + "rootLogger";

  /** Debug property name in log4testng.properties. */
  private static final String DEBUG_PROPERTY= PREFIX + "debug";

  /** The standard error stream (this is allways System.err except for unit tests) */
  private static PrintStream err= System.err;

  /** The standard output stream (this is allways System.out except for unit tests) */
  private static PrintStream out= System.out;

  /** An ordered list of level names. */
  private static final String[] levelNames= new String[LEVEL_COUNT];

  static {
    levelNames[TRACE]= "TRACE";
    levelNames[DEBUG]= "DEBUG";
    levelNames[INFO]= "INFO";
    levelNames[WARN]= "WARN";
    levelNames[ERROR]= "ERROR";
    levelNames[FATAL]= "FATAL";
  }

  /** A map from level name to level integer index (TRACE->0, DEBUG->1 ...) */
  private static final Map<String, Integer> levelMap= Maps.newHashMap();

  static {
    for(i= 0; i < LEVEL_COUNT; ++i) {
      levelMap.put(levelNames[i], new Integer(i));
    }
  }

  /** true if the Logging system has been initialized. */
  private static boolean initialized;

  /** Map from Logger names to level index (as specified in log4testng.properties) */
  private static final Map<String, Integer> loggerLevels = Maps.newHashMap();

  /** Map of all known loggers. */
  private static final Map<Class, Logger> loggers = Maps.newHashMap();

  /** The logging level of the root logger (defaults to warn). */
  private static int rootLoggerLevel= WARN;

  /** Should log4testng log what it is doing (defaults to false). */
  private static boolean debug= false;

  /** The logger's level */
  private final int level;

  /** The logger's name. */
  private final Class klass;
  private final String m_className;

  /**
   * Retrieve a logger named according to the value of the pClass.getName()
   * parameter. If the named logger already exists, then the existing instance
   * will be returned. Otherwise, a new instance is created. By default, loggers
   * do not have a set level but inherit it from their nearest ancestor with
   * a set level.
   *
   * @param pClass The class' logger to retrieve.
   * @return a logger named according to the value of the pClass.getName().
   */
  public static synchronized Logger getLogger(Class pClass) {
    initialize();
    Logger logger= loggers.get(pClass);
    if(logger != null) {
      return logger;
    }
    int level= getLevel(pClass);
    logger= new Logger(pClass, level);
    loggers.put(pClass, logger);

    return logger;
  }

  /**
   * Check whether this logger is enabled for the TRACE Level.
   * @return true if this logger is enabled for level TRACE, false otherwise.
   */
  public boolean isTraceEnabled() {
    return isLevelEnabled(TRACE);
  }

  /**
   * Log a message object with the TRACE level. This method first checks if this
   * logger is TRACE enabled. If this logger is TRACE enabled, then it converts
   * the message object (passed as parameter) to a string by invoking toString().
   * WARNING Note that passing a Throwable to this method will print the name of
   * the Throwable but no stack trace. To print a stack trace use the
   * trace(Object, Throwable) form instead.
   * @param message the message object to log.
   */
  public void trace(Object message) {
    log(TRACE, message, null);
  }

  /**
   * Log a message object with the TRACE level including the stack trace of the
   * Throwable t passed as parameter.
   * See Logger.trace(Object) form for more detailed information.
   * @param message the message object to log.
   * @param t the exception to log, including its stack trace.
   */
  public void trace(Object message, Throwable t) {
    log(TRACE, message, t);
  }

  /**
   * Check whether this logger is enabled for the DEBUG Level.
   * @return true if this logger is enabled for level DEBUG, false otherwise.
   */
  public boolean isDebugEnabled() {
    return isLevelEnabled(DEBUG);
  }

  /**
   * Log a message object with the DEBUG level.
   * See Logger.trace(Object) form for more detailed information.
   * @param message the message object to log.
   */
  public void debug(Object message) {
    log(DEBUG, message, null);
  }

  /**
   * Log a message object with the DEBUG level including the stack trace of the
   * Throwable t passed as parameter.
   * See Logger.trace(Object, Throwable) form for more detailed information.
   * @param message the message object to log.
   * @param t the exception to log, including its stack trace.
   */
  public void debug(Object message, Throwable t) {
    log(DEBUG, message, t);
  }

  /**
   * Check whether this logger is enabled for the INFO Level.
   * @return true if this logger is enabled for level INFO, false otherwise.
   */
  public boolean isInfoEnabled() {
    return isLevelEnabled(INFO);
  }

  /**
   * Log a message object with the INFO level.
   * See Logger.trace(Object) form for more detailed information.
   * @param message the message object to log.
   */
  public void info(Object message) {
    log(INFO, message, null);
  }

  /**
   * Log a message object with the WARN level including the stack trace of the
   * Throwable t passed as parameter.
   * See Logger.trace(Object, Throwable) form for more detailed information.
   * @param message the message object to log.
   * @param t the exception to log, including its stack trace.
   */
  public void info(Object message, Throwable t) {
    log(INFO, message, t);
  }

  /**
   * Log a message object with the WARN level.
   * See Logger.trace(Object) form for more detailed information.
   * @param message the message object to log.
   */
  public void warn(Object message) {
    log(WARN, message, null);
  }

  /**
   * Log a message object with the ERROR level including the stack trace of the
   * Throwable t passed as parameter.
   * See Logger.trace(Object, Throwable) form for more detailed information.
   * @param message the message object to log.
   * @param t the exception to log, including its stack trace.
   */
  public void warn(Object message, Throwable t) {
    log(WARN, message, t);
  }

  /**
   * Log a message object with the ERROR level.
   * See Logger.trace(Object) form for more detailed information.
   * @param message the message object to log.
   */
  public void error(Object message) {
    log(ERROR, message, null);
  }

  /**
   * Log a message object with the DEBUG level including the stack trace of the
   * Throwable t passed as parameter.
   * See Logger.trace(Object, Throwable) form for more detailed information.
   * @param message the message object to log.
   * @param t the exception to log, including its stack trace.
   */
  public void error(Object message, Throwable t) {
    log(ERROR, message, t);
  }

  /**
   * Log a message object with the FATAL level.
   * See Logger.trace(Object) form for more detailed information.
   * @param message the message object to log.
   */
  public void fatal(Object message) {
    log(FATAL, message, null);
  }

  /**
   * Log a message object with the FATAL level including the stack trace of the
   * Throwable t passed as parameter.
   * See Logger.trace(Object, Throwable) form for more detailed information.
   * @param message the message object to log.
   * @param t the exception to log, including its stack trace.
   */
  public void fatal(Object message, Throwable t) {
    log(FATAL, message, t);
  }

  private Logger(Class pClass, int pLevel) {
    level= pLevel;
    klass= pClass;
    m_className= pClass.getName().substring(pClass.getName().lastIndexOf('.') + 1);
  }

  private static synchronized void initialize() {
    if(initialized) {
      return;
    }

    // We flag as initialized right away because if anything goes wrong
    // We still consider it initialized. TODO Is this OK?
    initialized= true;

    InputStream is= Thread.currentThread().getContextClassLoader().getResourceAsStream("log4testng.properties");
    if(is == null) {
      return;
    }
    Properties properties= new Properties();
    try {
      properties.load(is);
    }
    catch(IOException e) {
      throw new RuntimeException(e);
    }

    checkProperties(properties);
  }

  private static void checkProperties(Properties pProperties) {
    {
      // See if we want to debug log4testng
      String debugStr= pProperties.getProperty(DEBUG_PROPERTY);
      if(debugStr != null) {
        if(debugStr.equalsIgnoreCase("true")) {
          debug= true;
        }
        else if(debugStr.equalsIgnoreCase("false")) {
          debug= false;
        }
        else {
          throw new IllegalArgumentException("Unknown " + DEBUG_PROPERTY
                                             + " value " + debugStr);
        }
      }
      loglog4testng("log4testng.debug set to " + debug);
    }
    
    {
      // Set the value of the root logger (if any).
      String rootLevelStr= pProperties.getProperty(ROOT_LOGGER);
      if(rootLevelStr != null) {
        Integer ilevel= levelMap.get(rootLevelStr.toUpperCase());
        if(ilevel == null) {
          throw new IllegalArgumentException("Unknown level for log4testng.rootLogger "
                                             + rootLevelStr + " in log4testng.properties");
        }
        rootLoggerLevel= ilevel.intValue();
        loglog4testng("Root level logger set to " + rootLevelStr + " level.");
      }
    }

    Iterator it= pProperties.entrySet().iterator();
    while(it.hasNext()) {
      Map.Entry entry= (Entry) it.next();
      String logger= (String) entry.getKey();
      String level= (String) entry.getValue();

      if(!logger.startsWith(PREFIX)) {
        throw new IllegalArgumentException("Illegal property value: " + logger);
      }
      if(logger.equals(DEBUG_PROPERTY)) {
        // Already handled
      }
      else if(logger.equals(ROOT_LOGGER)) {
        // Already handled
      }
      else {
        if(!logger.startsWith(LOGGER_PREFIX)) {
          throw new IllegalArgumentException("Illegal property value: " + logger);
        }

        Integer ilevel= levelMap.get(level.toUpperCase());
        if(ilevel == null) {
          throw new IllegalArgumentException("Unknown level " + level + " for logger " + logger
                                             + " in log4testng.properties");
        }

        loggerLevels.put(logger.substring(LOGGER_PREFIX.length()), ilevel);
        loglog4testng("logger " + logger + " set to " + ilevel + " level.");
      }
    }
  }

  /**
   * Returns the level associated to the current class. The level is obtain by searching
   * for a logger in the "testng-logging.properties" resource. For example, if class is
   * "org.testng.TestNG" the the following loggers are searched in this order:
   * <ol>
   * <li>"org.testng.TestNG"</li>
   * <li>"org.testng"</li>
   * <li>"org"</li>
   * <li>The root level</li>
   * </ol>
   *
   * @param pClass the class name used for logger name.
   * @return the level associated to the current class.
   */
  private static int getLevel(Class pClass) {
    String name= pClass.getName();
    loglog4testng("Getting level for logger " + name);
    while(true) {
      Integer level= loggerLevels.get(name);
      if(level != null) {
        loglog4testng("Found level " + level + " for logger " + name);

        return level.intValue();
      }
      int dot= name.lastIndexOf('.');
      if(dot != -1) {
        loglog4testng("Found level " + rootLoggerLevel + " for root logger");

        // Logger name not found. Defaults to root logger level.
        return rootLoggerLevel;
      }
      name= name.substring(0, dot);
    }
  }

  private boolean isLevelEnabled(int pLevel) {
    return level <= pLevel;
  }

  private void log(int pLevel, Object pMessage, Throwable pT) {
    if(isLevelEnabled(pLevel)) {
      PrintStream ps= (pLevel >= ERROR) ? err : out;
      if(null != pT) {
        synchronized(ps) {
          ps.println("[" + m_className + "] [" + levelNames[pLevel] + "] " + pMessage);
          pT.printStackTrace(ps);
        }
      }
      else {
        ps.println("[" + m_className + "] [" + levelNames[pLevel] + "] " + pMessage);
      }
    }
  }

  /**
   * Logs the message to System.out of debug is on.
   * @param pmessage the message to log to the console
   */
  private static void loglog4testng(String pmessage) {
    if(debug) {
      out.println("[log4testng] [debug] " + pmessage);
    }
  }

  /**
   * This method is for debugging purpose only.
   *
   * @param pProperties a properties bundle initialised as log4testng
   * property file would be.
   * @param pOut the standard output stream to be used for logging.
   * @param pErr the standard error stream to be used for logging.
   */
  private static synchronized void testInitialize(Properties pProperties,
                                                  PrintStream pOut,
                                                  PrintStream pErr) {
    initialized= true;
    loggers.clear();
    rootLoggerLevel= WARN;
    debug= false;
    out= pOut;
    err= pErr;
    checkProperties(pProperties);
  }

  /**
   * Makes sure the default debug value is false.
   */
  private static void testDebugDefault() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.rootLogger", "WARN");
    testInitialize(props, out2, err2);
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
  }

  /**
   * Makes sure the debug value can be turned on and actualls logs something.
   */
  private static void testDebugOn() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.debug", "true");
    props.put("log4testng.rootLogger", "WARN");
    testInitialize(props, out2, err2);
    Assert.assertTrue(out1.toString().startsWith("[log4testng][debug]"));
    Assert.assertEquals(err1.toString(), "");
  }

  /**
   * Makes sure the debug value can be turned off and logs nothing.
   */
  private static void testDebugOff() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.debug", "false");
    props.put("log4testng.rootLogger", "WARN");
    testInitialize(props, out2, err2);
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
  }

  /**
   * Makes sure an illegal debug value throws an exception.
   */
  private static void testDebugError() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.debug", "unknown");
    props.put("log4testng.rootLogger", "WARN");
    try {
      testInitialize(props, out2, err2);
      throw new RuntimeException("failure");
    }
    catch(IllegalArgumentException pEx) {

      // Normal case
      Assert.assertEquals(out1.toString(), "");
      Assert.assertEquals(err1.toString(), "");
    }
  }

  /**
   * Tests that the root logger's default level is WARN and that loggers do not
   * log bellow this level and do log in the correct stream for levels equal to
   * and above WARN.
   */
  private static void testRootLoggerDefault() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    testInitialize(props, out2, err2);

    Logger strLogger= Logger.getLogger(String.class);
    strLogger.trace("trace should not appear");
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
    strLogger.debug("debug should not appear");
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
    strLogger.info("info should not appear");
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
    strLogger.warn("warn should appear");
    int outlength= out1.toString().length();
    Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [WARN] warn should appear"));
    Assert.assertEquals(err1.toString(), "");
    strLogger.error("error should appear");
    Assert.assertEquals(out1.toString().length(), outlength);
    Assert.assertTrue(err1.toString().startsWith("[java.lang.String] [ERROR] error should appear"));
    strLogger.fatal("fatal should appear");
    Assert.assertEquals(out1.toString().length(), outlength);
    Assert.assertTrue(err1.toString().contains("[java.lang.String] [FATAL] fatal should appear"));
  }

  /**
   * Test setting the root logger level
   */
  private static void testRootLoggerSet() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.rootLogger", "DEBUG");
    testInitialize(props, out2, err2);

    Logger strLogger= Logger.getLogger(String.class);
    strLogger.trace("trace should appear");
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
    strLogger.debug("debug should appear");
    Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [DEBUG] debug should appear"));
    Assert.assertEquals(err1.toString(), "");
  }

  /**
   * Test setting the root logger to an illegal level value throws an exception.
   */
  private static void testRootLoggerSetError() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.rootLogger", "unknown");
    try {
      testInitialize(props, out2, err2);
      throw new RuntimeException("failure");
    }
    catch(IllegalArgumentException pEx) {

      // Normal case
      Assert.assertEquals(out1.toString(), "");
      Assert.assertEquals(err1.toString(), "");
    }
  }

  /**
   * Test setting a user logger level
   */
  private static void testUserLoggerSet() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.logger.java.lang.String", "DEBUG");
    testInitialize(props, out2, err2);

    Logger strLogger= Logger.getLogger(String.class);
    strLogger.trace("trace should not appear");
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
    strLogger.debug("debug should appear");
    int outLength= out1.toString().length();
    Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [DEBUG] debug should appear"));
    Assert.assertEquals(err1.toString(), "");

    Logger classLogger= Logger.getLogger(Class.class);
    classLogger.debug("debug should not appear");
    Assert.assertEquals(out1.toString().length(), outLength);
    Assert.assertEquals(err1.toString(), "");
    classLogger.warn("warn should appear");
    Assert.assertTrue(out1.toString().contains("[java.lang.Class] [WARN] warn should appear"));
    Assert.assertEquals(err1.toString(), "");
  }

  /**
   * Test setting a user logger to an illegal level value throws an exception
   */
  private static void testUserLoggerSetError() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.logger.java.lang.String", "unknown");
    try {
      testInitialize(props, out2, err2);
      throw new RuntimeException("failure");
    }
    catch(IllegalArgumentException pEx) {

      // Normal case
      Assert.assertEquals(out1.toString(), "");
      Assert.assertEquals(err1.toString(), "");
    }
  }

  /**
   * Tests setting a partial logger name (a hierarchy scope)
   */
  private static void testUserLoggerSetHierarchy() {
    Properties props= new Properties();
    ByteArrayOutputStream out1= new ByteArrayOutputStream();
    ByteArrayOutputStream err1= new ByteArrayOutputStream();
    PrintStream out2= new PrintStream(out1);
    PrintStream err2= new PrintStream(err1);
    props.put("log4testng.logger.java.lang", "DEBUG");
    testInitialize(props, out2, err2);

    Logger strLogger= Logger.getLogger(String.class);
    strLogger.trace("trace should not appear");
    Assert.assertEquals(out1.toString(), "");
    Assert.assertEquals(err1.toString(), "");
    strLogger.debug("debug should appear");
    Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [DEBUG] debug should appear"));
    Assert.assertEquals(err1.toString(), "");
  }

  /**
   * Run all tests. (very crusty ...)
   * @param pArgs not used
   */
  public static void main(String[] pArgs) {
    testDebugDefault();
    testDebugOn();
    testDebugOff();
    testDebugError();
    testRootLoggerDefault();
    testRootLoggerSet();
    testRootLoggerSetError();
    testUserLoggerSet();
    testUserLoggerSetError();
    testUserLoggerSetHierarchy();
  }
}
