package nxm.sys.lib;

import static java.lang.System.currentTimeMillis;
import static java.lang.System.nanoTime;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static java.util.Collections.unmodifiableSortedSet;
import static java.util.Objects.requireNonNull;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.SpecialLoggingRequired;

/** Defines useful utility functions useful throughout NeXtMidas and NeXtMidas applications.

    @author Jeff Schoen, J. Czechowski IV, Others
 */
@SpecialLoggingRequired // <-- This annotation must be added to nxm.sys.lib.Util
public class Util {
  private Util() { } // prevent instantiation since all methods are static.

  // We need to use the traditional logger methods from this class since the methods in this class
  // have special initialization and calling restrictions due to their use by LogUtilities.
  private static final Logger LOG = Logger.getLogger(Util.class.getName());
  /** nano-second factor */
  public static final int NS_FACTOR = 1000000;
  private static final AtomicLong nsOffset;
  static {
    // To get a precise offset value, we loop through many times, The first few
    // we throw away as this is just making sure the JIT compiler has optimized
    // the code before we try for the readings we want.
    //
    // We then average a number of readings since it can be tricky to get the
    // transition point right. The number of loops here are just what emperically
    // seemed to work back in Mar 2020.
    int    skip    = 5;
    int    loops   = 20;
    long[] offsets = new long[loops+skip];

    for (int i = 0; i < offsets.length; i++) {
      offsets[i] = getNsOffset();
    }
    BigInteger sum = BigInteger.ZERO;
    for (int i = skip; i< offsets.length; i++) {
      sum = sum.add(BigInteger.valueOf(offsets[i]));
    }
    nsOffset = new AtomicLong(sum.divide(BigInteger.valueOf(loops)).longValueExact());
  }

  /** Gets one reading of the offset. This should be called by the static
   *  constructor. Note that the loop here has a lot of throw-away iterations
   *  for the same reason the static constructor does. The ultimate goal is to
   *  see what the ns time is closets to a 1 ms time tic and then compute the
   *  offset to use in converting between nanoTime() and the current time.
   *  @return The offset in ns
   */
  private static long getNsOffset () {
    long lastMS = currentTimeMillis();
    for (int i = 0; ; i++) {
      long ms = currentTimeMillis();
      long ns = nanoTime();
      if ((ms == lastMS+1) && (i >= 100000)) { // is that number right, should it be NS_FACTOR
        return (ms * NS_FACTOR) - ns;
      }
      lastMS = ms;
    }
   }

  /** Gets the current time with nanosecond precision. Note that this ins't
   *  guaranteed to be accurate to 1 ns, only that it makes a best effort that
   *  is not worse than 1 ms. <br>
   *  <br>
   *  The intent here is to get a value that is substantially more precise than
   *
   *  @return The current time in nano seconds
   */
  public static long currentTimeNano() {
    if (nsOffset == null) {
      // special case where this method is called before init is done. In such
      // a case this will just return the current time to the nearest 1 ms.
      return currentTimeMillis() * NS_FACTOR;
    }
    // If there are multiple threads hitting this at once, there is the
    // possibility that an update to nsOffset will cause times to be
    // reported that aren't monotonically increasing. If there is an
    // intervening modification, this will repeat the computation. With
    // this current implementation, the only way calls could return
    // values out-of-order are if they are on different threads and there
    // is a delay at the point where the return is called.
    while (true) {
      long nsOff = nsOffset.get();
      long ns    = nanoTime() + nsOff;
      long ms    = currentTimeMillis();
      long ns2ms = ns/NS_FACTOR;

      if (ns2ms == ms) {
        if (nsOffset.get() == nsOff) {
          return ns;
        }
      }
      else if (ns2ms > ms) {
        long error = ns - ms*NS_FACTOR;
        long nano = ms*NS_FACTOR + (NS_FACTOR-1);
        if (nsOffset.compareAndSet(nsOff, nsOff - error/2)) {
          return nano;
        }
      }
      else {
        long error = ms*NS_FACTOR -ns;
        long nano  = ms*NS_FACTOR;
        if (nsOffset.compareAndSet(nsOff, nsOff - error/2)) {
          return nano;
        }
      }
    }
  }

