package nxm.sys.lib;

import static java.lang.Math.min;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
//import java.lang.reflect.Executable; TODO: uncomment for java 8 code
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.DataTypes;
import nxm.sys.inc.Indexable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.KeyObjectNames;
import nxm.sys.inc.Keyable;

/** A class to handle accessing Objects from the macro language. This class is used by the results
 *  table to access methods and fields of objects. <br>
 *  <br>
 *  A very simplified version of what is being done:
 *  <pre>
 *    For read access, the algorithm first looks for a getNAME() method, where NAME is the key name
 *    for the attribute.  The search is case insensitive. If this fails, a case insensitive search
 *    for a field named NAME is performed.
 *
 *    For write access, the algorithm first looks for a setNAME(arg) method, where NAME is the key
 *    name for the attribute. The search is case insensitive. If this fails, it tries without the
 *    set for a NAME(arg) method.
 *
 *    If these fail and the root object implements Chainable, the access request is passed on to
 *    the next object in the chain.
 *  </pre>
 *  Simplified state diagram:
 *  <pre>
 *    {@code               START                                                                   }
 *    {@code                 |                                                                     }
 *    {@code                 v                                                                     }
 *    {@code      +---------------------+                                                          }
 *    {@code      | Check search order  |                                                          }
 *    {@code      +- - - - - - - - - - -+                                                          }
 *    {@code      | try to determine the|--------+ Turn SOFT_GET and SOFT_SET                      }
 *    {@code      | search order if a   |        | into GET or SET if possible                     }
 *    {@code      | "soft" setting      |<-------+                                                 }
 *    {@code      +---------------------+                                                          }
 *    {@code                 |                                                                     }
 *    {@code                 v                                                                     }
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj null?        |----------------> return null                             }
 *    {@code      +---------------------+                                                          }
 *    {@code                 | no                                                                  }
 *    {@code                 v                                                                     }
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj a Table or   |----------------+                                         }
 *    {@code      | or KeyVector?       |                v                                         }
 *    {@code      +---------------------+    +---------------------+ yes                           }
 *    {@code                 | no            | Is SET with 2 args? |-----> Do a Table.getKey(..)   }
 *    {@code                 |               +---------------------+       then a getIndexed(..)   }
 *    {@code                 |                           | no                                      }
 *    {@code                 |                           v                                         }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |               | Is GET or has more  | no                            }
 *    {@code                 |               | than 1 arg?         |-----> return Table.setKey(..) }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | yes                                     }
 *    {@code                 |                           v                                         }
 *    {@code                 |               +---------------------+ yes                           }
 *    {@code                 |               | Does it have 1 arg? |-----> return T.getKey(..)     }
 *    {@code                 |               |                     | no                            }
 *    {@code                 |               |                     |-----> Do a T.getKey(..)       }
 *    {@code                 |               +---------------------+       then a getIndexed(..)   }
 *    {@code                 v                                                                     }
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj an XMValue?  |----------------+                                         }
 *    {@code      +---------------------+                v                                         }
 *    {@code                 | no            +---------------------+ yes                           }
 *    {@code                 |               | Is key "GETSIZE"?   |-----> return XMValue.size()   }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | no                                      }
 *    {@code                 |                           v                                         }
 *    {@code                 |               +---------------------+ yes                           }
 *    {@code                 |               | Is this a GET?      |-----> return XMValue.get(..)  }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                          |            no                            }
 *    {@code                 v                          +----------------> return XMValue.set(..)  }
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj Keyable?     |----------------+                                         }
 *    {@code      +---------------------+                v                                         }
 *    {@code                 | no            +---------------------+                               }
 *    {@code                 |               | Try get/set->val    |                               }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           |                                         }
 *    {@code                 |                           v                                         }
 *    {@code                 |               +---------------------+ no                            }
 *    {@code                 |               | Is val null?        |-----> return val              }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | no                                      }
 *    {@code                 |  +------------------------+                                         }
 *    {@code                 v  v                                                                  }
 *    {@code      +---------------------+                                                          }
 *    {@code      | Check for Array     |----------------+                                         }
 *    {@code      +---------------------+                v                                         }
 *    {@code                 | no            +---------------------+                               }
 *    {@code                 |               | Is key "GETSIZE" or | yes                           }
 *    {@code                 |               | or "SIZE"?          |-----> return array.length     }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | no                                      }
 *    {@code                 |                           v                                         }
 *    {@code                 |               +---------------------+ 0                             }
 *    {@code                 |               | How many args were  |-----> return getIndexed(..)   }
 *    {@code                 |               | passed in?          | 1                             }
 *    {@code                 |               |                     |-----> return setIndexed(..)   }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | 2+                                      }
 *    {@code                 |  +------------------------+                                         }
 *    {@code                 v  v                                                                  }
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj Indexable and|----------------+                                         }
 *    {@code      | key an index value? |                v                                         }
 *    {@code      +---------------------+    +---------------------+ yes                           }
 *    {@code                 | no            | Is GET with 0 args? |-----> return getIndexed(..)   }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | no                                      }
 *    {@code                 |                           v                                         }
 *    {@code                 |               +---------------------+ yes                           }
 *    {@code                 |               | Is SET with 1 arg?  |-----> return setIndexed(..)   }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                           | no                                      }
 *    {@code                 |  +------------------------+                                         }
 *    {@code                 v  v                                                                  }
 *    {@code      +---------------------+ 1 found*                                                 }
 *    {@code      | Find members to     |--------------------------------> return invoke(...)      }
 *    {@code      | access in obj (hard)| 2+ found                                                 }
 *    {@code      |                     |--------------------------------> ERROR                   }
 *    {@code      +---------------------+                                                          }
 *    {@code                 | 0 found                         (* If a method and field both match)}
 *    {@code                 v                                 (  the field is ignored.           )}
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj Indexable and|----------------+                                         }
 *    {@code      | args[0] is an index |                v                                         }
 *    {@code      | value and a GET?    |    +---------------------+                               }
 *    {@code      +---------------------+    | Try from START using|                               }
 *    {@code                 | no            | only obj and key    |                               }
 *    {@code                 |               +---------------------+                               }
 *    {@code                 |                   | got error  | got value                          }
 *    {@code                 |  +----------------+ or null    |                                    }
 *    {@code                 |  |                             v                                    }
 *    {@code                 |  |            +---------------------+                               }
 *    {@code                 |  |            | Try from top using  | pass                          }
 *    {@code                 |  |            | only obj and key and|-----> Use getIndexed(..) on   }
 *    {@code                 |  |            | then do getIndexed  |       value from obj+key GET  }
 *    {@code                 |  |            | with resulting value|                               }
 *    {@code                 |  |            +---------------------+                               }
 *    {@code                 |  |                        | fail                                    }
 *    {@code                 |  |  +---------------------+                                         }
 *    {@code                 v  v  v                                                               }
 *    {@code      +---------------------+ 1 found*                                                 }
 *    {@code      | Find members to     |--------------------------------> return invoke(...)      }
 *    {@code      | access in obj (soft)| 2+ found                                                 }
 *    {@code      |                     |--------------------------------> ERROR                   }
 *    {@code      +---------------------+                                                          }
 *    {@code                 | 0 found                         (* If a method and field both match)}
 *    {@code                 v                                 (  the field is ignored.           )}
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj Chainable?   |--------------------------------> Repeat ftom START using }
 *    {@code      +---------------------+                                  getNextLink()           }
 *    {@code                 | no                                                                  }
 *    {@code                 v                                                                     }
 *    {@code      +---------------------+ yes                                                      }
 *    {@code      | Is obj Keyable?     |--------------------------------> return null             }
 *    {@code      +---------------------+                                  (done above w/o error)  }
 *    {@code                 | no                                                                  }
 *    {@code                 v                                                                     }
 *    {@code               ERROR                                                                   }
 *
 *    Notes:
 *      - Prior to NeXtMidas 3.7 the state diagram was included in the "NeXtMidas User's Guide" as
 *        part of the "KeyObject" section. It was moved here in NeXtMidas 3.7 to make it easier to
 *        keep the diagram in synch with the code.
 *      - The documentation in NeXtMidas 3.6 and earlier erroneously omitted XMValue from the state
 *        diagram, its "addition" to the NeXtMidas 3.7 documention is merely a correction (it was
 *        not added in the 3rd Edition)
 *      - The 3rd Edition made slight changes to the "search order" handling, method matching, and
 *        the way Indexable objects are accessed
 *  </pre>
 *  Version History:
 *  <pre>
 *    1st Edition - First edition as it existed at the end of the NeXtMidas 1.x series
 *
 *    2nd Edition - Re-worked to fix mechanism for finding methods/constructors in NeXtMidas 2.x,
 *                  added aupport for XMValue, and various updates to address bugs
 *
 *    3rd Edition - Re-worked to add support for KeyObjectNames in NeXtMidas 3.7, this had been a
 *                  desired update for a number of years (to fix a problem with conflicting names)
 *                  but could not be done until NeXtMidas moved to Java 7 (which added annotations)

 *    As can be seen by the state diagram (above), the algorithm is extremely complex. Much of this
 *    is due to the 1st Edition of KeyObject which introduced all of the complex rules for support
 *    of Chainable, Indexable, KeyVector, Keyable, and Table; and the rules for permitting method
 *    names that omit the set/get and can be abbreviated or passed as accronyms. These complex rules
 *    evolved throughout the NeXtMidas 1.x versions as various authors contributed to this class
 *    without a pre-defined CONOP in mind for the overal interworking of KeyObject.
 *
 *    Another artifact of the 1st Edition is that the macro-level RESULTS, GET, SET, and INVOKE
 *    commands all enter KeyObject in via the getKey(..) method, despite there being other methods
 *    that are more appropriate (e.g. SET should probably call setKey(..)).
 *
 *    Due to backwards-compatibility concerns (many users hit the corner-cases), the updates made in
 *    the 2nd Edition and 3rd Edition have had to maintain nearly all of the behaviors of the 1st
 *    Edition. As a result of the complexity of KeyObject and backwards-compatibility concerns, any
 *    changes to KeyObject are extremely difficult to make.
 *  </pre>
 *  @see Chainable
 *  @see Indexable
 *  @see KeyObjectNames
 *  @see Keyable
 *  @author Jeff Schoen      (1st Edition, early)
 *  @author Various          (1st Edition, later)
 *  @author J. Czechowski IV (2nd Edition)
 *  @author Neon Ngo         (2nd Edition, updates)
 *  @author Conrad Clark     (3rd Edition)
 *  @author J. Czechowski IV (3nd Edition)
 */
