package nxm.sys.lib;

import static java.time.Instant.ofEpochMilli;
import static java.time.Instant.ofEpochSecond;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySortedSet;
import static java.util.Collections.singleton;
import static java.util.Objects.requireNonNull;
import static nxm.sys.inc.MapKBT.newMap;
import static nxm.sys.inc.MapKBT.singletonMap;
import static nxm.sys.inc.MapKBT.wrapMap;
import static nxm.sys.lib.JsonUtilities.toJSON;
import static nxm.sys.lib.PropsUtilities.extractProps;
import static nxm.sys.lib.PropsUtilities.loadProps;
import static nxm.sys.lib.Util.currentTimeNano;
import static nxm.sys.lib.Util.first;
import static nxm.sys.lib.Util.round;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.function.Supplier;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.KeyValuePair;
import nxm.sys.inc.MapKBT;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.SpecialLoggingRequired;
import nxm.sys.lib.JsonUtilities.Mode;
/** Various logging functions. The logging methods are intended to function similar to the normal
 *  Java logging methods, except:
 *  <pre>
 *    LogUtilities.log(class, level, msg, args); // providing logging class improves speed
 *    or
 *    LogUtilities.log(level, msg, args);        // simpler API and logging class is auto-discovered
 *  </pre>
 *  Replaces:
 *  <pre>
 *    Logger.getLogger(MyClass.class.getName()).log(level, msg, args);
 *  </pre>
 *  The supported logging levels in descending order are:
 *  <pre>
 *    NAME      | VALUE | SEE ALSO
 *    ----------+-------+-----------------------------
 *    ADMIN     |  1100 | {@link #Level_ADMIN}
 *    SEVERE    |  1000 | {@link Level#SEVERE}
 *    WARNING   |   900 | {@link Level#WARNING}
 *    INFO      |   800 | {@link Level#INFO}
 *    CONFIG    |   700 | {@link Level#CONFIG}
 *    VERBOSE   |   600 | {@link #Level_VERBOSE}
 *    ----------+-------+-----------------------------
 *    FINE      |   500 | {@link Level#FINE}
 *    FINER     |   400 | {@link Level#FINER}
 *    FINEST    |   300 | {@link Level#FINEST}
 *    DEBUG     |   200 | {@link #Level_DEBUG}
 *    ----------+-------+-----------------------------
 *  </pre>
 *  Implementation Note:
 *  <pre>
 *    This class appeared in a couple different places before being "adopted" into the NeXtMidas
 *    baseline. Due to the wide prior-use, the API has been retained, including the support for
 *    "extra" fields that are generally associated with Elasticstack/LogStash.
 *  </pre>
 *  @author J. Czechowski
 *  @since  NeXtMidas 3.9.0
 */
@ProvisionalUseOnly("Not fully tested and API could change")
@SpecialLoggingRequired
@SuppressWarnings({"BroadCatchBlock", "TooBroadCatch", "UseSpecificCatch", "ClassWithMultipleLoggers"})
public final class LogUtilities {
  private LogUtilities () { } // prevent instantiation

  private static final Object[] ONE_ARG  = new Object[0];  // Has one argument (passed separate)
  private static final Object[] NO_ARGS  = null;           // Has no arguments

  /** The VERBOSE logging level. This comes below CONFIG but above FINE. */
  public static final Level VERBOSE = new Level("VERBOSE", 600) {
    private static final long serialVersionUID = 315666203327385033L;
  };

  /** The DEBUG logging level. This comes below FINEST and will cause the log to absolutely spew
   *  output. However, enabling this can be useful for debugging.
   */
  public static final Level DEBUG = new Level("DEBUG", 200) {
    private static final long serialVersionUID = 315666203327385033L;
  };

  /** The ADMIN logging level. This is above SEVERE and intended to be used with administrative
   *  messages pushed through the logging system for tracking purposes such as "start of log"
   *  and "end of log" messages. This differs from CONFIG which is intended for basic configuration
   *  messages and will be excluded if the logging level is set to INFO, WARNING, or SEVERE. <br>
   *  <br>
   *  This also differs from SEVERE in its naming since an ADMIN message is purely informative in
   *  nature and does not denote an erroneous occurrence.
   */
  public static final Level ADMIN = new Level("ADMIN", 1100) {
    private static final long serialVersionUID = 315666203327385033L;
  };

  /** The VERBOSE logging level. This is identical to {@link #VERBOSE}. */
  public static final Level Level_VERBOSE = VERBOSE;

  /** The DEBUG logging level. This is identical to {@link #DEBUG}. */
  public static final Level Level_DEBUG = DEBUG;

  /** The ADMIN logging level. This is identical to {@link #ADMIN}. */
  public static final Level Level_ADMIN = ADMIN;

  // We need to use the traditional logger methods from this class since the methods in this class
  // have special initialization and calling restrictions. Note that this must come AFTER we define
  // VERBOSE and DEBUG otherwise it can cause a "Bad level value for property: jicd.app.level" error.
  private static final Logger LOG = Logger.getLogger(LogUtilities.class.getName());

  private static final int INFO_INT_LEVEL    = 800;  // same as Level.INFO.intValue();
  private static final int WARNING_INT_LEVEL = 900;  // same as Level.WARNING.intValue();
  private static final int SEVERE_INT_LEVEL  = 1000; // same asLevel.SEVERE.intValue();

  private static final int NOT_AVAILABLE = -1; // This information is not available

  // Cache the loggers being supported. This is faster than always calling Logger.getLogger(..)
  // since it can assume that the user isn't altering the resource bundle.
  private static final Map<String,Logger> LOGGERS = new WeakHashMap<>(128);
  private static final Logger             GLOBAL  = Logger.getGlobal();
  private static final Method             demandLogger;

  private static final String JDK_17_INACCESSIBLEOBJECTEXCEPTION_CLASS_STRING = "class java.lang.reflect.InaccessibleObjectException";

  static {
    // ==== INIT demandLogger =====================================================================
    // This code will throw an illegal access error in JDK 17+ and a warning in earlier versions
    // To get rid of that warning add this jvm option to the runtime:
    // --add-opens java.logging/java.util.logging=ALL-UNNAMED
    Method _demandLogger = null;
    try {
      _demandLogger = Logger.class.getDeclaredMethod("demandLogger", String.class, String.class, Class.class);
      _demandLogger.setAccessible(true);
    }
    catch (Throwable t) {
      if (t.getClass().toString().equals(JDK_17_INACCESSIBLEOBJECTEXCEPTION_CLASS_STRING)) {
        LOG.log(Level.WARNING, "java.lang.reflect.InaccessibleObjectException: To use Logger.demandLogger(..) add this jvm option: --add-opens java.logging/java.util.logging=ALL-UNNAMED");
      }
      else {
        LOG.log(Level.FINE, "Could not use Logger.demandLogger(..)", t);
      }
      _demandLogger = null;
    }
    demandLogger = _demandLogger;
  }

  //***************************************************************************
  //***************************************************************************
  //  Initialization and Loading of Properties related methods
  //***************************************************************************
  //***************************************************************************

  /** Initializes the logging utilities. */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void initLogUtilities () {
    // Initialization done in the static initializer, but calling this method gives an easy way
    // for another class to ensure that the static initializer has been invoked.
  }