   /** <b>INTERNAL USE ONLY:</b> Creates a new array that contains the contents of the given arrays.
      <br>
      <br>
      NOTE: No more than one of <code>clazz</code> and <code>dest</code> may be <code>null</code>.

      @param clazz  The component type for the new array.
      @param dest   The destination array.
      @param first  The array containing the elements to add to the head of the destination array.
      @param second The array containing the elements to add to the tail of the destination array.

      @return <code>dest</code>, or the new array created if <code>dest</code> is <code>null</code>.

      @throws IllegalArgumentException  If any of the objects passed in is not an array.
      @throws IndexOutOfBoundsException If <code>(dest.length < (first.length + second.length))
                                        </code>.
      @throws ArrayStoreException       If an element either <code>first</code> or
                                        <code>second</code> can not be stored in <code>dest</code>
                                        due to type differences (i.e. elements in either
                                        <code>first</code> or <code>second</code> are not
                                        instances of the elements in <code>dest</code>).
      @throws NullPointerException      If either <code>first</code> or <code>second</code> are
                                        <code>null</code>.

      @since NeXtMidas 2.1.0
   */
  @SuppressWarnings("all")
  private static Object doCombineArrays(Class<?> clazz, Object dest, Object first, Object second) {
    final int firstLength  = Array.getLength(first);
    final int secondLength = Array.getLength(second);

    if(dest == null) {
      dest = Array.newInstance(clazz, (firstLength + secondLength));
    }

    System.arraycopy(first,  0, dest, 0,           firstLength);
    System.arraycopy(second, 0, dest, firstLength, secondLength);

    return dest;
  }

  /** Creates a new array that contains the contents of the given arrays. <br>
      <br>
      If <code>dest</code> is <code>null</code> a new array will be created according to the
      following rules:
      <ul>
        <li>If <code>first</code> is a primitive array, the new array will be a primitive array with
            the same component type.</li>
        <li>If <code>first</code> and <code>second</code> have the same component type, the new
            array will also have that same component type.</li>
        <li>If neither of the above match the new array will be an {@link Object} array
            (see {@link #combineArraysAs(Class,Object,Object)} to specify the new array type).</li>
      </ul>

      @param dest   The destination array.
      @param first  The array containing the elements to add to the head of the destination array.
      @param second The array containing the elements to add to the tail of the destination array.

      @return <code>dest</code>, or the new array created if <code>dest</code> is <code>null</code>.

      @throws IllegalArgumentException  If any of the objects passed in is not an array.
      @throws IndexOutOfBoundsException If <code>(dest.length &lt; (first.length + second.length))
                                        </code>.
      @throws ArrayStoreException       If an element either <code>first</code> or
                                        <code>second</code> can not be stored in <code>dest</code>
                                        due to type differences (i.e. elements in either
                                        <code>first</code> or <code>second</code> are not
                                        instances of the elements in <code>dest</code>).
      @throws NullPointerException      If either <code>first</code> or <code>second</code> are
                                        <code>null</code>.

      @since NeXtMidas 2.1.0
   */
  public static Object combineArrays(Object dest, Object first, Object second) {
    Class<?> clazz = null;

    if(dest == null) {
      final Class<?> firstClass  = first.getClass().getComponentType();
      final Class<?> secondClass = second.getClass().getComponentType();


      if(firstClass.equals(secondClass) || (firstClass.isPrimitive())) {
        clazz = firstClass;
      } else {
        clazz = Object.class;
      }
    }

    return doCombineArrays(clazz, dest, first, second);
  }

  /** Creates a new array that contains the contents of the given arrays. <br>
      <br>
      This is identical to <code>combineArrays(null,first,second)</code>

      @param first  The array containing the elements to add to the head of the destination array.
      @param second The array containing the elements to add to the tail of the destination array.

      @return The new array created.

      @throws IllegalArgumentException  If any of the objects passed in is not an array.
      @throws NullPointerException      If any of the parameters are <code>null</code>.

      @since NeXtMidas 2.1.0
   */
  public static Object combineArrays(Object first, Object second) {
    return combineArrays(null,first,second);
  }

  /** Creates a new array that contains the contents of the given arrays.

      @param clazz  The component type for the new array.
      @param first  The array containing the elements to add to the head of the destination array.
      @param second The array containing the elements to add to the tail of the destination array.

      @return <code>dest</code>

      @throws IllegalArgumentException  If <code>first</code> or <code>second</code> are
                                        <code>null</code> in is not an array.
      @throws ArrayStoreException       If <code>clazz</code> is not a superclass of all the
                                        elements in <code>first</code> and <code>second</code>.
      @throws NullPointerException      If any of the parameters are <code>null</code>.

      @since NeXtMidas 2.1.0
   */
  public static Object combineArraysAs(Class<?> clazz, Object first, Object second) {
    return doCombineArrays(clazz, null, first, second);
  }

  /** Reverse (re-order) the elements in an array of homogeneous Objects of type T.
      @param array Object array to be modified. null input does nothing.
      @param startIndex index (inclusive) into array to begin reversing the elements with the back.
      @param <T> the type of the objects in the array
      @return modified input array is returned
      @since NeXtMidas 2.9.0
   */
  public static <T> T[] reverseArray (T[] array, int startIndex) {
    if (array==null) { return array; }
    int right = array.length-1;
    T tmp;
    for (int left=startIndex; left < right; left++, right--) {
      tmp = array[left];
      array[left] =  array[right];
      array[right] = tmp;
    }
    return array;
  }