public class KeyObject extends Object implements DataTypes {
  private static final Object[]    EMPTY_OBJECT_ARRAY = new Object[0];   // immutable zero-length Object array
  private static final Class<?>[]  EMPTY_CLASS_ARRAY  = new Class<?>[0]; // immutable zero-length Class array
  private static final Set<String> INIT_ONLY          = Collections.singleton("<init>");
  private static final String[]    COMMON_PACKAGES    = { "java.lang.",   "java.util.",
                                                          "nxm.sys.inc.", "nxm.sys.lib.", "nxm.sys.libg." };
  private static final boolean     BE_ANNOYING        = false; // Enables public variable warnings.
  private static final boolean     DEBUG_FINDER       = false; // Enables debugging printouts for the finder
  private static final boolean     EXTRA_DEBUG        = false; // Enables extra debug information for DEBUG_FINDER
  private static final boolean     DEBUG_GETKEY       = false; // Enables debug for getKey()
  private static final boolean     WARN_AMBIGUOUS     = false; // Warn (rather than error) for ambiguities

  /** store constructed MacroAccessibleObject sets for later reuse */
  private static final Map<Class<?>, Set<MacroAccessibleObject>> allMAOsByClass = new ConcurrentHashMap<>(128);

  /** Flag for setKeys(..): Perform a soft set, do not error out and do not issue a warning if
                            no applicable set method is found.
   */
  public static final int SOFT=0x1;

  /** Flag for setKeys(..): *** Not Used *** */
  public static final int RESOLVE=0x2;

  /** Flag for setKeys(..): Issue a warning if no applicable set method is found. This is private
                            since it should always be combined with SOFT (see SOFT_WARN).
   */
  private static final int WARN=0x4;

  /** Flag for setKeys(..): Perform a soft set with a warning, do not error out and but issue a
                            warning if no applicable set method is found. (This includes {@link #SOFT}).
   */
  public static final int SOFTWARN=SOFT | WARN;

  /** Infer Java bean accessor or modifier names and include them in searches */
  private static enum SearchOrder {
    GET("get","is"),
    SET("set",null),
    SOFT_GET("get","is"),
    SOFT_SET("set",null);

    public final String prefix;
    public final String alternate;

    /** Creates a new instance.
     *  @param prefix    The method prefix.
     *  @param alternate The alternate method prefix (null if n/a).
     */
    private SearchOrder (String prefix, String alternate) {
      this.prefix    = prefix;
      this.alternate = alternate;
    }
  }

  /** Creates a new instance.
   *  @deprecated Since NeXtMidas 3.7.1, all methods in this class are static, do not instantiate.
   */
  @Deprecated // This should be private and class should be final
  public KeyObject () {
    // does nothing
  }

  /** Get a keyed value from an object. This is identical to {@code     getKey(root,key,null,null}.
   *  @param root  The object to get keyed field in
   *  @param key   The key to get
   *  @return The keyed value.
   */
  public static Object getKey (Object root, String key) {
    return getKey(root,key,null,null);
  }

  /** Get a keyed value from an object.
   *  @param root  The object to get keyed field in
   *  @param key   The key to get
   *  @param value Extra arguments to pass when invoking the method to get the keyed value
   *  @param ref   Reference object (to get Midas context) used when getting keys
   *  @return The keyed value.
   */
  public static Object getKey (Object root, String key, Object value, Object ref) {
    if (DEBUG_GETKEY) {
      Shell.writeln("KeyObject.getKey(..) ENTER; root="+Util.getClassName(root)
        +" key="+key+" value="+Util.getClassName(value)+" ref="+Util.getClassName(ref));
    }
    for (int kp; (kp=key.indexOf('.'))>0; ) {  // kp = index of next dot in key
      int klp = key.indexOf('(');              // index of first left paren (open)
      // if (klp>0&&klp<kp) break;             // ORIG 2.6.x
      if (klp>0 && klp<kp) {                   // if left paren is before dot (fix for Bug 1647 & 1840)
        int lpcount = 1;                       // count of left parens including first left paren
        int rpcount = 0;                       // count of right parens after first left paren
        boolean doublequote = false;           // true if within a double quote
        boolean singlequote = false;           // true if within a single quote
        int klen = key.length();               // length of current key (different for each loop iteration)
        int ii=klp+1;                          // index of to start looking for left and right parens
        for (; ii<klen; ii++) {
          char ch = key.charAt(ii);
          if (ch == ')' && !doublequote && !singlequote) {    // right paren outside of single and/or double quotes
            rpcount++;
            if (ii+1 == kp && rpcount==lpcount) break;        // right paren just before dot & parens balanced, break from inner for loop
          }
          else if (ch=='(' && !doublequote && !singlequote) { // left paren outside of single and double quotes
            lpcount++;
          }
          else if (ch=='"')  doublequote = !doublequote;
          else if (ch=='\'') singlequote = !singlequote;
        } // end inner-for loop

        if (DEBUG_GETKEY) {
          debug("", "KeyObject.getKey(..) ==> key="+key+" klp="+klp+" kp="+kp+" key.len="+klen+
                    " rpcount="+rpcount+" lpcount="+lpcount + " ii="+ii);
        }

        if (rpcount >  lpcount) {
          throw new MidasException("Extra right-parenthesis. key="+key);
        }
        else if (rpcount == lpcount) { // balanced left and right parens
          if (ii == klen) break;       // right paren at end of string (so break out)
                                       // otherwise continue processing
        }
        else throw new MidasException("Unbalanced parenthesis. key="+key);
      } // end if (klp>0 && klp<kp)

      String subKey = key.substring(0,kp);
      if (DEBUG_GETKEY) {
        Shell.writeln("KeyObject.getKey(..)   calling getKey(....) subKey="+subKey+" klp="+klp+" kp="+kp);
      }
      root=getKey(root,subKey,null,ref);
      key=key.substring(kp+1);
    } // end for loop

    if (DEBUG_GETKEY) {
      Shell.writeln("KeyObject.getKey(..)   after for loop root="+Util.getClassName(root)+" key="+key);
    }

    if (root==null) {
      return null;
    }
    Object[] args;
    int ip = key.indexOf('(');
    if (ip>0) {
      args = parseArgs(key,ip,key.length(),value,ref);
      key = key.substring(0,ip);
    }
    else if (value instanceof Object[]) {
      args = (Object[])value;
    }
    else {
      args = null;
    }

    if (DEBUG_FINDER) {
      debug("","KeyObject.getKey(..)");
    }
    return findAndInvoke(" | ", root, key, args, SearchOrder.SOFT_GET, ref);
  }

  /** Set a keyed value in an object. This is identical to {@code     setKey(root,key,value,null)}.
   *  @param root  The object to get keyed field in
   *  @param key   The key to get
   *  @param value Extra arguments to pass when invoking the method to get the keyed value
   *  @return The keyed value.
   */
  public static Object setKey (Object root, String key, Object value) {
    return setKey(root,key,value,null);
  }

  /** Set a keyed value in an object.
   *  @param root  The object to get keyed field in
   *  @param key   The key to get
   *  @param value Extra arguments to pass when invoking the method to get the keyed value
   *  @param ref   Reference object (to get Midas context) used when getting keys
   *  @return The keyed value.
   */
  public static Object setKey (Object root, String key, Object value, Object ref) {
    for (int kp; (kp=key.indexOf('.'))>0; ) {
      int klp = key.indexOf('('); if (klp>0&&klp<kp) break;
      root=getKey(root,key.substring(0,kp),null,ref);
      key=key.substring(kp+1);
    }
    if (root==null) return null;
    if (value==null) return getKey (root,key,null,ref); // just run Method
    Object[] args;
    int ip = key.indexOf('(');
    if (ip>0) {
      args = parseArgs(key,ip,key.length(),value,ref);
      key = key.substring(0,ip);
    }
    else if (value instanceof Object[]) {
      args = (Object[])value;
    }
    else {
      args = new Object[1];
      args[0] = value;
    }

    if (DEBUG_FINDER) {
      debug("","KeyObject.setKey(..)");
    }
    value = findAndInvoke(" | ", root, key, args, SearchOrder.SET, ref);
    return (value == null)? args[args.length-1]: value; // <-- To appease Results.java
  }