  /** Reset any cached logging settings. A call to this method implies a call to
   *  {@link #initLogUtilities()} if such has not already been done.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void resetLogUtilities () {
    initLogUtilities();
    LOGGERS.clear();
  }

  /** Loads the logging properties from a MapKBT with "LOGGING-" prefixes
   *  on the logging key names. This will call {@link #resetLogUtilities()}
   *  to ensure any cached settings are reloaded.
   *  @param propMap The MapKBT that has the logging properties
   *  @return properties
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Properties loadLoggingProperties (MapKBT propMap) {
    return loadLoggingProperties(propMap, true);
  }

  /** Loads the logging properties from a MapKBT. This will call
   *  {@link #resetLogUtilities()} to ensure any cached settings are reloaded.
   *  @param propMap   The MapKBT that has the logging properties
   *  @param hasPrefix Indicates if the logging properties are Piranha properties
   *                   file style with "LOGGING-" prefix (true) or in the normal
   *                   "logging.properties" style (false)
   *  @return properties
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Properties loadLoggingProperties (MapKBT propMap, boolean hasPrefix) {
    Properties properties = PropsUtilities.loadProps(propMap);
    loadLoggingProperties(properties, hasPrefix);
    return properties;
  }

  /** Loads the logging properties from a properties file with "LOGGING-" prefixes
   *  on the logging properties. This will call {@link #resetLogUtilities()} to ensure any cached
   *  settings are reloaded.
   *  @param propsFile The name of the properties file to load.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void loadLoggingProperties (String propsFile) {
    loadLoggingProperties(loadProps(propsFile), true);
  }

  /** Loads the logging properties from a {@link Properties} object with "LOGGING-"
   *  prefixes on the logging properties. This will call {@link #resetLogUtilities()} to ensure any
   *  cached settings are reloaded.
   *  @param props     The {@link Properties} object to load the logging properties from.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void loadLoggingProperties (Properties props) {
    loadLoggingProperties(props, true);
  }

  /** Loads the logging properties from a properties file. This will call
   *  {@link #resetLogUtilities()} to ensure any cached settings are reloaded.
   *  @param propsFile The name of the properties file to load.
   *  @param hasPrefix Indicates if the logging properties are in a Piranha properties file with
   *                   the "LOGGING-" prefix (true) or if the logging properties are from a normal
   *                   "logging.properties" file (false).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void loadLoggingProperties (String propsFile, boolean hasPrefix) {
    loadLoggingProperties(loadProps(propsFile), hasPrefix);
  }

  /** Loads the logging properties from a {@link Properties} object. This will call
   *  {@link #resetLogUtilities()} to ensure any cached settings are reloaded.
   *  @param props     The {@link Properties} object to load the logging properties from.
   *  @param hasPrefix Indicates if the logging properties are in a Piranha properties file with
   *                   the "LOGGING-" prefix (true) or if the logging properties are from a normal
   *                   "logging.properties" file (false).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void loadLoggingProperties (Properties props, boolean hasPrefix) {
    Properties logProps = (hasPrefix)? extractProps(props, "LOGGING-", false) : props;

    try {
      StringWriter out = new StringWriter();
      logProps.store(out, null);
      ByteArrayInputStream in = new ByteArrayInputStream(out.toString().getBytes());
      resetLogUtilities();
      LogManager.getLogManager().readConfiguration(in);
    }
    catch (IOException | SecurityException e) {
      // This should be exceedingly rare, if not impossible, since we are using ByteArrayInputStream
      // rather than reading from disk.
      throw new RuntimeException("Unable to load logging properties", e);
    }
  }

  /** Lazily converts properties to a printable string. For properties this specifically
   *  converts any '\n' values into '\\n' since that is usually the desired usage.
   *  @param props The properties to convert.
   *  @return The supplier that will do the conversion.
   */
  @InternalUseOnly
  public static Map<String,String> propsAsMap (Properties props) {
   if (props == null) return null;

    Enumeration<?>         e   = props.propertyNames();
    TreeMap<String,String> map = new TreeMap<>();
    while (e.hasMoreElements()) {
      String key = e.nextElement().toString();
      String val = props.getProperty(key);

      if (val != null) {
        val = val.replaceAll("\n", "\\n")
                 .replaceAll("\r", "\\r")
                 .replaceAll("\f", "\\f");
      }
      map.put(key, val);
    }
    return map;
  }

  //***************************************************************************
  //***************************************************************************
  // PUBLIC RECORD BASED static LOG methods -
  // logs include class name, method name, and line number
  //***************************************************************************
  //***************************************************************************