  /** Removes a set of elements from a List.
      @param list  The list to remove the elements from.
      @param start The first element to remove. (If start is &lt; 0 or &gt;= list.size() this method
                   does nothing and returns false).
      @param count The number of elements to remove or -1 to remove all elements after start.
                   (If count is 0 this method does nothing and returns false).
      @return true if any elements were removed, false if no elements were removed.
   */
  public static boolean removeListElements (List<?> list, int start, int count) {
    boolean anyRemoved = false;

    if ((start >= 0) && (start < list.size()) && (count != 0) && !list.isEmpty()) {
      if (count < 0) {
        if (start == 0) { list.clear();                                   }
        else            { while (start < list.size()) list.remove(start); }

      } else {
        for (int i = 0; (i < count) && (start < list.size()); i++) {
          list.remove(start);
        }
      }
      anyRemoved = true;
    }

    return anyRemoved;
  }

  /** Returns the enum constant of the specified enum type with the specified name. Unlike
      Enum's valueOf method, this method tries a case-sensitive check first and then falls
      back on a case-sensitive check if that fails. This is especially useful when users
      specify enumerated constants from the macro language where case-insensitivity is
      the norm.
      @param enumType The <code>Class</code> object of the Enum type from which to return a constant
      @param name     The name of the constant to return
      @param <E>      The enum type
      @return the enum constant of the specified enum type with the specified name
      @throws IllegalArgumentException if the specified type has no constant
               with the specified name, or <code>enumType</code> does not represent
               an enum type
      @throws NullPointerException if <code>enumType</code> or <code>name</code> is null
   */
  public static <E extends Enum<E>> E valueOf (Class<E> enumType, String name) {
    if (enumType == null) throw new NullPointerException("Expected an enumerated type, given null");
    if (name == null) throw new NullPointerException("Cannot convert NULL to instance of " + enumType);

    E[] enums = enumType.getEnumConstants();
    if (enums == null) throw new IllegalArgumentException("Expected an enumerated type, given " +enumType);

    // try case-sensitive
    for (E e : enums) { if (e.toString().equals(name)) return e; }

    // try again case-insensitive
    for (E e : enums) { if (e.toString().equalsIgnoreCase(name)) return e; }

    throw new IllegalArgumentException("Selection '"+name+"' not found in "+ StringUtil.join(enums));
  }

  /** Compares two objects. If both strings can be converted to a number (using
      {@link Convert#o2n(Object)}) then a numeric comparison is done, otherwise they are converted
      to strings and String comparison is done. If both values are equal (using "==") then true is
      immediately returned.
      @param val1 The first  value (may be null).
      @param val2 The second value (may be null).
      @return true if equal, false otherwise.
   */
  public static boolean equals(Object val1, Object val2) {
    boolean equal = true;

    if (val1 == val2) {
      // Same
    } else if ((val1 == null) || (val2 == null)) {
      equal = false;
    } else {
      Number num1 = Convert.o2n(val1);
      Number num2 = Convert.o2n(val2);

      if ((num1 != null) && (num2 != null)) {
        equal = (num1.doubleValue() == num2.doubleValue());
      } else {
        equal = val1.toString().equals(val2.toString());
      }
    }

    return equal;
  }

  /** Compares two objects. If both strings can be converted to a number (using
      {@link Convert#o2n(Object)}) then a numeric comparison is done, otherwise they are converted
      to strings and String comparison is done. If both values are equal (using "==") then 0 is
      immediately returned. <br>
      <br>
      (Note: This comparison is for sort order and there is no guarantee that values found to be
             equal with {@link #equals(Object,Object)} will have a comparison of 0. For example
             +0.0 and -0.0 are treated the same by {@link #equals(Object,Object)} but are different
             here.)
      @param val1 The first  value (may not be null).
      @param val2 The second value (may not be null).
      @return 0 if both are comparably equal, &lt;0 if the first value is less than the second; and
              &gt;0 if the first value is greater than the second.
   */
  public static int compare(Object val1, Object val2) {
    int comp;

    if (val1 == val2) {
      comp = 0;
    } else {
      Number num1 = Convert.o2n(val1);
      Number num2 = Convert.o2n(val2);

      if ((num1 != null) && (num2 != null)) {
        comp = Double.compare(num1.doubleValue(), num2.doubleValue());
      } else {
        comp = val1.toString().compareTo(val2.toString());
      }
    }

    return comp;
  }