  /** Get a member of an indexable object. <br>
      <br>
      This is used to get values out of an array or array-like object. Currently this supports the
      following array-like types:
      <ul>
        <li><i>Java Arrays</i></li>
        <li>Instances of {@link java.util.List}</li>
        <li>Instances of {@link Indexable} interface (e.g. {@link Data} and {@link KeyVector})</li>
      </ul>
      @param root  The array or array-like object.
      @param index The index into the array-like object. Note that some array-like objects are
                   one-based while the majority tend to be zero-based.
      @return An element from the array-like object.
      @throws IllegalArgumentException If unable to get the element from the array-like object.
      @see #setIndexed(Object,int,Object)
   */
  @InternalUseOnly("Since NeXtMidas 3.7.1 - This is only for internal use within SYS")
  @SuppressWarnings({"UseSpecificCatch", "BroadCatchBlock", "TooBroadCatch"})
  public static Object getIndexed (Object root, int index) {
    // NOTE: this check needs to match the check in setIndexed(..) and isIndexable(..) below
    try {
      if (root instanceof Indexable) {
        return ((Indexable)root).getIndex(index);
      }
      if (root instanceof List) {
        return ((List<?>)root).get(index);
      }
      if ((root != null) && root.getClass().isArray()) {
        return Array.get(root,index);
      }
    }
    catch (Exception e) {
      throw new IllegalArgumentException("KeyObject.getIndexed(): Error in accessing element "
                                       + index + " from " + root + ": " + e);
    }

    throw new MidasException("Object is not indexable. index="+ index + " root=["+ root + "]");
  }

  /** Set a member of an indexable object. <br>
      <br>
      This is used to get values out of an array or array-like object. This supports the same object
      types as are supported by {@link #getIndexed(Object,int)}.
      @param root  The array or array-like object.
      @param index The index into the array-like object. Note that some array-like objects are
                   one-based while the majority tend to be zero-based.
      @param value The value to set.
      @return <code>value</code> (This may change in the future.)
      @throws IllegalArgumentException If unable to set the element in the array-like object.
      @see #getIndexed(Object,int)
   */
  @SuppressWarnings({"UseSpecificCatch", "BroadCatchBlock", "TooBroadCatch", "unchecked"})
  @InternalUseOnly("Since NeXtMidas 3.7.1 - This is only for internal use within SYS")
  public static Object setIndexed (Object root, int index, Object value) {
    // NOTE: this check needs to match the check in getIndex(..) above and isIndexable(..) below
    try {
      if (root instanceof Indexable) {
        ((Indexable)root).setIndex(index,value);
        return value;
      }
      if (root instanceof List) {
        ((List<Object>)root).set(index,value);
        return value;
      }
      if ((root != null) && root.getClass().isArray()) {
        Array.set(root,index,value);
        return value;
      }
    }
    catch (Exception e) {
      throw new IllegalArgumentException("KeyObject.setIndexed(): Error in setting element "
                                       + index + " in " + root + " to " + value + ": " + e);
    }
    throw new MidasException("Object is not indexable. index="+ index + " value=" + value + " root=["+ root + "]");
  }

  private static boolean isIndexable (Object obj) { // since 3.5.2
    // NOTE: this check needs to match the check in getIndex(..) and setIndexed(...) above
    return (obj instanceof Indexable)
        || (obj instanceof List     )
        || ((obj != null) && obj.getClass().isArray());
  }

  /** Get the index value associated with the named field. This is used for legacy functionality
      that used arrays with a given ordering rather than using an object. Use of this is discouraged
      since many array values do not correspond to these names and using these names with those
      arrays will give the wrong results. <br>
      <br>
      The currently supported <i>Key--&gt;Index</i> are as follows:
      <pre>
         "X"  --&gt; 0    "Y"  --&gt; 1    "Z"  --&gt; 2    "T"  --&gt; 3
         "R"  --&gt; 0    "I"  --&gt; 1
        "ALT" --&gt; 0   "LAT" --&gt; 1   "LON" --&gt; 2
      </pre>
      If the key is a number then that number is used as the index.
      @param key The named key.
      @return The index associated with that key or <code>-1</code> if none is found.
   */
  @InternalUseOnly("Since NeXtMidas 3.7.1 - This is only for internal use within SYS")
  public static int getKeyIndex (String key) {
    return getKeyIndex(key, null);
  }

  /** @since 3.5.2 for BUG 2869 */
  private static int getKeyIndex (String key, Object obj) {
    if ((obj != null) && !isIndexable(obj)) {
      return -1;
    }
    if (StringUtil.isNumber(key)) {
      return Convert.o2l(key);
    }
    switch (key) {
      case "X":   return  0;
      case "Y":   return  1;
      case "Z":   return  2;
      case "T":   return  3;
      case "R":   return  0;
      case "I":   return  1;
      case "ALT": return  0;
      case "LAT": return  1;
      case "LON": return  2;
      default:    return -1;
    }
  }

  /** Sets keys in an object from a table of key=value pairs. This is identical to
      <code>setKeys(root,tab,null,0)</code>.
      @param root  The object to set fields in.
      @param tab   The table of sets.
      @return Number of keys set. Returns -1 if root or tab is null.
      @see #setKeys(Object, Table, Object, int)
   */
  public static int setKeys (Object root, Table tab) {
    return setKeys(root,tab,null,0);
  }

  /** sets keys in an object from a table of key=value pairs
      @param root  The object to set fields in.
      @param tab   The table of sets.
      @param ref   Reference used when setting keys.
      @param flags Optional flags to control how sets are applied (0=Throw exception on error,
                   {@link #SOFT}=Ignore all errors (no warning), {@link #SOFTWARN}=Warn on error
                   but continue on).
      @return Number of keys set. Returns -1 if root or tab is null. Returns 0 if tab is empty.
   */
  public static int setKeys (Object root, Table tab, Object ref, int flags) {
    if ((root==null) || (tab==null) || (tab.isEmpty())) return -1;
    if (tab.isEmpty()) return 0; // optimization since 3.1.0
    int nkey=0;
    for (Table.Iterator ti=tab.iterator(); ti.getNext(); ) {
      String key = ti.key;
      Object xarg = ti.value;
      if (xarg instanceof Table) {
        try {
          setKey (root,key,xarg,ref); nkey++; continue;  // nkey++ and continue added in 3.1.1 to prevent double adding (BUG 2563)
        }
        catch (MidasException ex) {
          Object troot=null;
          try {
            troot = getKey(root,key);
          } catch (MidasException e) {
            // ignore
          }
          if (troot!=null) {
            setKeys(troot,(Table)xarg,ref,flags);
            continue;
          }
        }
      }
      if (xarg instanceof String) {
        xarg = Convert.s2o((String)xarg,'_',ref);
      }
      if ((flags&SOFT)==0) {
        setKey(root,key,xarg,ref); nkey++;
      }
      else {
        try {
          setKey(root,key,xarg,ref); nkey++;
        }
        catch (MidasException e) {
          if ((flags&WARN) != 0) {
            ref2Midas(ref).printStackTrace("KeyObject: Can not set "+key+"="+xarg,e);
          }
        }
      }
    }
    return nkey;
  }

  /** Converts a reference to a Midas instance, never returns null. */
  private static Midas ref2Midas(Object ref) {
    Midas midas = Convert.ref2Midas(ref);
    return (midas!=null)? midas: Shell.getSharedMidasContext();
  }

  /** Parses an arguments string into a parameters array
      Since 3.1.0 this method is private, this was deprecated in 2.7.0 as this is for internal use only.
      @param argstr The argument string
      @param i1 Start offset into argstr INCLUSIVE (0..len-1)
      @param i2 End offset into argstr EXCLUSIVE (1..len)
      @param xarg eXtra arguments to be added
      @param ref Midas reference
      @return Arguments as an array of Objects
   */
  private static Object[] parseArgs (String argstr, int i1, int i2, Object xarg, Object ref) {
    // jph -- 23AUG2004 - This method appears to be a "quick and dirty" way to parse
    // arguments... it probably should be deprecated/replaced.  I added the ability
    // for it to handle QUOTES but it may have problems with other more complicated
    // arg patterns

    int i,nargs=0;
    // Count the number of arguments
    int quotes = 0;
    for (i=++i1,i2--; i<i2; i++) {
      if (argstr.charAt(i) == '"') quotes = 1 - quotes;
      // Ignore commas inside quotes...
      if (quotes == 0) {
        i=argstr.indexOf(',',i); if (i<0) i=i2; nargs++;
      }
    }
    if (xarg!=null) nargs++;
    //System.out.println("ParseArgs: '"+argstr+"' nargs "+nargs);
    Object[] args = new Object[nargs];
    int iQuote;
    int iQuoteStart = -1;
    for (nargs=0; i1<i2; i1=i+1) {
      // Is the current character a QUOTE?
      if (argstr.charAt(i1) == '"') { iQuote = i1; }
      else { iQuote = -1; }

      //System.out.println("i1 "+i1+" char "+argstr.charAt(i1)+" i "+i+" iQuote "+iQuote);
      if (iQuote>=0) { if (iQuoteStart < 0) iQuoteStart = iQuote; }
      if (iQuoteStart < 0) {
        i=argstr.indexOf(',',i1); if (i<0) i=i2;
        //System.out.println("parseArgs: convert s20 on arg # "+(nargs+1)+" = '"
        //                  +argstr.substring(i1,i)+"'");
        args[nargs++] = Convert.s2o(argstr.substring(i1,i),'_',ref);
      } else {
        // Look for second QUOTE
        i=argstr.indexOf('"',iQuoteStart+1); if (i<0) i=i2; else i=i+1;
        //System.out.println("parseArgs: convert s20 on QUOTED arg # "+(nargs+1)+" = '"
        //                  +argstr.substring(iQuoteStart,i)+"'");
        args[nargs++] = Convert.s2o(argstr.substring(iQuoteStart,i),'_',ref);
        iQuoteStart = -1;
      }
    }
    if (xarg!=null) args[nargs++]=xarg;
    //for (i=0;i<nargs;i++) System.out.println("Arg"+i+"="+args[i]);
    return args;
  }