  //***************************************************************************
  //** Log methods with the calling/logging class as a parameter
  //** 1) enabling a very quick return if logging is not needed and
  //** 2) a quicker look up of method and line number when logging is needed
  //***************************************************************************

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param level The log level of the message
   *  @param msg   The message to be logged
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, String msg) {
    _log_with_class(clazz, null, level, msg, null, null, NO_ARGS);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param param  Parameter to the log message.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz,Level level, String msg, Object param) {
    _log_with_class(clazz, null, level, msg, null, param, ONE_ARG);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, String msg, Object... params) {
    _log_with_class(clazz,null, level, msg, null, null, params);
  }

  /** Logs a message describing an exception where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, String msg, Throwable thrown) {
    _log_with_class(clazz, null, level, msg, thrown, null, NO_ARGS);
  }

  /** Logs a message describing an exception where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   *  @param param  Parameter to the log message.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, String msg, Throwable thrown, Object param) {
    _log_with_class(clazz, null, level, msg, thrown, param, ONE_ARG);
  }

  /** Logs a message describing an exception where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, String msg, Throwable thrown, Object... params) {
    _log_with_class(clazz, null, level, msg, thrown, null, params);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz       The calling class
   *  @param level       The severity level (not null).
   *  @param msgSupplier A function, which when called, produces the desired log message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, Supplier<String> msgSupplier) {
    _log_with_class(clazz, null, level, null, msgSupplier);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz       The calling class
   *  @param level       The severity level (not null).
   *  @param thrown      Throwable associated with log message.
   *  @param msgSupplier A function, which when called, produces the desired log message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Level level, Throwable thrown, Supplier<String> msgSupplier) {
    _log_with_class(clazz, null, level, thrown, msgSupplier);
  }

  // Logs that provide a Midas reference as well as the calling/logging class
  // and look up class, method, and line number

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz  The calling class
   *  @param midas Midas context - used only for Midas logging and can be null
   *  @param level The severity level (not null).
   *  @param msg   The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Midas midas, Level level, String msg) {
    _log_with_class(clazz, midas, level, msg, null, null, NO_ARGS);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class
   *  @param clazz  The calling class
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Midas midas, Level level, String msg, Object params) {
    _log_with_class(clazz, midas, level, msg, null, params, ONE_ARG);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz  The calling class
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Midas midas, Level level, String msg, Object... params) {
    _log_with_class(clazz, midas, level, msg, null, null, params);
  }

  /** Logs a message describing an exception where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz The class that is logging the message
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Midas midas, Level level, String msg, Throwable thrown) {
    _log_with_class(clazz, midas, level, msg, thrown, null, NO_ARGS);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz       The calling class
   *  @param midas       Midas context - used only for Midas logging and can be null
   *  @param level       The severity level (not null).
   *  @param msgSupplier A function, which when called, produces the desired log message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Midas midas, Level level, Supplier<String> msgSupplier) {
    _log_with_class(clazz, midas, level, null, msgSupplier);
  }

  /** Logs a message with line number, class name and method name where it will
   *  use the first stack trace element that matches the given class.
   *  @param clazz       The calling class
   *  @param midas       Midas context - used only for Midas logging and can be null
   *  @param level       The severity level (not null).
   *  @param thrown      Throwable associated with log message.
   *  @param msgSupplier A function, which when called, produces the desired log message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Class<?> clazz, Midas midas, Level level, Throwable thrown, Supplier<String> msgSupplier) {
    _log_with_class(clazz, midas, level, thrown, msgSupplier);
  }

  //***************************************************************************
  //** Logs with a simpler API that auto-discover calling/logging class
  //** 1) should not be used in very time critical code
  //** 2) needs to auto-discover logging class before can determine if logging
  //**    (but this code is highly optimized)
  //** 3) the extra time needed when actually logging is probably negligible
  //***************************************************************************

  /** Logs a message with line number, class name and method name<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level The severity level (not null).
   *  @param msg   The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Level level, String msg) {
    _log(null, level, msg, null, null, NO_ARGS);
  }

  /** Logs a message with line number, class name and method name
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param param  Parameter to the log message.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Level level, String msg, Object param) {
    _log(null, level, msg, null, param, ONE_ARG);
  }

  /** Logs a message with line number, class name and method name<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Level level, String msg, Object... params) {
    _log(null, level, msg, null, null, params);
  }

  /** Logs a message describing an exception.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Level level, String msg, Throwable thrown) {
    _log(null, level, msg, thrown, null, NO_ARGS);
  }

  /** Logs a message describing an exception.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   *  @param param  Parameter to the log message.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Level level, String msg, Throwable thrown, Object param) {
    _log(null, level, msg, thrown, param, ONE_ARG);
  }

  /** Logs a message describing an exception.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Level level, String msg, Throwable thrown, Object... params) {
    _log(null, level, msg, thrown, null, params);
  }

  //** Logs that provide a Midas reference and look up class, method, and line number

  /** Logs a message with line number, class name and method name<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param midas Midas context - used only for Midas logging and can be null
   *  @param level The severity level (not null).
   *  @param msg   The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Midas midas, Level level, String msg) {
    _log(midas, level, msg, null, null, NO_ARGS);
  }

  /** Logs a message with line number, class name and method name<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Midas midas, Level level, String msg, Object params) {
    _log(midas, level, msg, null, params, ONE_ARG);
  }

  /** Logs a message with line number, class name and method name<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param params Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void log (Midas midas, Level level, String msg, Object... params) {
    _log(midas, level, msg, null, null, params);
  }


  //** Logs that allow alternate messages depending on log level.  These look up class, method,
  //   and line number

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level1 The severity level (not null).
   *  @param msg1   The message (or a key in the message catalog).
   *  @param level2 The severity level (not null).
   *  @param msg2   The message (or a key in the message catalog).
   *  @param params Array of parameters to the message for *both* messages.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Level level1, String msg1,
                                Level level2, String msg2, Object... params) {
    _log(null, requireNonNull(level1), msg1, null, null, params,
         requireNonNull(level2), msg2, null, null, params);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param params1 Array of parameters to the message
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param params2 Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Level level1, String msg1, Object    params1,
                                Level level2, String msg2, Object... params2) {
    _log(null, requireNonNull(level1), msg1, null, params1, ONE_ARG,
         requireNonNull(level2), msg2, null, null, params2);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param params1 Array of parameters to the message
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param params2 Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Level level1, String msg1, Object[]  params1,
                                Level level2, String msg2, Object... params2) {
    _log(null, requireNonNull(level1), msg1, null, null, params1,
         requireNonNull(level2), msg2, null, null, params2);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param thrown1 The exception.
   *  @param params1 Array of parameters to the message
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param thrown2 The exception.
   *  @param params2 Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Level level1, String msg1, Throwable thrown1, Object    params1,
                                Level level2, String msg2, Throwable thrown2, Object... params2) {
    _log(null, requireNonNull(level1), msg1, thrown1, params1, ONE_ARG,
         requireNonNull(level2), msg2, thrown2, null, params2);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param thrown1 The exception.
   *  @param params1 Array of parameters to the message
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param thrown2 The exception.
   *  @param params2 Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Level level1, String msg1, Throwable thrown1, Object[]  params1,
                                Level level2, String msg2, Throwable thrown2, Object... params2) {
    _log(null, requireNonNull(level1), msg1, thrown1, null, params1,
         requireNonNull(level2), msg2, thrown2, null, params2);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level1 The severity level (not null).
   *  @param msg1   The message (or a key in the message catalog).
   *  @param level2 The severity level (not null).
   *  @param msg2   The message (or a key in the message catalog).
   *  @param params Array of parameters to the message for *both* messages.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Midas midas, Level level1, String msg1,
                                Level level2, String msg2, Object... params) {
    _log(midas, requireNonNull(level1), msg1, null, null, params,
         requireNonNull(level2), msg2, null, null, params);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param params1 Array of parameters to the message
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param params2 Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Midas midas, Level level1, String msg1, Object    params1,
                                Level level2, String msg2, Object... params2) {
    _log(midas, requireNonNull(level1), msg1, null, params1, ONE_ARG,
         requireNonNull(level2), msg2, null, null, params2);
  }

  /** Logs either message #1 or message #2 depending on the log level. This makes it easy to log
   *  additional details if the logging level is higher.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param midas  Midas context - used only for Midas logging and can be null
   *  @param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param params1 Array of parameters to the message
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param params2 Array of parameters to the message
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logEither (Midas midas, Level level1, String msg1, Object[]  params1,
                                Level level2, String msg2, Object... params2) {
    _log(midas, requireNonNull(level1), msg1, null, null, params1,
         requireNonNull(level2), msg2, null, null, params2);
  }

  /** Logs an exception that was uncaught by the normal methods in a thread. This is intended to
   *  be used along with {@link Thread#setUncaughtExceptionHandler(UncaughtExceptionHandler)}
   *  such as:
   *  {@code
   *    myThread.setUncaughtExceptionHandler(LogUtilities::logUncaughtException);
   *  }<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param t the thread
   *  @param e the exception
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void logUncaughtException (Thread t, Throwable e) {
    log(Level.SEVERE, "Caught exception in {0}", e, t.getName());
  }

  //***************************************************************************
  //  PUBLIC LOG wrapper methods for logging specific level
  //  these also look up and include line number, class name, and method name
  /****************************************************************************

  /** Logs a SEVERE message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void severe (String msg) {
    log(Level.SEVERE, msg);
  }

  /** Logs a WARNING message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void warning (String msg) {
    log(Level.WARNING, msg);
  }

  /** Logs a INFO message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void info (String msg) {
    log(Level.INFO, msg);
  }

  /** Logs a CONFIG message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void config (String msg) {
    log(Level.CONFIG, msg);
  }

  /** Logs a VERBOSE message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void verbose (String msg) {
    log(Level_VERBOSE, msg);
  }

  /** Logs a FINE message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void fine (String msg) {
    log(Level.FINE, msg);
  }

  /** Logs a FINER message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void finer (String msg) {
    log(Level.FINER, msg);
  }

  /** Logs a FINEST message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void finest (String msg) {
    log(Level.FINEST, msg);
  }

  /** Logs a DEBUG message.<br>
   *  Note: The calling/logging class is auto-discovered before
   *  the logging level is checked
   *  @param msg The message (or a key in the message catalog).
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static void debug (String msg) {
    log(Level_DEBUG, msg);
  }

  //***************************************************************************
  //  private log methods containing actual implementation
  //****************************************************************************

  /** Logs a message with class name, method name, line number, and any exception information
   *  @param midas Midas context Used only for Midas logging and can be null)
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   *  @param params Array of parameters to the message
   *  @param param  Single-parameter param list.
   *  @param extra  Any "extra" tags.
   */
  private static void _log (Midas midas, Level level, String msg, Throwable thrown, Object param, Object[] params) {
    _log(midas, level, msg,  thrown, param, params,
         null,  null, null, null,  null);
  }

  /** Logs a message with class name, method name, line number, and any exception information
   *  @param clazz  The class that is logging the message (used to find the method and line number of the caller)
   *  @param midas  Midas context Used only for Midas logging and can be null)
   *  @param level  The severity level (not null).
   *  @param msg    The message (or a key in the message catalog).
   *  @param thrown The exception.
   *  @param params Array of parameters to the message
   *  @param param  Single-parameter param list.
   *  @param extra  Any "extra" tags.
   */
  private static void _log_with_class (Class<?> clazz, Midas midas, Level level, String msg, Throwable thrown, Object param, Object[] params) {
    _log_with_class(clazz, midas, level, msg,  thrown, param, params,
         null,  null, null, null,  null);
  }

  /** Logs a message with class name, method name, line number, and any exception information
   *  @param clazz       The class that is logging the message (used to find the method
   *                     and line number of the caller)
   *  @param midas       Midas context used only for Midas logging and can be null)
   *  @param level       The severity level (not null).
   *  @param thrown      The exception.
   *  @param msgSupplier A function, which when called, produces the desired log message
   */
  private static void _log_with_class (Class<?> clazz, Midas midas, Level level, Throwable thrown, Supplier<String> msgSupplier) {
    _log_with_class(clazz, midas, level, thrown, msgSupplier, null,  null, null);
  }

  /** Logs a message with class name, method name, line number, and any exception information
   *  @param midas   Midas context used only for Midas logging and can be null)@param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param thrown1 The exception.
   *  @param params1 Array of parameters to the message
   *  @param param1  Single parameter param list. [We use this to skip an array creation in the case
   *                 where it is not loggable.]
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param thrown2 The exception.
   *  @param params2 Array of parameters to the message
   *  @param param2  Single-parameter param list. [We use this to skip an array creation in the case
   *                 where it is not loggable.]
   */
  private static void _log (Midas midas, Level level1, String msg1, Throwable thrown1, Object param1, Object[] params1,
                            Level level2, String msg2, Throwable thrown2, Object param2, Object[] params2) {
    // ==== GET THE LOGGER =========================================================================
    StackTraceElement caller = CallStackUtil.getCaller(LogUtilities.class);
    Logger            log;

    if ((caller != null) && (caller.getClassName() != null)) {
      log = LOGGERS.get(caller.getClassName());
      if (log == null) {
        log = getLogger(caller);
        LOGGERS.put(caller.getClassName(), log);
      }
    }
    else {
      log = GLOBAL;
    }


    // ==== DETERMINE WHICH ONE TO LOG =============================================================
    if ((level2 == null) || (level1.intValue() >= level2.intValue())) {
      // use the first level
      if (!log.isLoggable(level1)) return;
    }
    else {
      // use the second level
      if (!log.isLoggable(level2)) return;
      level1  = level2;
      msg1    = msg2;
      thrown1 = thrown2;
      param1    = param2;
      params1   = params2;
    }

   // convert since parameter to an array
   if (params1 == ONE_ARG) {
      params1 = new Object[] { param1 };
   }

    LogUtilities._logRecord(midas, caller, level1, msg1, thrown1, params1, log);
  }

  /**
   * This utility method will generate the static logger for a classs and add the handler to it.
   * @param className  The class name to create the logger for
   * @param handler    The handler to be added to the logger
   */
  @InternalUseOnly
  public static void createAndAddHandler(String className, Handler handler) {
    Logger log;

    log = LOGGERS.get(className);
    if (log == null) {
      log = getLogger(className);
      LOGGERS.put(className, log);
    }
    log.addHandler(handler);
  }

  /** Logs a message with class name, method name, line number, and any exception information
   *  @param clazz   The class that is logging the message (used to find the method and line number of the caller)
   *  @param midas   Midas context used only for Midas logging and can be null)@param level1  The severity level (not null).
   *  @param msg1    The message (or a key in the message catalog).
   *  @param thrown1 The exception.
   *  @param params1 Array of parameters to the message
   *  @param param1  Single parameter param list. [We use this to skip an array creation in the case
   *                 where it is not loggable.]
   *  @param level2  The severity level (not null).
   *  @param msg2    The message (or a key in the message catalog).
   *  @param thrown2 The exception.
   *  @param params2 Array of parameters to the message
   *  @param param2  Single-parameter param list. [We use this to skip an array creation in the case
   *                 where it is not loggable.]
   */
  private static void _log_with_class (Class<?> clazz, Midas midas, Level level1, String msg1, Throwable thrown1, Object param1,
      Object[] params1, Level level2, String msg2, Throwable thrown2, Object param2, Object[] params2) {

    // ==== GET THE LOGGER =========================================================================
    Logger log;

    log = LOGGERS.get(clazz.getName());
    if (log == null) {
      log = getLogger(clazz.getName());
      LOGGERS.put(clazz.getName(), log);
    }

    // ==== DETERMINE WHICH ONE TO LOG =============================================================
    if ((level2 == null) || (level1.intValue() >= level2.intValue())) {
      // use the first level
      if (!log.isLoggable(level1)) return;
    }
    else {
      // use the second level
      if (!log.isLoggable(level2)) return;
      level1  = level2;
      msg1    = msg2;
      thrown1 = thrown2;
      param1    = param2;
      params1   = params2;
    }

    // convert since parameter to an array
    if (params1 == ONE_ARG) {
      params1 = new Object[] { param1 };
    }

    // Get the StackTraceElement matching this class
    StackTraceElement caller = CallStackUtil.getCallerByClass(clazz);

    LogUtilities._logRecord(midas, caller, level1, msg1, thrown1, params1, log);
  }

  /** Logs a message with class name, method name, line number, and any exception information
   *  @param clazz        The class that is logging the message (used to find the method
   *                      and line number of the caller)
   *  @param midas        Midas context used only for Midas logging and can be null)
   *  @param level1       The severity level (not null).
   *  @param thrown1      The exception.
   *  @param msgSupplier1 A function, which when called, produces the desired log message
   *  @param level2       The severity level (not null).
   *  @param thrown2      The exception.
   *  @param msgSupplier1 A function, which when called, produces the desired log message
   */
  private static void _log_with_class (Class<?> clazz, Midas midas, Level level1, Throwable thrown1,
      Supplier<String> msgSupplier1, Level level2, Throwable thrown2, Supplier<String> msgSupplier2) {

    // ==== GET THE LOGGER =========================================================================
    Logger log;

    log = LOGGERS.get(clazz.getName());
    if (log == null) {
      log = getLogger(clazz.getName());
      LOGGERS.put(clazz.getName(), log);
    }

    // ==== DETERMINE WHICH ONE TO LOG =============================================================
    if ((level2 == null) || (level1.intValue() >= level2.intValue())) {
      // use the first level
      if (!log.isLoggable(level1)) return;
    }
    else {
      // use the second level
      if (!log.isLoggable(level2)) return;
      level1  = level2;
      msgSupplier1    = msgSupplier2;
      thrown1 = thrown2;
    }

    // Get the StackTraceElement matching this class
    StackTraceElement caller = CallStackUtil.getCallerByClass(clazz);

    LogUtilities._logRecord(midas, caller, level1, msgSupplier1.get(), thrown1, null, log);
  }

  /** Writes a log message including class name, method name, line number, and
   *  any exception information.<br>
   *  Note: This is only called after it has been determined that logging is needed
   *  @param midas   Midas context Used only for Midas logging and can be null)
   *  @param caller  StackTraceElement containing the caller information
   *  @param level   The severity level (not null).
   *  @param msg     The message (or a key in the message catalog).
   *  @param thrown  The exception.
   *  @param params  Array of parameters to the message
   *  @param log     The logger
   */
  private static void _logRecord (Midas midas, StackTraceElement caller, Level level, String msg,
                                  Throwable thrown, Object[] params, Logger log) {

    // ==== CHECK FOR ANY StringSupplier INSTANCES =================================================
    if (params != null) {
      for (int i = 0; i < params.length; i++) {
        if (params[i] instanceof Supplier) {
          params[i] = ((Supplier) params[i]).get();
        }
      }
    }
    // By default, coreio option is not set
    boolean classBaseLoggerShouldUseMidasLogger = false;
    if (Shell.isShellInit()) {
      // If the shell has been initialized, check if the coreio option is set.
      classBaseLoggerShouldUseMidasLogger = Shell.getMidasContext().io
          .isOptionSet(CoreIO.IOOptions.ClassBasedLoggerShouldUseMidasLogger);
    }

    if (level.intValue() < INFO_INT_LEVEL || !classBaseLoggerShouldUseMidasLogger) {
      // ==== BUILD THE LOG RECORD ===================================================================
      EnhancedLogRecord rec = new EnhancedLogRecord(level, msg);
      rec.setResourceBundle(log.getResourceBundle());
      rec.setResourceBundleName(log.getResourceBundleName());
      rec.setParameters(params);
      rec.setThrown(thrown);
      if (caller != null) {
        rec.setSourceClassName(caller.getClassName());
        rec.setSourceMethodName(caller.getMethodName());
        rec.setSourceLineNumber(caller.getLineNumber());
        rec.setSourceFileName(caller.getFileName());
      }
      else {
        // prevent it from listing this class by inference
        rec.setSourceClassName(null);
        rec.setSourceMethodName(null);
        rec.setSourceLineNumber(null);
        rec.setSourceFileName(null);
      }

      // ==== WRITE TO THE LOG =======================================================================
      log.log(rec);
    }
    else {
      midasLog(midas, level, caller.getClassName(), caller.getMethodName(), caller.getLineNumber(), msg, params);
    }
  }

  //******** log method that does Midas logging *************************************************
  /** Midas Log where caller is discovered
   *
   *  @param midas  Midas
   *  @param level  Logging Level
   *  @param msg    message
   *  @param params parameters
   */
  public static void midasLog (Midas midas, Level level, String msg, Object[] params) {
    StackTraceElement caller = CallStackUtil.getCaller(LogUtilities.class);
    midasLog(midas, level, caller.getClassName(), caller.getMethodName(), caller.getLineNumber(), msg, params);
  }

  /** Transfer the log message information into a Midas log message
   *  @param midas        The Midas context to use if doing Midas logging (can be null)
   *  @param level        The severity level (not null)
   *  @param sourceClass  name of class that issued the logging request
   *  @param sourceMethod name of method that issued the logging request
   *  @param lineNumber   line number that issued the logging request
   *  @param msg          The string message (or a key in the message catalog)
   *  @param params       Array of parameters to the message
   */
  private static void midasLog (Midas midas, Level level, String sourceClass,
                                String sourceMethod, int lineNumber, String msg, Object[] params) {
    int intLogLevel = level.intValue();
    if (intLogLevel < INFO_INT_LEVEL) return; // Midas does not log anything below INFO
    if (midas == null) {
      // use shared Midas context - if we get here then the Shell is already loaded so this is safe
      midas = Shell.getMidasContext();
    }
    // from will include SourceName.sourceMethod:lineNumber
    String from = (lineNumber == NOT_AVAILABLE) ? sourceClass+"."+sourceMethod :
                  sourceClass+"."+sourceMethod+":"+lineNumber;
    // message will include parameters
    if (params!=null) {
      for (int i = 0; i < params.length; i++) {
        msg = msg.replace("{"+i+"}", params[i].toString());
      }
    }
    switch (intLogLevel) {
      case INFO_INT_LEVEL:    midas.message(msg, Midas.INFO, from); break;
      case WARNING_INT_LEVEL: midas.message(msg, Midas.WARNING, from); break;
      case SEVERE_INT_LEVEL:  midas.message(msg, Midas.ERROR, from); break;
    }
  }

  //****************************************************************************
  //****************************************************************************
  //  JSON related logging methods
  //****************************************************************************
  //****************************************************************************

  //***************** PUBLIC JSON related methods ******************************

  /** Lazily converts an object to a printable string.
   *  @param obj The map to convert.
   *  @return The supplier that will do the conversion.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Supplier<String> lazyToString (Object obj) {
    return () -> _toJSON(obj);
  }

  /** Lazily converts properties to a printable string. For properties this specifically
   *  converts any '\n' values into '\\n' since that is usually the desired usage.
   *  @param props The properties to convert.
   *  @return The supplier that will do the conversion.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Supplier<String> lazyToString (Properties props) {
    return () -> _toJSON(propsAsMap(props));
  }

  /** Lazily converts a character array to a printable string.
   *  @param buf The buffer containing the string content.
   *  @return The supplier that will do the conversion.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Supplier<String> lazyToString (byte[] buf) {
    return () -> _toJSON(buf, 0, -1);
  }

  /** Lazily converts a character array to a printable string.
   *  @param buf The buffer containing the string content.
   *  @return The supplier that will do the conversion.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Supplier<String> lazyToString (char[] buf) {
    return () -> _toJSON(buf, 0, -1);
  }

  /** Lazily converts a character array to a printable string.
   *  @param buf The buffer containing the string content.
   *  @param off The offset into the buffer.
   *  @param len The length of the string.
   *  @return The supplier that will do the conversion.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  @SuppressWarnings("ImplicitArrayToString")
  public static Supplier<String> lazyToString (byte[] buf, int off, int len) {
    return () -> _toJSON(buf, off, len);
  }

  /** Lazily converts a character array to a printable string.
   *  @param buf The buffer containing the string content.
   *  @param off The offset into the buffer.
   *  @param len The length of the string.
   *  @return The supplier that will do the conversion.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  @SuppressWarnings("ImplicitArrayToString")
  public static Supplier<String> lazyToString (char[] buf, int off, int len) {
    return () -> _toJSON(buf, off, len);
  }

  //***************** PRIVATE JSON related methods ******************************

  /** Converts to a JSON string.
   *  @param obj The object.
   *  @return The string.
   */
  private static String _toJSON (Object obj) {
    if (obj == null) return "null";
    try {
      return toJSON(obj, Mode.Auto).toString();
    }
    catch (Exception e) {
      return "<"+e+">";
    }
  }

  /** Converts to a JSON string.
   *  @param buf The buffer containing the string content.
   *  @param off The offset into the buffer.
   *  @param len The length of the string.
   *  @return The supplier that will do the conversion.
   */
  private static String _toJSON (byte[] buf, int off, int len) {
    if (buf == null) return "null";
    try {
      if (len < 0) len = buf.length;
      return toJSON(new String(buf, off, len)).toString();
    }
    catch (Exception e) {
      return "<"+e+">";
    }
  }

  /** Converts to a JSON string.
   *  @param buf The buffer containing the string content.
   *  @param off The offset into the buffer.
   *  @param len The length of the string.
   *  @return The supplier that will do the conversion.
   */
  private static String _toJSON (char[] buf, int off, int len) {
    if (buf == null) return "null";
    try {
      if (len < 0) len = buf.length;
      return toJSON(new String(buf, off, len)).toString();
    }
    catch (Exception e) {
      return "<"+e+">";
    }
  }


  /** Gets the demandLogger initialized in the static block
   *  @return logger or null if received Illegal Access Exception
   */
  @InternalUseOnly
  public static Method getDemandLogger () {
    return demandLogger;
  }

  /** Gets the logger for the calling class.
   *  Warning: This exposes the current underlying logging utility.  It is not
   *  intended for general use, as the underlying logging utility may change in
   *  future.
   *  @return The logger (currently a java.util.Logger)
   */
  @InternalUseOnly
  public static Logger getLogger () {
    return getLogger(CallStackUtil.getCaller(LogUtilities.class));
  }

  /** Gets the logger for the named calling class.
   *  @param caller The calling class.
   *  @return The logger.
   */
  private static Logger getLogger (StackTraceElement caller) {
    if (demandLogger != null) {
      // Logger.getLogger(..) is CallerSensitive, so this calls the method that underlies
      // the Logger.getLogger(..) method (at least in OpenJDK).
      try {
        Class<?> callingClass = Class.forName(caller.getClassName());
        return (Logger)demandLogger.invoke(null, caller.getClassName(), null, callingClass);
      }
      catch (Throwable t) {
        LOG.log(Level.WARNING, "Unable to use demandLogger", t);
        return Logger.getLogger(caller.getClassName());
      }
    }
    else {
      return Logger.getLogger(caller.getClassName());
    }
  }

  /** Gets the logger for the named calling class.
   *  @param caller The calling class.
   *  @return The logger.
   */
  private static Logger getLogger (String className) {
    if (demandLogger != null) {
      // Logger.getLogger(..) is CallerSensitive, so this calls the method that underlies
      // the Logger.getLogger(..) method (at least in OpenJDK).
      try {
        Class<?> callingClass = Class.forName(className);
        return (Logger)demandLogger.invoke(null, className, null, callingClass);
      }
      catch (Throwable t) {
        LOG.log(Level.WARNING, "Unable to use demandLogger", t);
        return Logger.getLogger(className);
      }
    }
    else {
      return Logger.getLogger(className);
    }
  }


  //***************************************************************************
  //***************************************************************************
  //  Extra Related methods and Class
  //***************************************************************************
  //***************************************************************************

  /** Returns an extra that wraps an (currently-)empty map.
   *  @return An extra that wraps the an (currently-)empty map.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra extra () {
    return new Extra();
  }

  /** Returns an extra that combines all of the given extra parameters. This is used to add extra
   *  arguments into the log message that can get passed down to the log handler. Log handlers
   *  like LogStash will use them, while other will ignore them. <br>
   *  <br>
   *  This relies on the fact that (currently) the log handlers ignore extra parameters passed
   *  in, for example:
   *  <pre>
   *    Given:
   *      log(Level.INFO, "{0} {1}", "Hello", "World", "Monkey");
   *
   *    The output will be:
   *      INFO: Hello World
   *  </pre>
   *  Hence, with {@code extra(..)} we can do:
   *  <pre>
   *    Given:
   *      log(Level.INFO, "{0} {1}", "Hello", "World", extra(PLANET-&gt;"Earth"));
   *
   *    The output to the console/file will be:
   *      INFO: Hello World
   *
   *    The output to LogStash will be something like:
   *      {"level":"INFO","msg":"Hello World","PLANET":"Earth"}
   *  </pre>
   *  If there is a desire to print out the extra arguments, just include the formatting information:
   *  <pre>
   *    Given:
   *      log(Level.INFO, "{0} {1} {2}", "Hello", "World", extra(PLANET-&gt;"Earth"));
   *
   *    The output to the console/file will be:
   *      INFO: Hello World {PLANET=Earth}
   *
   *    The output to LogStash will be something like:
   *      {"level":"INFO","msg":"Hello World {PLANET=Earth}","PLANET":"Earth"}
   *  </pre>
   *  @param extraArgs The extra arguments.
   *  @return An extra with the given arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra extra (KeyValuePair<?>... extraArgs) {
    return new Extra(extraArgs);
  }

  /** Returns an extra that wraps the keys in the provided map.
   *  @param map The map.
   *  @return An extra that wraps the given map.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (Map<String,Object> map) {
    return new Extra(map);
  }

  /** Creates a set of extra arguments based on a file. This is identical to
   *  {@code asExtra("FILE",file)} combined with a default "FILENAME" setting.
   *  @param file The file.
   *  @return The extra arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (BaseFile file) {
    Extra extra = asExtra("FILE", file);
    extra.put("FILENAME", file.getName());
    return extra;
  }

  /** Creates a set of extra arguments based on a file. This is identical to
   *  {@code asExtra("FILE",file)} combined with a default "FILENAME" setting.
   *  @param file The file.
   *  @return The extra arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (File file) {
    Extra extra = asExtra("FILE", file);
    extra.put("FILENAME", file);
    return extra;
  }

  /** Creates a set of extra arguments based on a file path. This is identical to
   *  {@code asExtra("FILE",file)} combined with a default "FILENAME" setting.
   *  @param path The path.
   *  @return The extra arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (Path path) {
    Extra extra = asExtra("FILE", path);
    extra.put("FILENAME", path);
    return extra;
  }

  /** Creates a set of extra arguments based on a file path. This is identical to
   *  {@code asExtra(prefix, file.toFile())}.
   *  @param prefix A prefix for the names (not null or ""). Good choices include "IN",
   *                "OUT", or "FILE".
   *  @param path   The path.
   *  @return The extra arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (String prefix, Path path) {
    return asExtra(prefix, (path == null)? null : path.toFile());
  }

  /** Creates a set of extra arguments based on a file. The following will be included, if
   *  available:
   *  <pre>
   *    prefix+"_DURATION"            - The file duration (in seconds).
   *    prefix+"_EXISTS"              - Does the file exist?
   *    prefix+"_LAST_MODIFIED"       - The file last modified time (as a string).
   *    prefix+"_LAST_MODIFIED_J1950" - The file start time (as a J1950 seconds).
   *    prefix+"_LAST_MODIFIED_SINCE" - The number of seconds between local system time and LastModified.
   *    prefix+"_LENGTH"              - The file size (in bytes).
   *    prefix+"_NAME"                - The file name.
   *    prefix+"_SIZE"                - The file size (bytes or elements).
   *    prefix+"_TIME"                - The file start time (as a Midas time *or* ISO string).
   *    prefix+"_TIME_J1950"          - The file start time (as a J1950 seconds).
   *    prefix+"_TIME_SINCE"          - The number of seconds between local system time and TIME.
   *
   *    Note: If the object passed in is null, the file name will be reported as null.
   *  </pre>
   *  @param prefix A prefix for the names (not null or ""). Good choices include "inFile",
   *                "outFile", "tempFile", or "file".
   *  @param file   The file.
   *  @return The extra arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (String prefix, BaseFile file) {
    // Note: The keys used here are kept consistent with the keys used for a File/Path
    Extra  extra = new Extra();
    double now   = Time.current();
    Time   time;

    if (file != null) {
      extra.put(prefix+"_NAME", file.getName());
      extra.put(prefix+"_SIZE", file.getSize());

      if (file.io != null) {
        time = file.io.lastModifiedTime();
        extra.put(prefix+"_EXISTS", file.io.exists());
        extra.put(prefix+"_LENGTH", file.io.getLength());

        if (time != null) {
          extra.put(prefix+"_LAST_MODIFIED",       time);
          extra.put(prefix+"_LAST_MODIFIED_J1950", time.getSec());
          extra.put(prefix+"_LAST_MODIFIED_SINCE", now - time.getSec());
        }
      }

      if (file instanceof DataFile) {
        DataFile df = (DataFile)file;
        Time     start = df.getTimeAt(0.0, null);
        Time     end   = df.getTimeAt(df.getSize(), null);

        extra.put(prefix+"_TIME",        start);
        extra.put(prefix+"_TIME_J1950",  start.getSec());
        extra.put(prefix+"_TIME_SINCE",  now - start.getSec());
        extra.put(prefix+"_DURATION",    end.diff(start));
      }
    }
    else {
      extra.put(prefix+"_NAME", (Object)null);
    }
    return extra;
  }

  /** Creates a set of extra arguments based on a file. The following will be included, if
   *  available:
   *  <pre>
   *    prefix+"_EXISTS"              - Does the file exist?
   *    prefix+"_LAST_MODIFIED"       - The file last modified time (as a Midas time *or* ISO string).
   *    prefix+"_LAST_MODIFIED_J1950" - The file start time (as a J1950 seconds).
   *    prefix+"_LAST_MODIFIED_SINCE" - The number of seconds between local system time and LastModified.
   *    prefix+"_LENGTH"              - The file size (in bytes).
   *    prefix+"_NAME"                - The file name.
   *
   *    Note: If the object passed in is null, the file name will be reported as null.
   *  </pre>
   *  @param prefix A prefix for the names (not null or ""). Good choices include "inFile",
   *                "outFile", "tempFile", or "file".
   *  @param file   The file.
   *  @return The extra arguments.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Extra asExtra (String prefix, File file) {
    // Note: The keys used here are kept consistent with the keys used for a BaseFile/DataFile
    Extra  extra = new Extra();
    double now   = Time.current();

    extra.put(prefix+"_NAME", Objects.toString(file));

    if (file != null) {
      long t = file.lastModified();

      extra.put(prefix+"_EXISTS", file.exists());
      extra.put(prefix+"_LENGTH", file.length());

      if (t > 0) {
        Time time = new Time(Time.J1970TOJ1950 + (t * 0.001));
        extra.put(prefix+"_LAST_MODIFIED",       time);
        extra.put(prefix+"_LAST_MODIFIED_J1950", time.getSec());
        extra.put(prefix+"_LAST_MODIFIED_SINCE", now - time.getSec());
      }
    }
    return extra;
  }

  /** Creates a timer that is an extra that will provide an "extra" parameter with the name
   *  "duration" and a value that is equal to the number of seconds since the object creation.
   *  This is identical to {@code timer("DURATION")}.
   *  @return The extra arguments. Note that subsequent calls to the timer's methods may have
   *          different values.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Duration duration () {
    return duration("DURATION");
  }

  /** Creates a timer that is an extra that will provide an "extra" parameter with the given name
   *  and a value that is equal to the number of seconds since the object creation. This is intended
   *  to be used within a method to support reporting of durations. For example:
   *  <pre>
   *    Given:
   *      Extra duration = durationAsExtra("duration");
   *
   *      callRestService(); // takes 0.005 seconds
   *
   *      log(Level.INFO, "{0} {1}", "Hello", "World", duration);
   *
   *    The output to the console/file will be:
   *      INFO: Hello World
   *
   *    The output to LogStash will be something like:
   *      {"level":"INFO","msg":"Hello World","duration":0.005}
   *  </pre>
   *  @param name The name of the timer.
   *  @return The extra arguments. Note that subsequent calls to the timer's methods may return
   *          different values.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static Duration duration (String name) {
    return new Duration(name);
  }

  /** Creates an "extra" for a single key=val pair. The following are essentially the same, except
   *  for this one initializing slightly faster, and being read-only (whereas {@link #extra}) would
   *  create a modifiable one:
   *  <pre>
   *    Extra extra = extra(key-&gt;"val");
   *
   *    Extra extra = singletonExtra("key", "val");
   *  </pre>
   *  @param key The key.
   *  @param val The value.
   *  @return The extra arguments <b>(read only)</b>.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  @SuppressWarnings("unchecked")
  public static Extra singletonExtra (String key, Object val) {
    return new Extra(singletonMap(key, val));
  }

  // Additional Message formatting API
  /** Allows easy building of a log message
   *
   *  @param msg    base message
   *  @param params paramters to be incorporated into message
   *  @return built message
   */
  @InternalUseOnly
  public static String buildMessage (String msg, Object... params) {
    for (Object param : Arrays.asList(params)) {
      if (msg.contains("{}")) {
        msg = msg.replaceFirst("\\{\\}", Matcher.quoteReplacement(parseParam(param)));
      }
      else msg = msg.concat(" " + parseParam(param));
    }
    return msg;
  }

  private static String parseParam (Object param) {
    if (param == null) return "null";
    if (param instanceof List) {
      StringBuilder list = new StringBuilder();
      ((List<?>) param).forEach(o -> {
        if (!list.toString().isEmpty()) {
          list.append(", ");
        } else {
          list.append("[");
        }
        list.append(parseParam(o));
      });
      list.append("]");
      return list.toString();
    }
    return param.toString();
  }

  /** Indents a msg by 1 tab character.<br>
   *  <br>
   *  Every line of a multi-line message will be indented. Lines will be determined by {@link System#lineSeparator()}
   *
   *  @param msg the message
   *  @return the indented message
   */
  @InternalUseOnly
  public static String indentBlock (String msg) {
    return indentBlock(msg, 1);
  }

  /** Indents a msg by the specified number of tab characters.<br>
   *  <br>
   *  Every line of a multi-line message will be indented. Lines will be determined by {@link System#lineSeparator()}
   *
   *  @param msg the message
   *  @param indentLevel the level to indent
   *  @return the indented message
   */
  @InternalUseOnly
  public static String indentBlock (String msg, int indentLevel) {
    StringBuilder out = new StringBuilder();
    String[] lines = msg.split(System.lineSeparator());
    for (int i = 0; i < lines.length; i++) {
      out.append(System.lineSeparator());
      for (int j = 0; j < indentLevel; j++) {
        out.append("\t");
      }
      out.append(lines[i]);
    }
    return out.toString();
  }

  /** "Extra" parameters to accompany the log message. This class does some lazy-loading
   *  in an attempt to minimize the overhead of using it in situations where it is not
   *  logged (which is important for FINE/FINER/FINEST/etc.). <br>
   *  <br>
   *  The "add" and "default" parameters are ones that will roll-up with other {@link Extra}
   *  parameters present. The "add" ones will provide a default that is incremental; for
   *  example:
   *  <pre>
   *    This code:
   *      extra1.putAdd("LUCKY_NUMBERS", 1, 2)
   *      extra2.putAdd("LUCKY_NUMBERS", 2, 3)
   *
   *    Is identical to having:
   *      extra.putDef("LUCKY_NUMBERS", asList(1,2,2,3))
   *  </pre>
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static class Extra {
    private final KeyValuePair<?>[] extraArgs;
    private       MapKBT            extraMap;

    /** Creates a new instance. */
    Extra () {
      this(new TreeMap<>());
    }

    /** Creates a new instance.
     *  @param map the map to wrap.
     */
    Extra (Map<String,Object> map) {
      this.extraArgs = null;
      this.extraMap  = wrapMap(map);
    }

    /** Creates a new instance.
     *  @param extraArgs The args.
     */
    Extra (KeyValuePair<?>... extraArgs) {
      this.extraArgs = extraArgs;
      this.extraMap  = null; // <-- Lazy-load this
    }

    /** Gets the extra parameters.
     *  @return The extra parameters. This can be empty, but will never be null.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    @SuppressWarnings("unchecked")
    public MapKBT getExtra () {
      if (extraMap == null) {
        if ((extraArgs == null) || (extraArgs.length == 0)) {
          extraMap = newMap();
        }
        else {
          extraMap = wrapMap(KeyValuePair.toMap(extraArgs));
        }
      }
      return extraMap;
    }

    /** Optional Operation: Adds additional parameters.
     *  @param map The values to add.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void putAll (Map<String,? extends Object> map) {
      // The put(single,v) method may have overrides in subclasses...
      getExtra().putAll(map);
    }

    /** Optional Operation: Adds additional parameters.
     *  @param extra The values to add.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void putAll (Extra extra) {
      putAll(extra.getExtra());
    }

    /** Optional Operation: Adds an extra parameter if it is non-null.
     *  See {@link #put(String,Collection)} for details.
     *  @param key  The parameter key which can end with "(S)" indicating that the plural and
     *              singular forms should both be set, with consideration for the number of entries.
     *  @param val  The parameter value.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void add (String key, Object val) {
      put(key, val, true);
    }

    /** Optional Operation: Adds an extra parameter if it is non-null, with special support for a
     *  possible plural. See {@link #put(String,Collection)} for details.
     *  @param key The parameter key which can end with "(S)" indicating that the plural and
     *             singular forms should both be set, with consideration for the number of entries.
     *  @param vals The parameter values.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void add (String key, Collection<?> vals) {
      put(key, vals, true);
    }

    /** Optional Operation: Adds an extra parameter if it is non-null, with special support for a
     *  possible plural. See {@link #put(String,Collection)} for details.
     *  @param key  The parameter key which can end with "(S)" indicating that the plural and
     *              singular forms should both be set, with consideration for the number of entries.
     *  @param vals The parameter values.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void add (String key, Object... vals) {
      put(key, asList(vals), true);
    }

    /** Optional Operation: Adds an extra parameter. See {@link #put(String,Collection)} for details.
     *  @param key  The parameter key which can end with "(S)" indicating that the plural and
     *              singular forms should both be set, with consideration for the number of entries.
     *  @param val  The parameter value.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void put (String key, Object val) {
      put(key, val, false);
    }

    /** Optional Operation: Adds an extra parameter. See {@link #put(String,Collection)} for details.
     *  @param key  The parameter key which can end with "(S)" indicating that the plural and
     *              singular forms should both be set, with consideration for the number of entries.
     *  @param vals The parameter values.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void put (String key, Object... vals) {
      put(key, asList(vals), false);
    }

    /** Optional Operation: Adds an extra parameter with special support for a possible plural.
     *  <pre>
     *                              | CURRENT FOO | CURRENT FOOS |   NEW FOO   |   NEW FOOS   |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   put("FOO(S)", null)        | any         | any          | null        | null         |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   put("FOO(S)", obj)         | any         | any          | obj         | {obj}        |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   put("FOO(S)", {})          | any         | any          | null        | {}           |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   put("FOO(S)", {a})         | any         | any          | a           | {a}          |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   put("FOO(S)", {a,b,...})   | any         | any          | null        | {a,b,...}    |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   add("FOO(S)", any)         | null        | null         | Same as put("FOO(S)",any)  |
     *   ---------------------------+-------------+--------------+----------------------------+
     *   add("FOO(S)", null) or     | any         | any          | No changes                 |
     *   add("FOO(S)", {})          |             |              |                            |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   add("FOO(S)", obj)         | a           | null or {a}  | null        | {a,obj}      |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   add("FOO(S)", obj)         | null        | {a,b}        | null        | {a,b,obj}    |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   add("FOO(S)", {q,...})     | a           | null or {a}  | null        | {a,q,...}    |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *   add("FOO(S)", {q,...})     | null        | {a,b}        | null        | {a,b,q,...}  |
     *   ---------------------------+-------------+--------------+-------------+--------------+
     *  </pre>
     *  @param key The parameter key which can end with "(S)" indicating that the plural and
     *             singular forms should both be set, with consideration for the number of entries.
     *  @param val The parameter value.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final void put (String key, Collection<?> val) {
      put(key, val, false);
    }

    /** Optional Operation: Adds an extra parameter with special support for a possible plural.
     *  @param key The parameter key which can end with "(S)" indicating that the plural and
     *             singular forms should both be set, with consideration for the number of entries.
     *  @param add Is this an add?
     *  @param val The parameter value.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    protected void put (String key, Object val, boolean add) {
      if (add && (val == null)) return;

      if (key.endsWith("(S)")) {
        String single   = key.substring(0, key.length()-3);
        String multiple = single+"S";
        MapKBT e        = getExtra();
        Set<?> s;

        if (!add) {
          e.put(multiple, singleton(val));
          e.put(single,   val);
          return;
        }

        if (e.containsKey(multiple)) {
          s = merge(e.getCollection(multiple), val);
        }
        else if (e.containsKey(single)) {
          s = merge(singleton(val), e.get(single));
        }
        else {
          e.put(multiple, singleton(val));
          e.put(single,   val);
          return;
        }

        if (s.size() == 1) {
          e.put(multiple, singleton(val));
          e.put(single,   val);
        }
        else {
          e.put(multiple, s);
          e.put(single,   null);
        }
      }
      else {
        getExtra().put(key, val);
      }
    }

    /** Optional Operation: Adds an extra parameter with special support for a possible plural.
     *  @param key The parameter key which can end with "(S)" indicating that the plural and
     *             singular forms should both be set, with consideration for the number of entries.
     *  @param add Is this an add?
     *  @param c   The parameter value.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    protected void put (String key, Collection<?> c, boolean add) {
      if (key.endsWith("(S)")) {
        String single   = key.substring(0, key.length()-3);
        String multiple = single+"S";
        MapKBT e        = getExtra();

        if (add) {
          if ((c == null) || c.isEmpty()) {
            return;
          }

          if (e.containsKey(multiple)) {
            c = merge(e.getCollection(multiple), c);
          }
          else if (e.containsKey(single)) {
            c = merge(c, e.get(single));
          }
        }

        if ((c != null) && (c.size() == 1)) {
          e.put(multiple, c);
          e.put(single,   first(c));
        }
        else {
          e.put(multiple, c);
          e.put(single,   null);
        }
      }
      else if (add && (c == null)) {
        // do nothing
      }
      else {
        getExtra().put(key, c);
      }
    }

    /** Merges two collections into a set.
     *  @param a The first collection.
     *  @param b The second collection.
     *  @return The merged set.
     */
    @SuppressWarnings("unchecked")
    private static Set<?> merge (Collection<?> a, Collection<?> b) {
      if (a.isEmpty()) return setOf(b);
      if (b.isEmpty()) return setOf(a);

      if (a instanceof LinkedHashSet2) {
        a.addAll((Collection)b);
        return (Set<?>)a;
      }
      else {
        LinkedHashSet2 set = new LinkedHashSet2(a);
        set.addAll(b);
        return set;
      }
    }

    /** Merges a collection and a value into a set.
     *  @param a The first collection.
     *  @param b The second set.
     *  @return The merged set.
     */
    @SuppressWarnings("unchecked")
    private static Set<?> merge (Collection<?> a, Object b) {
      if (a instanceof LinkedHashSet2) {
        LinkedHashSet2 s = (LinkedHashSet2)a;
        s.add(b);
        return s;
      }
      if ((a instanceof TreeSet2) && (first(a).getClass() == b.getClass())) {
        TreeSet2 s = (TreeSet2)a;
        s.add((Comparable)b);
        return s;
      }
      return merge(a, singleton(b));
    }

    /** Merges a collection and a value into a set.
     *  @param a The first collection.
     *  @param b The second set.
     *  @return The merged set.
     */
    @SuppressWarnings({"unchecked","rawtypes"})
    private static Set<?> setOf (Collection<?> c) {
      if (c instanceof Set) return (Set<?>)c;
      if (c.isEmpty()     ) return emptySortedSet();

      return new LinkedHashSet2(c);
    }

    /** Removes an extra parameter.
     *  @param key The parameter key.
     *  @return The value removed (may be null) or null if not found.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final Object remove (String key) {
      return getExtra().remove(key);
    }

    /** Gets an extra parameter.
     *  @param key The parameter key.
     *  @return The value replaced (may be null) or null if not found.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public final Object get (String key) {
      return getExtra().get(key);
    }

    /** The extra parameters formatted as a map.
     *  This is identical to {@code getExtra().toString()}.
     *  @return The extra parameters formatted as a map.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    @Override
    public final String toString () {
      return getExtra().toString();
    }
  }

  /** A timer that will return an extra arg with the given name and a value that is equal to the
   *  number of seconds between object creation and the most recent call to {@link #getExtra()}.
   */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static class Duration extends Extra {
    private final String name;
    private final long   start;

    /** Creates a new instance.
     *  @param name The name of the timer.
     */
    Duration (String name) {
      this.name  = name;
      this.start = System.currentTimeMillis();
    }

    /** Gets "Extra" parameters to accompany the log message as a Map */
    @ProvisionalUseOnly("Not fully tested and API could change")
    @Override
    @SuppressWarnings("unchecked")
    public MapKBT getExtra () {
      MapKBT extraMap = super.getExtra();
      Number val      = round(getDurationMS() * 0.001, 3);
      extraMap.put(name, val);
      return extraMap;
    }

    /** Gets the duration in ms.
     *  @return The duration in ms.
     */
    public long getDurationMS () {
      return System.currentTimeMillis()-start;
    }
  }

  //***************************************************************************
  //***************************************************************************
  //  EnhancedLogInfo Interface
  //***************************************************************************
  //***************************************************************************

  /** The additional details appearing in a {@link EnhancedLogRecord} */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static interface EnhancedLogInfo {


    /** Gets the timestamp of the log record.
     *  @return The timestamp (not null)
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public Instant getInstant();

    /** Gets the source line number
     *  @return The source line number (null if n/a)
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public default Integer getSourceLineNumber () {
      return null;
    }

    /** Get the source file name
     *  @return The source file name (null if n/a)
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public default String getSourceFileName () {
      return null;
    }
  }

  //***************************************************************************
  //***************************************************************************
  //  EnhancedLogRecord Class
  //***************************************************************************
  //***************************************************************************

  /** A {@link LogRecord} with additional details. */
  @ProvisionalUseOnly("Not fully tested and API could change")
  public static class EnhancedLogRecord extends LogRecord implements EnhancedLogInfo {
    private static final long serialVersionUID = 0x45646a125482b456L;

    /** source line number */
    private Integer sourceLineNumber;
    /** source file name */
    private String  sourceFileName;
    /** instant in time */
    private Instant instant;

    /** Creates a new instance.
     *  @param level The logging level.
     *  @param msg   The message to log.
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public EnhancedLogRecord (Level level, String msg) {
      super(level, msg);
      setInstant(null);
    }

    /** Sets the source line number.
     *  @param val The source line number (null or -N if n/a).
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public void setSourceLineNumber (Integer val) {
      sourceLineNumber = ((val == null) || (val < 0))? null : val;
    }

    /** Gets the source line number.
     *  @return The source line number (null if n/a).
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public Integer getSourceLineNumber () {
      return sourceLineNumber;
    }

    /** Sets the source file name.
     *  @param val The source file name (null if n/a).
     */
    @ProvisionalUseOnly("Not fully tested and API could change")
    public void setSourceFileName (String val) {
      sourceFileName = val;
    }

    @Override
    public String getSourceFileName () {
      return sourceFileName;
    }

    @Override
    public Instant getInstant () {
      return instant;
    }

    @Override
    public final long getMillis() {
      return instant.toEpochMilli();
    }

    @SuppressWarnings("deprecation")
    @Override public final void setMillis (long ms) {
      setInstant(ofEpochMilli(ms));
    }

    // TODO - remove this entire method once Java 8 is no longer supported
    @SuppressWarnings("deprecation")
    public void setInstant (Instant ts) {
      this.instant = (ts!=null) ? ts : ofEpochSecond(0, currentTimeNano());
      super.setMillis(instant.toEpochMilli());
    }
  }

  //***************************************************************************
  //***************************************************************************
  //  TreeSet2 Class
  //***************************************************************************
  //***************************************************************************

  /** A set marked with our ownership. */
  static class TreeSet2 extends TreeSet<Comparable<?>> {
    private static final long serialVersionUID = 0x1b34689189e65487L;

    /** Creates a new instance with the given entries.
     *  @param c The entries.
     */
    TreeSet2 (Collection<Comparable<?>> c) {
      super(c);
    }
  }

  //***************************************************************************
  //***************************************************************************
  //  LinkedHashSet2 Class
  //***************************************************************************
  //***************************************************************************

  /** A set marked with our ownership. */
  static class LinkedHashSet2 extends LinkedHashSet<Object> {
    private static final long serialVersionUID = 0x123c68918f765487L;

    /** Creates a new instance with the given entries.
     *  @param c The entries.
     */
    LinkedHashSet2 (Collection<?> c) {
      super(c);
    }
  }

}