  /** Gets the short name of an object's class. This omits the package of the class and
      the enclosing class for nested classes (but not anonymous inner classes). Examples:
      <pre>
        getClassName(new String())   -&gt; "String"
        getClassName(new Table())    -&gt; "Table"
        getClassName(null)           -&gt; null
      </pre>
      This is identical to <code>getClassName(obj.getClass())</code>, except that it returns <code>null</code>
      if the object passed in is <code>null</code>.
      @param obj The object to get the class name for.
      @return The short form of the class name for the object.
   */
  public static String getClassName (Object obj) {
    return (obj == null)? null : getClassName(obj.getClass());
  }

  /** Gets the short name for a given class. This omits the package of the class and
      the enclosing class for nested classes (but not anonymous inner classes). Examples:
      <pre>
        getClassName(String.class)   -&gt; "String"
        getClassName(Table.class)    -&gt; "Table"
      </pre>
      @param clazz The class to get the short name for (can not be null).
      @return The short form of the class name for the object.
      @since NeXtMidas 2.7.0
   */
  public static String getClassName (Class<?> clazz) {
    String name;

    // Possible cases:
    //   (1) Proxy instances (usually in the form Proxy$<num>)                <-- do not change
    //   (2) Nested classes in the form <pkg>.<EnclosingClass>$<ClassName>    <-- change to <ClassName>
    //   (3) Anonymous inner classes in the form <pkg>.<EnclosingClass>$<Num> <-- change to <ClassName>$<Num>
    //   (4) Regular classes in the form <pkg>.<ClassName> or <ClassName>     <-- change to <ClassName>
    name = clazz.getName();

    if (!Proxy.isProxyClass(clazz)) {
      int i;
      if ((i = name.lastIndexOf('.')) > 0) {
        name = name.substring(i+1);
      }

      if ((i = name.indexOf('$')) > 0) {
        String tmp = name.substring(i+1);
        if (!StringUtil.isInteger(tmp)) {
          name = tmp;
        }
      }
    }
    return name;
  }


  /** Gets a constructor using reflection that can be invoked later. This is useful for cases where
      some versions do not have the constructor or where the class is only available at run time.
      Some examples:
      <pre>
        getConstructor("java.lang.StringBuffer()",                     false);
        getConstructor("java.lang.StringBuffer(java.lang.String)",     false);
      </pre>
      @param signature The constructor signature.
      @param except    Throw exception if the method is not found?
      @return The method or null if not found.
   */
  public static Constructor<?> getConstructor (String signature, boolean except) {
    Constructor<?> constructor;

    try {
      int      open       = signature.indexOf('(');
      int      close      = signature.indexOf(')');
      String   className  = signature.substring(0,open).trim();
      String   typeNames  = signature.substring(open+1,close).trim();
      Class<?> clazz      = Class.forName(className);
      Class<?>[] types    = getTypes(typeNames);

      constructor = clazz.getConstructor(types);

    } catch (Exception e) {
      if (except) throw new MidasException("Unable to load method "+signature, e);
      constructor = null;
    }
    return constructor;
  }

  /** Gets a method using reflection that can be invoked later. This is useful for cases where
      some versions do not have the method or where the class is only available at run time.
      Some examples:
      <pre>
        getMethod("java.lang.StringBuffer.toString()",                   false);
        getMethod("java.lang.StringBuffer.append(java.lang.Object)",     false);
        getMethod("java.lang.StringBuffer.append(char[])",               false);
        getMethod("java.lang.StringBuffer.getChars(int,int,char[],int)", false);
      </pre>
      @param signature The method signature.
      @param except    Throw exception if the method is not found?
      @return The method or null if not found.
   */
  public static Method getMethod (String signature, boolean except) {
    Method method;

    try {
      int      open       = signature.indexOf('(');
      int      close      = signature.indexOf(')');
      int      dot        = signature.lastIndexOf('.', open);
      String   className  = signature.substring(0,dot).trim();
      String   methodName = signature.substring(dot+1,open).trim();
      String   typeNames  = signature.substring(open+1,close).trim();
      Class<?> clazz      = Class.forName(className);
      Class<?>[] types    = getTypes(typeNames);

      method = clazz.getMethod(methodName, types);

    } catch (Exception e) {
      if (except) throw new MidasException("Unable to load method "+signature, e);
      method = null;
    }
    return method;
  }

  /** Gets a field using reflection that can be accessed later. This is useful for cases where
      some versions do not have the field or where the class is only available at run time.
      Some examples:
      <pre>
        getField("java.lang.Integer.MAX_VALUE", false);
      </pre>
      @param signature The field signature.
      @param except    Throw exception if the method is not found?
      @return The method or null if not found.
   */
  public static Field getField (String signature, boolean except) {
    Field field;

    try {
      int      dot        = signature.lastIndexOf('.');
      String   className  = signature.substring(0,dot).trim();
      String   fieldName  = signature.substring(dot+1).trim();
      Class<?> clazz      = Class.forName(className);

      field = clazz.getField(fieldName);

    } catch (Exception e) {
      if (except) throw new MidasException("Unable to load field "+signature, e);
      field = null;
    }
    return field;
  }