  /** key exact */
  public static final int KEY_EXACT=0;
  /** key caps */
  public static final int KEY_CAPS=1;
  /** key abbreviation */
  public static final int KEY_ABBR=2;
  /** key none */
  public static final int KEY_NONE=3;
  /** argument exact */
  public static final int ARG_EXACT=0;
  /** argument number */
  public static final int ARG_NUM=1;
  /** argument NUMO ?number object? */
  public static final int ARG_NUMO=2;
  /** argument CVT */
  public static final int ARG_CVT=3;
  /** argument none */
  public static final int ARG_NONE=4;

  /** Invoke a static method on a class and return the result. This method is very useful when
   *  you want to use classes that you don't want to import or otherwise reference explicitly.
   *  Examples include using the JNLP and netscape classes. In JDK1.4 the java.beans.Statement
   *  object can be used in place of this method.
   *  @param className  The class to invoke the method on.
   *  @param methodName The name of the static method to invoke.
   *  @param args       An array of objects to pass to the method.  If the method
   *                    takes no arguments, this can be null.
   *  @return The return value from invoking the method.
   *  @deprecated Since NeXtMidas 3.7.1 - This method was written to support reflective calls
   *              made when using older JVM versions, the APIs available in Java 8 make this
   *              method unnecessary.
   */
  @Deprecated
  @SuppressWarnings({"UseSpecificCatch", "BroadCatchBlock", "TooBroadCatch"})
  public static Object invokeStaticMethod (String className, String methodName, Object[] args) {
    try {
      Class<?> clas = Class.forName(className);
      Method methods[] = clas.getMethods();
      for (Method method : methods) {
        String name = method.getName();
        if (!name.equals(methodName)) continue;
        Class<?>[] params = method.getParameterTypes();
        if (params.length!=args.length) continue;
        boolean okArgs = true;
        for (int j=0; okArgs && j<params.length; j++) {
          String pname = params[j].getName();
          Class<?> aclass = args[j].getClass();
          String aname = aclass.getName();
          if (aname.equals(pname)) continue;
          okArgs=false;
          Class<?> sclass = aclass.getSuperclass();
          while (!okArgs && sclass != null) {
            okArgs = sclass.getName().equals(pname);
            sclass = sclass.getSuperclass();
          }
          if (okArgs) continue;
          Class<?>[] classes = aclass.getInterfaces();
          for (int k=0; classes!=null && k<classes.length && !okArgs; k++) {
            okArgs = classes[k].getName().equals(pname);
          }
        }
        if (okArgs) return method.invoke (null,args);
      }
    }
    catch (Exception e) {
      throw new MidasException("invokeStaticMethod: "+e);
    }
    throw new MidasException("No match to "+className+"."+methodName+" with "+Arrays.toString(args));
  }

  /** Invoke an instance method on an object and return the result. This method is very useful when
   *  you want to use classes that you don't want to import or otherwise reference explicitly.
   *  Examples include using the JNLP and netscape classes. In JDK1.4 the java.beans.Statement
   *  object can be used in place of this method.
   *  @param obj      The object to invoke the method on.
   *  @param methName The name of the static method to invoke.
   *  @param args     An array of objects to pass to the method.  If the method
   *                  takes no arguments, this can be null.
   *  @return The return value from invoking the method.
   *  @throws IllegalAccessException    If an {@link IllegalAccessException} is thrown when invoking
   *                                    the method.
   *  @throws NoSuchMethodException     If a {@link NoSuchMethodException} is thrown when invoking
   *                                    the method.
   *  @throws InvocationTargetException If an {@link InvocationTargetException} is thrown when invoking
   *                                    the method.
   *  @deprecated Since NeXtMidas 3.7.1 - This method was written to support reflective calls
   *              made when using older JVM versions, the APIs available in Java 8 make this
   *              method unnecessary.
   */
  @Deprecated
  public static Object invokeInstanceMethod (Object obj, String methName, Object[] args)
                                            throws IllegalAccessException, NoSuchMethodException,
                                                   InvocationTargetException  {

    int nargs = (args != null) ? args.length : 0;

    Class<?>[] argTypes = new Class<?>[ nargs ];
    for (int i = 0; i < nargs; i++) {
      argTypes[i] = args[i].getClass();
    }

    Method meth = obj.getClass().getMethod( methName, argTypes );
    return meth.invoke( obj, args );
  }

  /** Indicates if two types are matching. This should only be called from getGoodMethods(..).
      @param givenType       The given type.
      @param sampleType      The type to test for a match with the given type.
      @param allowBoxing     Indicates if boxing or primitives parameters is allowed.
      @param allowConversion Indicates if the conversion of primitive types is allowed for any
                             widening conversions.
      @param allowMidasConv  Indicates if Midas-specific conversions are permitted.
      @return An indication of whether or not they match.
   */
  private static boolean isMatchingType (Class<?> givenType, Class<?> sampleType, boolean allowBoxing,
                                         boolean allowConversion, boolean allowMidasConv) {
    boolean match = givenType.isAssignableFrom(sampleType);

    if (!match && allowBoxing && (givenType.isPrimitive() || sampleType.isPrimitive())) {

      match = isMatchingType(getBoxedType(givenType), getBoxedType(sampleType), false,
                             allowConversion, allowMidasConv);
    }

    if (!match && allowConversion) {
      if (Number.class.isAssignableFrom(givenType)) {
        match = Number.class.isAssignableFrom(sampleType) || sampleType.equals(Character.class);
      }

      if (givenType.equals(Character.class)) {
        match = match || Number.class.isAssignableFrom(sampleType);
      }
    }

    if (!match && allowMidasConv) {
      match = givenType.equals(String.class) || givenType.equals(Data.class);

      if (sampleType.equals(String.class)) {
        match = match || Number.class.isAssignableFrom(givenType)
                      || Boolean.class.isAssignableFrom(givenType)
                      || Character.class.isAssignableFrom(givenType);
      }

      if (!sampleType.isPrimitive()) {
        match = match || givenType.equals(Table.class);
      }
    }

    return match;
  }

  /** Gets the class for a given object. The class for the given object is determined as follows:
      <pre>
        (1) If obj==null then null will be returned.
        (2) If obj is an instance of Data...
               (2a) ...and the data is an ASCII value then String will be returned.
               (2b) ...and the data is a numerical scalar then the primitive type for the
                    data value will be returned (e.g. int).
               (2c) ...and the data is a numerical array then an array of the primitive type
                    for the data value will be returned (e.g. "VD" = double[]).
        (3) For all others, obj.getClass() will be returned.
      </pre>
      @param obj The object to get the class for.
      @return The object's class.
   */
  private static Class<?> getClassOf (Object obj) {
    if (obj == null) { return null; }
    if (obj instanceof Data) {
      Data data = (Data)obj;
      int  spa  = data.getSPA();

      switch (data.getFormat().charAt(1)) {
        case ASCII:    return String.class;
        case ASCII256: return String.class; // NXM-3570
        case DOUBLE:   return (spa == 1)? Double.TYPE  : (new double[spa]).getClass();
        case FLOAT:    return (spa == 1)? Float.TYPE   : (new float[spa] ).getClass();
        case XLONG:    return (spa == 1)? Long.TYPE    : (new long[spa]  ).getClass();
        case LONG:     return (spa == 1)? Integer.TYPE : (new int[spa]   ).getClass();
        case INT:      return (spa == 1)? Short.TYPE   : (new short[spa] ).getClass();
        case BYTE:     return (spa == 1)? Byte.TYPE    : (new byte[spa]  ).getClass();
      }
    }
    return obj.getClass();
  }

  /** Gets the class for each of a given set of objects.
      @param objs The Object array to get the class for each item.
      @return The object's class(es).
   */
  private static Class<?>[] getClassOf (Object[] objs) {
    Class<?>[] classes = new Class<?>[objs.length];

    for (int i = 0; i < classes.length; i++) {
      classes[i] = getClassOf(objs[i]);
    }

    return classes;
  }

  /** Converts a set of parameters to the appropriate types for invocation on a method.
      @param types  The types of the parameters the method takes in.
      @param params The parameters for the method, but not necessarily in the right form.
      @return The parameters to pass to the method.
   */
  private static Object[] convertParams (Class<?>[] types, Object[] params) {
    Object[] newParams = new Object[params.length];

    for (int i = 0; i < params.length; i++) {
      newParams[i] = convertParam(types[i], params[i]);

      if ((newParams[i] == null) && (params[i] != null)) {
        Shell.warning("KeyObject: Error converting "+params[i]+" to type "+types[i].getName()+".");
      }
    }

    return newParams;
  }

  /** Converts a parameter to the appropriate type for invocation on a method.
      @param type  The type of the parameters the method takes in.
      @param param The parameter for the method, but not necessarily in the right form.
      @return The parameters to pass to the method, returns <code>null</code> on error or if the
              parameter was <code>null</code>.
   */
  private static Object convertParam (Class<?> type, Object param) {
    type = getBoxedType(type); // Long.TYPE -> Long.class (no change for non-primitive types)

    if ((param == null) || type.isInstance(param)) {
      return param; // No conversion required
    }

    if (type.isArray() && param.getClass().isArray()) {
      Object[] array = new Object[Array.getLength(param)];
      Class<?> clazz;

      try {
        clazz = Class.forName(type.getName().substring(1)); /* int[] -> int */
      }
      catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("KeyObject: Unable to instantiate class "
                                         + type.getName().substring(1) + ": " + e);
      }

      for (int i = 0; i < Array.getLength(param); i++) {
        array[i] = convertParam(clazz, Array.get(param, i));
      }

      return array;

    }

    if (Number.class.isAssignableFrom(type) && (param instanceof Character)) {
      char charVal = (Character)param;
      return convertParam(type, Integer.valueOf(charVal));
    }