  /** Used only by getMethod(..), handles special translation for primitive types and arrays.
      @param typeNames String with type names
      @return array with Class types
      @throws ClassNotFoundException if match for class is not found
    */
  private static Class<?>[] getTypes (String typeNames) throws ClassNotFoundException {
    String[]   args  = (typeNames.length() == 0)? new String[0] : typeNames.split("\\s*[,]\\s*");
    Class<?>[] types = new Class<?>[args.length];

    for (int i = 0; i < types.length; i++) {
      types[i] = getClass(args[i]);
    }
    return types;
  }

  /** handles special translation for primitive types and arrays.
      @param name The name of the class
      @return the Class mathing the name
      @throws ClassNotFoundException exception if not match for class is found
      @since NeXtMidas 2.9.0 (made public)
   */
  public static Class<?> getClass (String name) throws ClassNotFoundException {
    int     arrayCount = 0;
    char    symbol     = 'L';
    Class<?> clazz      = null;

    while (name.endsWith("[]")) {
      arrayCount++;
      name = name.substring(0,name.length()-2).trim();
    }
    if (name.equals("byte"   )) { clazz = Byte.TYPE;       symbol = 'B'; }
    if (name.equals("short"  )) { clazz = Short.TYPE;      symbol = 'S'; }
    if (name.equals("int"    )) { clazz = Integer.TYPE;    symbol = 'I'; }
    if (name.equals("long"   )) { clazz = Long.TYPE;       symbol = 'J'; }
    if (name.equals("float"  )) { clazz = Float.TYPE;      symbol = 'F'; }
    if (name.equals("double" )) { clazz = Double.TYPE;     symbol = 'D'; }
    if (name.equals("boolean")) { clazz = Boolean.TYPE;    symbol = 'Z'; }
    if (name.equals("char"   )) { clazz = Character.TYPE;  symbol = 'C'; }

    if (arrayCount > 0) {
      String arraySpec = StringUtil.padRight("",arrayCount,'[');
      if (symbol == 'L') {
        clazz = Class.forName(arraySpec + "L" + name + ";");
      } else {
        clazz = Class.forName(arraySpec + symbol);
      }
    }

    return (clazz != null)? clazz : Class.forName(name);
  }



  //////////////////////////////////////////////////////////////////////////////
  // Functions to support the various OS and Arch options
  //////////////////////////////////////////////////////////////////////////////
  private static final Map<String,List<String>> OS_LIST;
  private static final Map<String,List<String>> ARCH_LIST;

  static {
    // Notes:
    //   1. It is not accidental that the initial versions of the following
    //      lists largely map up with the JOGL bindings with additions to
    //      match NeXtMidas standard+OSGI usage.
    //   2. The first entry will be the one used in file names (though
    //      converted to lower-case with '_' in place of ' ').
    //   3. getOSList(..) will map "Windows XP", "Windows 7", etc. to "windows"
    Map<String,List<String>> osList   = new LinkedHashMap<>();
    Map<String,List<String>> archList = new LinkedHashMap<>();

    addOsArchEntries(osList,   "FreeBSD",  "freebsd");
    addOsArchEntries(osList,   "Linux",    "linux");
    addOsArchEntries(osList,   "Mac OS X", "mac os x",
                               "Mac_OS_X", "mac_os_x"); // <-- for good measure
    addOsArchEntries(osList,   "SunOS",    "sunos");
    addOsArchEntries(osList,   "Windows",  "windows");

    addOsArchEntries(archList, "ppc");
    addOsArchEntries(archList, "sparc");
    addOsArchEntries(archList, "sparcv9");
    addOsArchEntries(archList, "x86", "i386", "i686");
    addOsArchEntries(archList, "x86_64", "amd64");

    // OS X "Universal" binary is a special case where universal maps to "i386"
    // and "x86_64" but not the reverse -- in this case we add in "x86" (even
    // though it isn't used on Mac OS X at this time) to be consistent with the
    // "x86_64" entry.
    archList.put("universal", Arrays.asList("universal", "i386", "x86", "x86_64"));
    archList.put("UNIVERSAL", Arrays.asList("universal", "i386", "x86", "x86_64"));

    OS_LIST   = Collections.unmodifiableMap(osList);
    ARCH_LIST = Collections.unmodifiableMap(archList);
  }

  /** Adds entries to the OS_LIST or ARCH_LIST mappings, only used in the
   *  static initializer above.
   */
  private static void addOsArchEntries (Map<String,List<String>> map, String ... entries) {
    List<String> list = Arrays.asList(entries);

    for (String e : list) {
      map.put(e,               list);
      map.put(e.toLowerCase(), list);
      map.put(e.toUpperCase(), list);

      if (e.contains(" ")) {
        e = e.replace(' ', '_');
        map.put(e,               list);
        map.put(e.toLowerCase(), list);
        map.put(e.toUpperCase(), list);
      }
    }
  }

  /** Gets the mapping of "given" OS name to the normalized form that should be
   *  used in a file name. Unknown names will simply map to themselves but in
   *  lower-case with spaces replaced by underscores.
   *  @param os The nominal version of the OS value (null to use Java VM default).
   *  @return The variation to use in the file name (not null).
   */
  public static String getOSName (String os) {
    if ((os == null) || os.isEmpty()) os = Shell.getProperty("os.name");
    return getOSList(os).get(0).toLowerCase().replace(' ','_');
  }

  /** Gets the mapping of "given" platform arch to the normalized form that
   *  should be used in a file name. Unknown names will simply map to themselves
   *  but in lower-case with spaces replaced by underscores.
   *  @param arch The nominal version of the arch value (null to use Java VM default).
   *  @return The variation to use in the file name (not null).
   */
  public static String getArchName (String arch) {
    if ((arch == null) || arch.isEmpty()) arch = Shell.getProperty("os.arch");
    return getArchList(arch).get(0).toLowerCase().replace(' ','_');
  }

  /** Gets the mapping of "given" OS name to the variations supported by it in
   *  os.name variable (these will be the list of names to include in a JNLP
   *  file). In most cases this will simply convert to the standard
   *  case-sensitive values (e.g. "linux" to {"Linux","linux"}).
   *  @param os The nominal version of the OS value.
   *  @return The list of OS values to use in the JNLP file. If no mapping is
   *          found in the lookup, this will return a one-element list with the
   *          given arch value. (The return value will never be null or an empty
   *          list.)
   */
  public static List<String> getOSList (String os) {
    os = os.trim();
    return getOSList(os, os);
  }

  /** Internal use only: used by getOSList(String). */
  private static List<String> getOSList (String os, String def) {
    List<String> list = OS_LIST.get(os.trim());
    if (list != null) return list;

    // Some OS versions (especially Windows) include a version number at the
    // end (e.g. "Windows XP" or "Windows 2003") that can be stripped off,
    // but always keep the existing 'def' value in case nothing is found.
    int i;
    if ((i = os.lastIndexOf(' ')) > 0) return getOSList(os.substring(0,i), def);
    if ((i = os.lastIndexOf('_')) > 0) return getOSList(os.substring(0,i), def);

    return Arrays.asList(def);
  }

  /** Gets the mapping of "given" platform arch to the variations supported by
   *  it in the os.arch variable (these will be the list of names to include
   *  in a JNLP file).
   *  @param arch The nominal version of the arch value.
   *  @return The list of preferred arch values to use in the JNLP file. If no
   *          mapping is found in the lookup, this will return a one-element
   *          list with the given arch value. (The return value will never be
   *          null or an empty list.)
   */
  public static List<String> getArchList (String arch) {
    List<String> list = ARCH_LIST.get(arch.trim());
    return (list != null)? list : Arrays.asList(arch);
  }

  /** <b>Internal Use Only:</b> Gets the default name of the native JAR to use
   *  for the given O/S and architecture. <br>
   *  <br>
   *  <i>This is here only because of convenience, this function probably belongs
   *  elsewhere.</i>
   *  @param opt  The option tree name (not null).
   *  @param os   The nominal version of the OS value (null or "NULL" to use Java VM default).
   *  @param arch The nominal version of the arch value (null or "NULL" to use Java VM default).
   *  @return The default JAR file name (e.g. "nxm-sys-native-linux-x86.jar")
   */
  public static String getNativeJarName (String opt, String os, String arch) {
    if (StringUtil.isNull(os)   ||   os.isEmpty()) os   = Util.getOSName(null);
    if (StringUtil.isNull(arch) || arch.isEmpty()) arch = Util.getArchName(null);
    return "nxm-"+opt.toLowerCase()+"-native-"+os+"-"+arch+".jar";
  }

  /** Gets the "short" name for a class. This is identical for the "cannonical" name of the class
   *  less the package name (if present).
   *  @param <T>   The class type.
   *  @param clazz The class.
   *  @return The "short" name for the class (null if the input is null).
   *  @since NeXtMidas 3.9.0
   */
  public static <T> String getShortClassName (Class<T> clazz) {
    if (clazz == null) return null;

    String  className = clazz.getCanonicalName();
    Package pkg       = clazz.getPackage();

    return (pkg == null)? className : className.substring(pkg.getName().length()+1);
  }