    if (type == Double.class   ) return Convert.o2d(param);
    if (type == Float.class    ) return Convert.o2f(param);
    if (type == Long.class     ) return Convert.o2x(param);
    if (type == Integer.class  ) return Convert.o2l(param);
    if (type == Short.class    ) return Convert.o2i(param);
    if (type == Byte.class     ) return Convert.o2b(param);
    if (type == Character.class) return (char)Convert.o2l(param);
    if (type == Boolean.class  ) return StringUtil.isTrue(""+param);
    if (type == String.class   ) return Convert.o2s(param);
    if (type == Data.class     ) return new Data(param);
    if (type == Table.class    ) return Convert.o2t(param);

    return param; // Cound not convert
  }

  /** Gets the boxed type for a primitive.
      @param type The primitive type.
      @return The boxed type.
   */
  private static Class<?> getBoxedType (Class<?> type) {
    if (!type.isPrimitive())    return type;
    if (type == Double.TYPE   ) return Double.class;
    if (type == Float.TYPE    ) return Float.class;
    if (type == Long.TYPE     ) return Long.class;
    if (type == Integer.TYPE  ) return Integer.class;
    if (type == Short.TYPE    ) return Short.class;
    if (type == Byte.TYPE     ) return Byte.class;
    if (type == Character.TYPE) return Character.class;
    if (type == Boolean.TYPE  ) return Boolean.class;
    if (type == Void.TYPE     ) return Void.class;
    throw new AssertionError("KeyObject: Unknown Java type "+type.getName()+".");
  }

  /** Generate a warning message that includes the signature for the required method.
      @param clazz      The class the filed is in.
      @param field      The deprecated field.
      @param isModifier Indicates if a modifier is required and not an accessor.
      @return The message to output.
   */
  private static StringBuilder getDeprecatedFieldWarning (Class<?> clazz, Field field, boolean isModifier) {
    char[] fieldName = field.getName().toCharArray();
    StringBuilder sig = new StringBuilder(255);

    sig.append("Direct use of public fields is deprecated, please add a method '");

    fieldName[0] = Character.toUpperCase(fieldName[0]);

    sig.append("public ");
    if (Modifier.isStatic(field.getModifiers())) { sig.append("static "); }
    sig.append(isModifier? "void": field.getType().getName());   /* Return type */
    sig.append(isModifier? " set": " get");                      /* Name prefix */
    sig.append(fieldName);                                       /* Name        */
    sig.append("(");
    sig.append(isModifier? field.getType().getName(): "");       /* Parameters  */
    sig.append(")");

    sig.append("' to ").append(clazz).append('.');
    return sig;
  }

  /** Finds a named method or property.
   *  @param clazz       The class to check in.
   *  @param name        The name of the property.
   *  @param types       The argument types to be applied. If this is <code>null</code> no argument
   *                     checks will be applied (used with the <code>QUERY</code> command) if this
   *                     is an array of length zero than the methods are assumed to accept zero
   *                     arguments.
   *  @param staticOnly  Only get static members.
   *  @return The members found.
   */
  @InternalUseOnly("Since NeXtMidas 3.7.1 - This is only for internal use within SYS")
  public static Member[] findMembers (Class<?> clazz, String name, Class<?>[] types, boolean staticOnly) {
    Set<MacroAccessibleObject> allMAOs = allMacroAccessibleObjects(clazz);
    TreeSet<MacroAccessibleObject> goodMAOs = new TreeSet<>();
    for (MacroAccessibleObject mao : allMAOs) {
      if (staticOnly && !mao.hasModifier(Modifier.STATIC)) continue;
      if ((mao.nameMatchMetric(SearchOrder.GET, name) < 0) &&
          (mao.nameMatchMetric(SearchOrder.SET, name) < 0)) {
        continue;
      }
      if ((mao.typeMatchMetric(SearchOrder.GET, types) < 0) &&
          (mao.typeMatchMetric(SearchOrder.SET, types) < 0)) {
        continue;
      }
      goodMAOs.add(mao);
    }
    Member[] members = new Member[goodMAOs.size()];
    int i = 0;
    for (MacroAccessibleObject mao : goodMAOs) {
      members[i] = (Member)mao.accessibleObject;
    }
    return members;
  }

  /** Finds a named method and invokes it.
   *  @param debugPrefix The debugging prefix.
   *  @param obj         The object to invoke the method on (may be a class if the method is static).
   *  @param key         The name of the method or name of a fields (with accessor/modifier).
   *  @param args        Arguments to pass to the method (may be an array of length zero).
   *  @param ref         Reference object (to get Midas context)
   *  @return The return value from the method (may be <code>null</code>) or <code>null</code> if
   *          there is no applicable return value.
   */
  @SuppressWarnings("null") // Suppress warning about debugInfo
  private static Object findAndInvoke (String debugPrefix, Object obj, String key, Object[] args,
                                       SearchOrder searchOrder, Object ref) {
    if (args == null) {
      args = EMPTY_OBJECT_ARRAY;
    }

    SearchOrder origSearchOrder = searchOrder;
    SearchOrder hardSearchOrder = searchOrder;
    boolean     hasOneArg       = (args != null) && (args.length == 1);
    boolean     isStatic        = ((obj != Class.class) && (obj instanceof Class)) || (obj == null);
    Class<?>    clazz           = (isStatic)? (Class<?>)obj: obj.getClass();

    if ((searchOrder == SearchOrder.SOFT_GET) || (searchOrder == SearchOrder.SOFT_SET)) {
      String keyLC = key.toLowerCase();
      if (keyLC.startsWith("get") || keyLC.startsWith("is")) {
        searchOrder     = SearchOrder.GET;
        hardSearchOrder = SearchOrder.GET;
      }
      else if (keyLC.startsWith("set")) {
        searchOrder     = SearchOrder.SET;
        hardSearchOrder = SearchOrder.SET;
      }
      else if (args.length == 0) {
        searchOrder     = SearchOrder.GET;
        hardSearchOrder = SearchOrder.GET;
      }
      else if (searchOrder == SearchOrder.SOFT_GET) {
        hardSearchOrder = SearchOrder.GET;
      }
      else {
        hardSearchOrder = SearchOrder.SET;
      }
    }

    if (DEBUG_FINDER) {
      debug(debugPrefix, "KeyObject.findAndInvoke()");
      debug(debugPrefix, "obj         = "+obj);
      debug(debugPrefix, "obj.class   = "+((obj!=null)? obj.getClass() : "n/a"));
      debug(debugPrefix, "ref         = "+ref);
      debug(debugPrefix, "key         = '"+key+"'");
      debug(debugPrefix, "args        = "+Arrays.toString(args));
      debug(debugPrefix, "searchOrder = "+origSearchOrder+" -> "+searchOrder+" ("+hardSearchOrder+")");
    }

    if (obj == null) {
      return null;
    }
    if ((obj instanceof Table) || (obj instanceof KeyVector)) {
      Keyable keyable = (Keyable)obj;

      Object iarg = (args.length==2)? args[0] : null;  // prep for indexed key
      if ((hardSearchOrder == SearchOrder.SET) && (iarg != null)) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a Table/KeyVector, using setIndexed(..)");
        }
        return setIndexed(keyable.getKey(key),Convert.o2l(iarg),args[1]);
      }
      else if (!hasOneArg || (hardSearchOrder == SearchOrder.GET)) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a Table/KeyVector, using getKey(String))");
        }
        Object value = keyable.getKey(key);

        // This next section used when getting "X(0)" from the results table where X is a VD.
        return (hasOneArg)? getIndexed(value, getKeyIndex(args[0].toString()))
                          : value;
      }
      else {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a Table/KeyVector, using setKey(String,Object))");
        }
        return keyable.setKey(key, args[0]);
      }
    }
    if (obj instanceof XMValue) {
      XMValue xmvalue = (XMValue)obj;

      if (key.equalsIgnoreCase("GETSIZE")) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a XMValue, getting size)");
        }
        return xmvalue.getSize();
      }
      else if (!hasOneArg || (hardSearchOrder == SearchOrder.GET)) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a XMValue, using get(String))");
        }
        return xmvalue.get(key);

      }
      else {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a XMValue, using set(String,Object))");
        }
        return xmvalue.set(key, args[0]);
      }
    }
    if (obj instanceof Keyable) {
      if ((args.length == 0) && (hardSearchOrder == SearchOrder.GET)) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = n/a (Object is a Keyable, using getKey(String))");
        }
        Object value = ((Keyable)obj).getKey(key);
        if (value != null) {
          return value;
        }
      }
      else if ((args.length == 1) && (hardSearchOrder == SearchOrder.SET)) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "(Object is a Keyable, using setKey(String,Object))");
        }
        Object value = ((Keyable)obj).setKey(key, args[0]);
        if (value != null) {
          return value;
        }
      }
    }
    if (clazz.isArray()) {
      int index = getKeyIndex(key,obj);

      if (key.equalsIgnoreCase("SIZE") || key.equalsIgnoreCase("GETSIZE")) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = null (getting size of array)");
        }

        if (args.length == 0) {
          return Array.getLength(obj);
        }
        else {
          throw new MidasException("KeyObject: Unable to set size of array, array size is immutable.");
        }
      }
      else if (args.length < 2) {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "member      = null (using '"+key+"' as array index)");
        }

        int length = Array.getLength(obj);

        if ((index >= 0) || (index < Array.getLength(obj))) {
          if (args.length == 0) {
            return getIndexed(obj, index);
          }
          else {
            return setIndexed(obj, index, args[0]);
          }
        }
        else {
          throw new MidasException("KeyObject: Unable to set array index '"+key+"' in "+getClassName(obj)+
                                   ": Given array has only "+length+" elements numbered 0.."+(length-1)+".");
        }
      }
      else {
        throw new MidasException("KeyObject: Unable to set array index '"+key+"' in "+getClassName(obj)+
                                 " when given "+args.length+" arguments.");
      }
    }

    //// TRY INDEX (TOP LEVEL) /////////////////////////////////////////////////////////////////////
    int keyIndex = getKeyIndex(key,obj); // since 3.5.2, call two param version for BUG 2869

    if (keyIndex >= 0) {
      if ((hardSearchOrder==SearchOrder.GET) && (args.length == 0)) {
        return getIndexed(obj, keyIndex);
      }
      if ((hardSearchOrder==SearchOrder.SET) && (args.length == 1)) {
        return setIndexed(obj, keyIndex, args[0]);
      }
    }

    //// TRY METHOD (HARD) /////////////////////////////////////////////////////////////////////////
    Class<?>[] types      = getClassOf(args);
    boolean    staticOnly = isStatic && !key.equalsIgnoreCase("<init>");

    if (DEBUG_FINDER) {
      debug(debugPrefix, "types       = "+Arrays.toString(types));
    }

    MacroAccessibleObject bestMAO = bestMacroAccessibleObject(debugPrefix,clazz,key,types,staticOnly,hardSearchOrder);

    if (DEBUG_FINDER) {
      debug(debugPrefix, "bestMAO     = "+bestMAO);
    }

    if (bestMAO != null) {
      return invokeMAO(debugPrefix, obj, key, args, clazz, types, bestMAO);
    }

    //// TRY INDEX (NEXT LEVEL) ////////////////////////////////////////////////////////////////////
    keyIndex = (args.length == 0)? -2 : getKeyIndex(args[0].toString());

    if ((keyIndex >= 0) && (hardSearchOrder==SearchOrder.GET)) {
      if (DEBUG_FINDER) {
        debug(debugPrefix, "member      = null (using keyIndex="+keyIndex+")");
      }
      Object val;
      try {
        val = findAndInvoke(debugPrefix+" | ",obj, key, EMPTY_CLASS_ARRAY, SearchOrder.GET, ref);
      }
      catch (Exception e) {
        val = null;
      }
      try {
        if (DEBUG_FINDER) {
          debug(debugPrefix, "val         = "+val);
          debug(debugPrefix, "indexable   = "+isIndexable(val));
          debug(debugPrefix, "keyIndex    = "+keyIndex);
        }
        if (isIndexable(val)) {
          return getIndexed(val, keyIndex);
        }
      }
      catch (Exception e) {
        throw new MidasException("KeyObject: An exception occurred while trying to access "+key+
                                 " via index "+keyIndex, e);
      }
    }

    //// TRY METHOD (SOFT) /////////////////////////////////////////////////////////////////////////
    if (searchOrder != hardSearchOrder) {
      bestMAO = bestMacroAccessibleObject(debugPrefix,clazz,key,types,staticOnly,searchOrder);
      if (DEBUG_FINDER) {
        debug(debugPrefix, "bestMAO     = "+bestMAO);
      }
      if (bestMAO != null) {
        return invokeMAO(debugPrefix, obj, key, args, clazz, types, bestMAO);
      }
    }

    //// TRY CHAINABLE /////////////////////////////////////////////////////////////////////////////
    Object nextLink = (obj instanceof Chainable)? ((Chainable)obj).getNextLink() : null;
    if (nextLink != null) {
      if (DEBUG_FINDER) {
        debug(debugPrefix, "member      = null (using Chainable interface)");
      }
      return findAndInvoke(debugPrefix+" | ",nextLink, key, args, searchOrder, ref);
    }

    //// TRY KEYABLE ///////////////////////////////////////////////////////////////////////////////
    if ((obj instanceof Keyable) || (clazz.isArray())) {
      // Handled above but returned null... not an error.
      return null;
    }

    //// FAILURE ///////////////////////////////////////////////////////////////////////////////////
    if (DEBUG_FINDER) {
      debug(debugPrefix, "member      = null (FAILURE)");
    }
    throw new MidasException("KeyObject: Can not access '"+key+"' in "+getClassName(obj)+
                             " when given "+getTypesList(types)+": No methods found.");
  }

  /** Invokes a MAO for findAndInvoke(..). */
  private static Object invokeMAO (String debugPrefix, Object obj, String key, Object[] args,
                                   Class<?> clazz, Class<?>[] types, MacroAccessibleObject bestMAO) {

    //// ACCESS THE METHOD/FIELD ///////////////////////////////////////////////////////////////////
    Class<?>[] paramTypes = null;
    Object[]   params     = null;
    // TODO: In Java 8 the "if Method" and "if Constructor" blocks can change to...
    //if (bestMAO.accessibleObject instanceof Executable) {
    //  paramTypes = ((Executable)bestMAO.accessibleObject).getParameterTypes();
   //   params     = convertParams(paramTypes, args);
   // }
    if (bestMAO.accessibleObject instanceof Method) {
      paramTypes = ((Method)bestMAO.accessibleObject).getParameterTypes();
      params     = convertParams(paramTypes, args);
    }
    else if (bestMAO.accessibleObject instanceof Constructor) {
      paramTypes = ((Constructor)bestMAO.accessibleObject).getParameterTypes();
      params     = convertParams(paramTypes, args);
    }
    else if ((bestMAO.accessibleObject instanceof Field) && (args.length != 0)) {
      paramTypes = new Class<?>[] { ((Field)bestMAO.accessibleObject).getType() };
      params     = convertParams(paramTypes, args);
    }

    if (DEBUG_FINDER) {
      debug(debugPrefix, "member      = "+bestMAO.accessibleObject);
      debug(debugPrefix, "paramTypes  = "+Arrays.toString(paramTypes));
      debug(debugPrefix, "params      = "+Arrays.toString(params));
    }
    if (bestMAO.accessibleObject instanceof Method) {
      Method method = (Method)bestMAO.accessibleObject;
      try {
        // since 3.1.1: Work around for Java BUG 4071957 (reflect) Method.invoke access control
        // does not understand inner class scoping. For BUG 2552.
        // Otherwise, we get an IllegalAccessException trying to invoke a "public abstract" method
        // from an inner interface (e.g. MJList.CachedDataFile implements ListFile -> RecordFileRW -> RecordFile.getURL())
        // nM> res reg.list2.listfile.url  ! to see problem
        if (Modifier.isAbstract(method.getModifiers())) {
          try {
            method.setAccessible(true); // try to allow access to public abstract methods
          }
          catch (SecurityException se) {
            // ignore and continue (for restricted Web Start sandbox)
          }
        } // end since 3.1.1 for BUG 2552

        return method.invoke(obj, params);
      }
      catch (InvocationTargetException e) {
        throw new MidasException("KeyObject: An exception occurred while trying to run "+method, e.getCause());
      }
      catch (IllegalAccessException | IllegalArgumentException e) {
        throw new MidasException("KeyObject: An exception occurred while trying to run "+method, e);
      }
    }
    else if (bestMAO.accessibleObject instanceof Constructor<?>) {
      Constructor<?> constructor = (Constructor<?>)bestMAO.accessibleObject;
      try {
        return constructor.newInstance(params);
      }
      catch (InvocationTargetException e) {
        throw new MidasException("KeyObject: An exception occurred while trying to run "+constructor, e.getCause());
      }
      catch (IllegalAccessException | IllegalArgumentException | InstantiationException e) {
        throw new MidasException("KeyObject: An exception occurred while trying to run "+constructor, e);
      }
    }
    else if (bestMAO.accessibleObject instanceof Field) {
      Field field = (Field)bestMAO.accessibleObject;
      if (BE_ANNOYING && !Modifier.isStatic(field.getModifiers())
                      && !Modifier.isFinal(field.getModifiers())) {
        // Output a warning message that includes the signature for the required method.
        Shell.warning(getDeprecatedFieldWarning(clazz, field, (args.length == 1)));
      }

      if (args.length > 1) {
        throw new MidasException("KeyObject: Could not access '"+key+"' in "+getClassName(obj)+
                                 " when given "+getTypesList(types)+": Too many parameters given ("+
                                 args.length+") when trying to set "+field);
      }
      else {
        try {
          if (args.length == 0) { //// GET FIELD ///////////////////////////////////////////////
            return field.get(obj);
          }
          else {                  //// SET FIELD ///////////////////////////////////////////////
            field.set(obj, params[0]);
            return null;
          }
        }
        catch (IllegalAccessException | IllegalArgumentException e) {
          throw new MidasException("KeyObject: An exception occurred while trying to "+
                                   ((args.length==1)? "set": "get")+" the value of "+field,e);
        }
      }
    }
    else {
      // This should be impossible at this time.... will add ability to work with constructors in the future
      throw new AssertionError("Variable member must be an instance of either Constructor, Method or Field, "+
                              "given instance of " + toString(bestMAO.accessibleObject.getClass()));
    }
  }

  /** Converts a class to a string and trims off common package names. */
  private static CharSequence getTypesList (Class<?>[] types) {
    if ((types == null) || (types.length == 0)) {
      return "no arguments";
    }
    else if (types.length == 1) {
      return "one argument of type ("+toString(types[0])+")";
    }
    else {
      StringBuilder str = new StringBuilder(40);
      str.append("arguments of type (");
      str.append(toString(types[0]));
      for (int i = 1; i < types.length; i++) {
        str.append(", ");
        str.append(toString(types[i]));
      }
      str.append(")");
      return str;
    }
  }

  /** Converts a class to a string and trims off common package names. */
  private static String getClassName (Object obj) {
    if (obj == null) return "null";
    if (obj instanceof Class) return obj.toString();
    return toString(obj.getClass());
  }

  /** Converts a class to a string and trims off common package names. */
  private static String toString (Class<?> clazz) {
    if (clazz == null) return null;

    String str = clazz.getName();
    for (String packageName : COMMON_PACKAGES) {
      if (str.startsWith(packageName) && (str.indexOf('.', packageName.length()) < 0)) {
        return str.substring(packageName.length());
      }
    }
    return str;
  }

  /** Prints out debug info. */
  @SuppressWarnings("UseOfSystemOutOrSystemErr")
  private static void debug (String debugPrefix, Object obj) {
    Shell.writeln(debugPrefix + obj);
  }


  /** For the given class, return a set of Macro-accessible Fields, Methods, and Constructors.
      Since these datasets are immutable, store them for later reuse.
      @since NeXtMidas 3.7.1
   */
  private static Set<MacroAccessibleObject> allMacroAccessibleObjects (Class<?> clazz) {
    Set<MacroAccessibleObject> allMAOs = allMAOsByClass.get(clazz);
    if (allMAOs == null) {
      allMAOs = MacroAccessibleObject.allInstancesFor(clazz);
      allMAOsByClass.put(clazz, allMAOs);
    }
    return allMAOs;
  }

  /** Identify the single best Macro-accessible Field, Method, or Constructor for the given criteria.
      If there is no good match in the given class, return null and expect Chainable to initiate a
      follow-up search elsewhere.  If two or more candidates are indistinguishable, pick one and
      display a warning about the ambiguity.
      @since NeXtMidas 3.7.1
   */
  private static MacroAccessibleObject bestMacroAccessibleObject (String  debugPrefix, Class<?>    clazz,
                                                                  String  name,        Class<?>[]  types,
                                                                  boolean staticOnly,  SearchOrder searchOrder) {
    final boolean FAST = !EXTRA_DEBUG;

    if (DEBUG_FINDER && EXTRA_DEBUG) {
      debug(debugPrefix, "bestMacroAccessibleObject("+clazz+", '"+name+"', "+Arrays.toString(types)+", "+
            staticOnly+", "+searchOrder+")");
    }

    Set<MacroAccessibleObject>                      allMAOs     = allMacroAccessibleObjects(clazz);
    TreeMap<MatchMetric,Set<MacroAccessibleObject>> rankedMAOs  = new TreeMap<>();
    MatchMetric                                     matchMetric = new MatchMetric(); // reused as much as possible

    for (MacroAccessibleObject mao : allMAOs) {
      if (staticOnly && !mao.hasModifier(Modifier.STATIC)) continue;

      matchMetric.nameMatchMetric = mao.nameMatchMetric(searchOrder, name);
      if (FAST && (matchMetric.nameMatchMetric == Integer.MAX_VALUE)) {
        continue; // not a match
      }
      matchMetric.typeMatchMetric = mao.typeMatchMetric(searchOrder, types);
      if (FAST && (matchMetric.typeMatchMetric == Integer.MAX_VALUE)) {
        continue; // not a match
      }

      Set<MacroAccessibleObject> maoSet = rankedMAOs.get(matchMetric);
      if (maoSet == null) {
        maoSet = new LinkedHashSet<>(4);
        rankedMAOs.put(matchMetric, maoSet);
        matchMetric = new MatchMetric(); // only after this do we need a new instance!
      }
      maoSet.add(mao);
    }

    if (DEBUG_FINDER && EXTRA_DEBUG) {
      debug(debugPrefix, "  Ranked MAOs:");
      for (Map.Entry<MatchMetric,Set<MacroAccessibleObject>> e : rankedMAOs.entrySet()) {
        debug(debugPrefix, "    "+e.getKey());
        for (MacroAccessibleObject m : e.getValue()) {
          debug(debugPrefix, "      "+m);
        }
      }
    }

    if (rankedMAOs.isEmpty()) {
      return null; // None found
    }

    Map.Entry<MatchMetric,Set<MacroAccessibleObject>> firstEntry = rankedMAOs.firstEntry();
    MatchMetric                                       bestMatch  = firstEntry.getKey();
    Set<MacroAccessibleObject>                        bestMAOs   = firstEntry.getValue();

    if (!FAST && ((bestMatch.nameMatchMetric == Integer.MAX_VALUE) ||
                  (bestMatch.typeMatchMetric == Integer.MAX_VALUE))) {
      // This won't take place in normal operation where FAST=true, but may occur if we are
      // debugging and set FAST=false.
      return null; // None found
    }

    if (bestMAOs.size() == 1) {
      // Exactly one match
      return bestMAOs.iterator().next();
    }

    // Multiple matches, so try discarding any fields (i.e. prefer getMWindow() over MW).
    Set<MacroAccessibleObject> bestMethods = new LinkedHashSet<>(4);
    for (MacroAccessibleObject mao : bestMAOs) {
      if (mao.accessibleObject instanceof Field) {
        // ignore
      }
      else {
        bestMethods.add(mao);
      }
    }
    if (bestMethods.size() == 1) {
      return bestMethods.iterator().next();
    }

    StringBuilder errMsg = new StringBuilder(128);
    boolean       first  = true;
    errMsg.append("KeyObject: Ambiguous member definition: found multiple matches for name '");
    errMsg.append(name);
    errMsg.append("': ");

    for (MacroAccessibleObject o : firstEntry.getValue()) {
      if (!first) errMsg.append(", ");
      errMsg.append(o.accessibleObject);
      first = false;
    }

    if (WARN_AMBIGUOUS) {
      Shell.warning(errMsg);
      // Arbitrary return of the first one
      return firstEntry.getValue().iterator().next();
    }
    else {
      throw new MidasException(errMsg.toString());
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // NESTED CLASSES
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** A class for aggregating information about Macro-accessible Fields, Methods, and Constructors.
   *  @since NeXtMidas 3.7.1
   */
  private static class MacroAccessibleObject extends Object implements Comparable<MacroAccessibleObject> {
    final Class<?>           accessibleObjectClass;
    final AccessibleObject   accessibleObject;
    final String             name;
    final Collection<String> aliases;
    final Collection<String> abbreviations;
    final Class<?>[]         types;

    /** Creates a new instance. This should not be called directly; the correct way to get an
     *  instance is via {@link #allInstancesFor(Class)}.
     *  @param accessibleObject The accessible object.
     *  @param name             The primary name.
     *  @param aliases          The aliases.
     *  @param abbreviations    The abbreviations.
     *  @param types            The types.
     */
    @SuppressWarnings("unchecked")
    MacroAccessibleObject (AccessibleObject accessibleObject, String name,
                           Collection<String> aliases, Collection<String> abbreviations,
                           Class<?>[] types) {
      Class<?> aoClass = null;
      if (accessibleObject instanceof Member) {
        aoClass = ((Member)accessibleObject).getDeclaringClass();
      }
      this.accessibleObjectClass = aoClass;
      this.accessibleObject      = accessibleObject;
      this.name                  = name;
      this.aliases               = (aliases       == null)? Collections.EMPTY_LIST : aliases;
      this.abbreviations         = (abbreviations == null)? Collections.EMPTY_LIST : abbreviations;
      this.types                 = types;
    }

    /** Gets all MacroAccessibleObject instances for the given class. */
    static Set<MacroAccessibleObject> allInstancesFor (Class<?> clazz) {
      return allInstancesFor(new TreeSet<MacroAccessibleObject>(), clazz);
    }

    /** Gets all MacroAccessibleObject instances for the given class. */
    private static Set<MacroAccessibleObject> allInstancesFor (Set<MacroAccessibleObject> maoSet, Class<?> clazz) {
      if (Modifier.isPublic(clazz.getModifiers())) {
        for (AccessibleObject o : clazz.getConstructors()) tryAdd(maoSet, o);
        for (AccessibleObject o : clazz.getMethods())      tryAdd(maoSet, o);
        for (AccessibleObject o : clazz.getFields())       tryAdd(maoSet, o);
      }
      else {
        // Go up to get the public methods from the parent classes/interfaces
        if (clazz.getSuperclass() != null) {
          allInstancesFor(maoSet, clazz.getSuperclass());
        }
        for (Class<?> c : clazz.getInterfaces()) {
          allInstancesFor(maoSet, c);
        }
      }
      return maoSet;
    }

    /** Tries to add an accessible object to the set. */
    private static void tryAdd (Set<MacroAccessibleObject> maoSet, AccessibleObject accessibleObject) {
      try {
        add(maoSet, accessibleObject);
      }
      catch (IllegalArgumentException e) {
        // ignore
      }
    }

    /** Adds an accessible object to the set. */
    private static void add (Set<MacroAccessibleObject> maoSet, AccessibleObject accessibleObject) {
      KeyObjectNames     kon     = accessibleObject.getAnnotation(KeyObjectNames.class);
      Collection<String> aliases = (kon == null)? null : Arrays.asList(kon.value());

      if (accessibleObject instanceof Constructor<?>) {
        Constructor<?> constructor = (Constructor<?>)accessibleObject;
        String         name        = "<init>";
        Class<?>[]     types       = constructor.getParameterTypes();

        if (aliases == null) {
          aliases = INIT_ONLY;
        }
        maoSet.add(new MacroAccessibleObject(constructor, name, aliases, null, types));
      }
      else if (accessibleObject instanceof Method) {
        Method             method        = (Method)accessibleObject;
        String             name          = method.getName();
        Class<?>[]         types         = method.getParameterTypes();
        Collection<String> abbreviations = null;
        boolean            isGet         = name.startsWith("get") && (name.length() > 3);
        boolean            isSet         = name.startsWith("set") && (name.length() > 3);
        boolean            isIs          = name.startsWith("is" ) && (name.length() > 2);

        if (aliases == null) {
          aliases = new ArrayList<>(3);
          aliases.add(name);

          if (isGet || isSet || isIs) {
            String baseName = (isIs)? name.substring(2) : name.substring(3);
            String accronym = StringUtil.getAcronym(baseName);

            aliases.add(baseName);
            if (!accronym.isEmpty()) {
              aliases.add(accronym);
            }
          }
        }

        if ((kon == null) && (isGet || isSet || isIs)) {
          abbreviations = new ArrayList<>(4);

          String baseName = (isIs)? name.substring(2) : name.substring(3);

          for (int i = 1; (i <= 4) && (i < baseName.length()); i++) {
            abbreviations.add(baseName.substring(0,i));
          }
        }
        maoSet.add(new MacroAccessibleObject(method, name, aliases, abbreviations, types));
      }
      else if (accessibleObject instanceof Field) {
        Field    field = (Field)accessibleObject;
        Class<?> clazz = field.getDeclaringClass();
        String   name  = field.getName();

        if (aliases == null) {
          aliases = Collections.singleton(name);
        }

        if (lacksGetter(clazz, field)) {
          // to avoid naming ambiguities, only add a virtual getter method if there isn't a real one
          Class<?>[] types = EMPTY_CLASS_ARRAY;
          maoSet.add(new MacroAccessibleObject(field, name, aliases, null, types));
        }
        if (lacksSetter(clazz, field)) {
          // to avoid naming ambiguities, only add a virtual setter method if there isn't a real one and the Field isn't final
          Class<?>[] types = new Class<?>[] { ((Field)accessibleObject).getType() };
          maoSet.add(new MacroAccessibleObject(field, name, aliases, null, types));
        }
      }
      else {
        throw new AssertionError("AccessibleObject "+accessibleObject+" is of unexpected type: "+
                                 accessibleObject.getClass());
      }
    }

    private static boolean lacksGetter (Class<?> clazz, Field field) {
      String getterName = "get"+field.getName(); // prep for case insensitive match
      for (Method method : clazz.getMethods()) {
        if (
          method.getName().equalsIgnoreCase(getterName) &&
          method.getParameterTypes().length==0 &&
          isMatchingType(method.getReturnType(), field.getType(),  true,  true,  true)
        ) return false; // found a getter for this Field
      }
      return true; // need a getter for this Field
    }

    private static boolean lacksSetter (Class<?> clazz, Field field) {
      if (Modifier.isFinal(field.getModifiers())) {
        return false; // no setter possible for a final Field
      }
      String setterName = "set"+field.getName(); // prep for case insensitive match
      for (Method method : clazz.getMethods()) {
        if (
          method.getName().equalsIgnoreCase(setterName) &&
          method.getParameterTypes().length==1 &&
          isMatchingType(method.getParameterTypes()[0], field.getType(),  true,  true,  true)
        ) return false; // found a setter for this Field
      }
      return true; // need a setter for this Field
    }

    public boolean hasModifier (int modifier) {
      return 0 < (modifier & ((Member)this.accessibleObject).getModifiers());
    }

    /** Gets a metric for how well a name matches.
     *  @param searchOrder The search order.
     *  @param baseName    The base name.
     *  @return The metric or {@link Integer#MAX_VALUE} if not a match. Where there is a match,
     *          the smaller the metric, the better the match with 0 being the best possible.
     */
    int nameMatchMetric (SearchOrder searchOrder, String baseName) {
      if (baseName==null) return 0;

      if (accessibleObject instanceof Method) {
        Method method = (Method)accessibleObject;

        if ((searchOrder == SearchOrder.SET) && (method.getName().startsWith("get") ||
                                                 method.getName().startsWith("is"))) {
          return Integer.MAX_VALUE; // Ignore -> Don't let a "get" method map to a SET request
        }
        if ((searchOrder == SearchOrder.GET) && (method.getName().startsWith("set"))) {
          return Integer.MAX_VALUE; // Ignore -> Don't let a "set" method map to a GET request
        }
      }

      SearchOrder _searchOrder = (searchOrder != null)? searchOrder : SearchOrder.GET;
      boolean     abbrOk       = (baseName.length() > 1) && (baseName.length() <= 4);
      String      baseNameTC   = baseName.substring(0,1).toUpperCase() + baseName.substring(1);
      String      fullName1    = _searchOrder.prefix    + baseNameTC;
      String      fullName2    = _searchOrder.alternate + baseNameTC;
      String      baseNameUC   = baseName;

      int metric = Integer.MAX_VALUE; // start with no match
      for (String memberName : aliases) {

        if (memberName.equals(fullName1) || memberName.equals(fullName2)) {
          return 0x00000000; // can't do any better, so just return
        }
        else if (memberName.equalsIgnoreCase(fullName1) || memberName.equalsIgnoreCase(fullName2)) {
          metric = min(metric, 0x01000000);
        }
        else if (memberName.equals(baseName)) {
          metric = min(metric, 0x03000000);
        }
        else if (memberName.equalsIgnoreCase(baseName)) {
          metric = min(metric, 0x04000000);
        }
      }

      if (abbrOk && (metric == Integer.MAX_VALUE)) {
        for (String memberName : abbreviations) {

          if (memberName.equalsIgnoreCase(baseName)) {
            metric = min(metric, 0x05000000+(name.length()-baseNameUC.length()));
          }
        }
      }
      return metric;
    }

    /** Gets a metric for how well a set of types match.
     *  @param searchOrder The search order.
     *  @param thatTypes   The types.
     *  @return The metric or {@link Integer#MAX_VALUE} if not a match. Where there is a match,
     *          the smaller the metric, the better the match with 0 being the best possible.
     */
    int typeMatchMetric (SearchOrder searchOrder, Class<?>[] thatTypes) {
      if (thatTypes == null) thatTypes = EMPTY_CLASS_ARRAY;

      Class<?>[] thisTypes = types;

      if (thisTypes.length != thatTypes.length) {
        return Integer.MAX_VALUE; // no match
      }
      int metric = 0; // start with a perfect match
      for (int i = 0; i < thisTypes.length; i++) {
        Class<?> thisType = getBoxedType(thisTypes[i]);
        Class<?> thatType = getBoxedType(thatTypes[i]);
        boolean  isIF     = thisType.isInterface();

        if (thisType == thatType) {
          metric += 0x00000000;  // perfect match found
        }
        else if (thisType.isAssignableFrom(thatType) && isIF) {
          metric += 0x00000001;  // implements Interface
        }
        else if (thisType.isAssignableFrom(thatType)) {
          metric += 0x00000002;  // extends Class
        }
        else if (isMatchingType(thisType,thatType,false,true,false)) {
          metric += 0x00010000;  // allowConversion
        }
        else if (isMatchingType(thisType,thatType, true,true,false)) {
          metric += 0x00020000;  // allowBoxing AND allowConversion
        }
        else if (isMatchingType(thisType,thatType, true,true, true)) {
          metric += 0x00030000;  // allowBoxing AND allowConversion AND allowMidasConv
        }
        else {
          return Integer.MAX_VALUE; // not a match (at all)
        }
      }
      return metric;
    }

    @Override
    public int compareTo (MacroAccessibleObject that) {
      if (that==null) {
        throw new NullPointerException("Can not compare "+this+" to "+that+".");
      }
      if (that.equals(this)) {
        return 0;
      }

      // Compare name...
      int retval = this.name.compareTo(that.name);
      if (retval!=0) return retval;

      // Compare number of types...
      retval = this.types.length - that.types.length;
      if (retval!=0) return retval;

      // Compare type names...
      int t = 0;
      for (Class<?> thisType : this.types) {
        retval = thisType.getName().compareTo(that.types[t++].getName());
        if (retval!=0) return retval;
      }
      return 0;
    }

    @Override
    public boolean equals (Object object) {
      if (!(object instanceof MacroAccessibleObject)) return false;

      MacroAccessibleObject that = (MacroAccessibleObject)object;
      return (that == this) ||
             (this.accessibleObject.equals(that.accessibleObject) &&
              Arrays.equals(this.types, that.types));
    }

    @Override
    public int hashCode () {
      return accessibleObject.hashCode();
    }

    @Override
    public String toString () {
      return "MacroAccessibleObject(class="+accessibleObjectClass+" accObj="+accessibleObject+
             ", name='"+name+"', aliases="+aliases+", abbreviations="+abbreviations+
             ", types="+Arrays.toString(types)+")";
    }
  }

  /** Metrics for a MacroAccessibleObject match. */
  private static class MatchMetric implements Comparable<MatchMetric> {
    int nameMatchMetric;
    int typeMatchMetric;

    @Override
    public String toString () {
      return "MatchMetric(0x"+Integer.toHexString(nameMatchMetric)+", "+
                         "0x"+Integer.toHexString(typeMatchMetric)+")";
    }

    @Override
    public boolean equals (Object obj) {
      if (!(obj instanceof MatchMetric)) return false;
      MatchMetric that = (MatchMetric)obj;
      return (this.nameMatchMetric == that.nameMatchMetric)
          && (this.typeMatchMetric == that.typeMatchMetric);
    }

    @Override
    public int hashCode () {
      return (nameMatchMetric << 4) ^ typeMatchMetric;
    }

    /** Gets the value used for compareTo(..) comparisons. */
    private long getCompValue () {
      if ((nameMatchMetric == Integer.MAX_VALUE) || (typeMatchMetric == Integer.MAX_VALUE)) {
        // If either is a total failure, the whole thing is.
        return Long.MAX_VALUE;
      }
      return ((long)nameMatchMetric) << 32
           | typeMatchMetric;
    }

    @Override
    public int compareTo (MatchMetric that) {
      return Long.compare(this.getCompValue(), that.getCompValue());
    }
  }
}