  /** Gets the first item from a collection. This has optimizations for some of the most common
   *  collection types. The name {@code first} matches the name used by {@link SortedSet}.
   *  @param <T> The type.
   *  @param c   The collection.
   *  @return The first item (which may be null) or null if the collection is null or empty.
   *  @since NeXtMidas 3.9.0
   */
  @ProvisionalUseOnly("until fully tested")
  public static <T> T first (Collection<T> c) {
    // The order of the next lines is important. Note that Deque is the parent to LinkedList and
    // using its getFirst/getLast is significantly faster than anything involving size() calls.
    if (c == null             ) return null;
    if (c.isEmpty()           ) return null;
    if (c instanceof Deque    ) return ((Deque<T>)c).getFirst();
    if (c instanceof List     ) return ((List<T>)c).get(0);
    if (c instanceof SortedSet) return ((SortedSet<T>)c).first();

    return c.iterator().next();
  }

  /** Gets the last item from a collection. This has optimizations for some of the most common
   *  collection types. The name {@code last} matches the name used by {@link SortedSet}.
   *  @param <T> The type.
   *  @param c   The collection.
   *  @return The last item (which may be null) or null if the collection is null or empty.
   *  @since NeXtMidas 3.9.0
   */
  @ProvisionalUseOnly("until fully tested")
  public static <T> T last (Collection<T> c) {
    // The order of the next lines is important. Note that Deque is the parent to LinkedList and
    // using its getFirst/getLast is significantly faster than anything involving size() calls.
    if (c == null             ) return null;
    if (c.isEmpty()           ) return null;
    if (c instanceof Deque    ) return ((Deque<T>)c).getLast();
    if (c instanceof List     ) return ((List<T>)c).get(c.size()-1);
    if (c instanceof SortedSet) return ((SortedSet<T>)c).last();

    Iterator<T> i = c.iterator();
    T           v = i.next();
    while (i.hasNext()) v = i.next();
    return v;
  }

  /** Creates a set using the given elements.
   *  @param <T>  The value type.
   *  @param vals The values.
   *  @return The set.
   *  @since NeXtmidas 3.9.0
   */
  @SafeVarargs
  public static <T> Set<T> asSet (T... vals) {
    return unmodifiableSet(new HashSet<>(asList(vals)));
  }

  /** Creates a sorted set using the given elements.
   *  @param <T>  The value type.
   *  @param vals The values.
   *  @return The set.
   *  @since NeXtMidas 3.9.0
   */
  @SafeVarargs
  public static <T> SortedSet<T> asSortedSet (T... vals) {
    return unmodifiableSortedSet(new TreeSet<>(asList(vals)));
  }

  /** Creates a {@link ThreadFactory} that will log any uncaught exceptions.
   *  @param name The name of the new thread group (not null).
   *  @return The new thread factory.
   *  @since NeXtMidas 3.9.0
   */
  public static ThreadFactory newThreadFactory (String name) {
    requireNonNull(name);
    ThreadGroup threadGroup = new ThreadGroup(name);

    return (r) -> {
      Thread thread = new Thread(threadGroup,r);
      thread.setUncaughtExceptionHandler((t,e) -> {
        LOG.log(Level.SEVERE, "Uncaught exception in "+t.getName(), e);
      });
      return thread;
    };
  }

  /** Creates a {@link RejectedExecutionHandler} that will log any reject executions.
   *  @param name The name of the associated thread group (not null).
   *  @return The new handler.
   *  @since NeXtMidas 3.9.0
   */
  @ProvisionalUseOnly("until fully tested")
  public static RejectedExecutionHandler newRejectedExecutionHandler (String name) {
    requireNonNull(name);
    return (r,ex) -> {
      LOG.log(Level.SEVERE, "{0}: Rejected execution of {1} by {2}", new Object[] {name, r, ex});
    };
  }

  /** Scales the value to the given number of decimal, but keeping the sign intact.
   *  This is identical to {@code round(val,scale,RoundingMode.HALF_EVEN,true,1)}.
   *  @param val   The original value.
   *  @param scale The number of decimal points (e.g. 3 for three decimal places).
   *  @return The value rounded to the specified number of decimal places, with special handling
   *          for values that would normally round to zero.
   *  @since NeXtMidas 3.9.0
   */
  public static Number scale (Number val, int scale) {
    return round(val,scale);
  }

  /** Rounds to the given number of decimal places, rounding to zero as applicable.
   *  This is identical to {@code round(val,scale,RoundingMode.HALF_EVEN,false,1)}.
   *  @param val   The original value.
   *  @param scale The number of decimal points (e.g. 3 for three decimal places).
   *  @return The value rounded to the specified number of decimal places.
   *  @since NeXtMidas 3.9.0
   */
  public static Number round (Number val, int scale) {
    return round(val,scale,RoundingMode.HALF_EVEN,false,1);
  }

  /** Rounds to the given number of decimal places. This is better-stated as a rounding to the
   *  nearest 10<sup>-scale</sup> since it supports rounding both to the left and right of the
   *  decimal point.
   *  @param val      The original value.
   *  @param scale    The number of decimal points (e.g. 3 for three decimal places). Negative
   *                  values imply rounding left of the decimal place (e.g. -2 means round to the
   *                  nearest 100).
   *  @param mode     The {@code RoundingMode} to use.
   *  @param keepSign Keep the sign on any non-zero value. If this is true and a non-zero value
   *                  rounds to zero, this method will return the non-zero value closest to zero
   *                  that has the same sign as the value passed in. This is useful in situations
   *                  where a value of zero has a special meaning (e.g. an API where zero means
   *                  "wait forever" and a small non-zero value means "wait for a small duration").
   *  @param fast     Should speed be preferred over consistency in type and {@code toString()}
   *                  output? The default is typically {@code fast=1} since rounding is often
   *                  related to output in string form.
   *                  <pre>
   *                    Fast | Return Type |                Allowed Return Values
   *                         |             | round(1234.0,-2,mode,fast)| round(0.210,3,mode,fast)
   *                    -----+-------------+---------------------------+----------------------------
   *                      0  | BigDecimal* | 1200                      | 0.210
   *                    -----+-------------+---------------------------+----------------------------
   *                     *1* | Number      | 1200 / 1200.0             | 0.210 / 0.21
   *                    -----+-------------+---------------------------+----------------------------
   *                      2  | Number      | 1200 / 1200.0 / 12e2      | 0.210 / 0.21
   *                    -----+-------------+---------------------------+----------------------------
   *                    (* Note that +Inf, -Inf, and NaN will always be returned as a Double)
   *                    (** Note - When fast &gt; 0 The return type will be BigDecimal unless it is of the type
				   Byte, Short, Integer, Long, AtomicInteger, or AtomicLong.
			           Otherwise it will be the type sent in.
   *                  </pre>
   *  @return The value rounded to the specified number of decimal places. The values +Inf,
   *          -Inf, and NaN will be returned as the applicable Double values. If the input is
   *          negative-zero the output may be stripped of its sign regardless of the value of
   *          {@code keepSign}.
   *  @throws NumberFormatException If the value is not null, +Inf, -Inf, or NaN and can not be
   *                                converted to {@link BigDecimal}.
   *  @since NeXtMidas 3.9.0
   */
  public static Number round (Number val, int scale, RoundingMode mode, boolean keepSign, int fast) {
    BigDecimal num;
    if (val == null) {
      return null;
    }
    else if ((val instanceof Byte) ||
             (val instanceof Short) ||
             (val instanceof Integer) ||
             (val instanceof Long) ||
             (val instanceof AtomicInteger) ||
             (val instanceof AtomicLong)) {

      if ((fast >= 1) && (scale >= 0)) {
        return val; // No rounding needed
      }
      num = BigDecimal.valueOf(val.longValue());
    }
    else if ((val instanceof Double) || (val instanceof Float)) {
      double v = val.doubleValue();
      if (Double.isInfinite(v)) return v;        // Return +Infinity/-Infinity
      if (Double.isNaN(v)     ) return v;        // Return NaN

      if ((v == 0) && (fast >= 1)) {
        return v;
      }
      num = BigDecimal.valueOf(v);
    }
    else if (val instanceof BigDecimal) {
      num = (BigDecimal)val;
    }
    else if (val instanceof BigInteger) {
      num = new BigDecimal(val.toString());
    }
    else {
      try {
        num = new BigDecimal(val.toString());
      }
      catch (NumberFormatException e) {
        try {
          double v = val.doubleValue();
          if (Double.isInfinite(v)) return v;        // Return +Infinity/-Infinity
          if (Double.isNaN(v)     ) return v;        // Return NaN
        }
        catch (NumberFormatException e2) {
        }
        throw e;

      }
    }

    BigDecimal dec = num.setScale(scale,mode);
    if (keepSign && (dec.signum() != num.signum())) {
      dec = BigDecimal.valueOf(num.signum(), scale);
    }

    if ((fast >= 2) || (scale >= 0)) {
      return dec;
    }
    else {
      // Set scale back to zero so rounding 1234 to the hundreds produces 1200 and not 12e2
      return dec.setScale(0, mode);
    }
  }

}
