package nxm.sys.intr;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.Keyable;
import nxm.sys.lib.Args;
import nxm.sys.lib.Command;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Terminal;

/**
  Queries an Object for public constructors, methods, fields, and interfaces.

  @author Jeff Schoen/Jay Hallam/Jeanette McCall
*/
public class Query extends Intrinsic {

  /** filter list for constructors, methods, and fields */
  public static final String filterList = "Public,Private,Inherited,Interface,FullSig,Docs";
  private static int F_NONE=0, F_PUBLIC=0x1, F_PRIVATE=0x2, F_INHERIT=0x4, F_INTER=0x8;
  private static int F_FULLSIG=0x10, F_DOCS=0x20;
  private static int F_ALL=0xF;
  private static int F_ACCESS=F_PUBLIC|F_PRIVATE; // access modifiers
  private static int F_INHERIT_INTER=F_INHERIT|F_INTER;
  private static final int DEFAULT_COL_WIDTH = 30;
  private static final int MODIFIER_PUBLIC_PROTECTED = Modifier.PUBLIC|Modifier.PROTECTED;
  private static final String INDENT = "  ";
  private static boolean pureReflectionCodeOnly = true;  // only use code that includes param names with signature

  private int constMask,fieldMask,methodMask;

  private int numRows;
  private int colWidth;
  private String memberName;
  private String listName,listValue;
  private String csName,resName;
  private boolean all, startsWith, exactMatch, resultOnly, userFullSig;

  /** default constructor */
  public Query () {}

  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  public int process () {
    listValue=null;

    Class<?> clas=null;
    Object object=null;

    // JDK 7+ uses CSS for Javadocs, so Query cannot properly render them (since 3.3.1 - Bug 2653)
    boolean jdk7plus = (StringUtil.compareVersions("1.7.0", Shell.getJavaVersion()) <= 0);

    // Get parameters
    csName     = MA.getCS("NAME");  // If case-sensitive value is needed
    resName    = MA.getU("NAME"); 	// If needed as a result name
    listName   = MA.getU("/LIST",null);  // If needed as a result name
    memberName = MA.getCS("METH");
    all        = MA.getState("/ALL");

    boolean pureReflectionCode = pureReflectionCodeOnly || (MA.getLength("/PARAMS") >= 0);
    resultOnly = listName!=null;

    int defMask = (pureReflectionCode && !resultOnly) ? F_PUBLIC|F_FULLSIG|F_INHERIT : F_PUBLIC|F_INHERIT;
    if (all) defMask |= (F_PRIVATE | F_INTER);
    defMask = MA.getOptionMask("/MASK",filterList,defMask);
    userFullSig = (MA.getOptionMask("/MASK",filterList,0) & F_FULLSIG) != 0;

    if (jdk7plus) defMask &= ~F_DOCS; // docs not supported in JDK7+
    fieldMask = constMask = methodMask = defMask;

    if (MA.getState("/GETS")) { memberName = "GET*"; constMask = fieldMask = F_NONE; }
    if (MA.getState("/SETS")) { memberName = "SET*"; constMask = fieldMask = F_NONE; }
    if (MA.getState("/FIELDS")) { methodMask = constMask = F_NONE; }

    methodMask = MA.getOptionMask("/M",filterList,methodMask);
    fieldMask  = MA.getOptionMask("/F",filterList,fieldMask);
    constMask  = MA.getOptionMask("/C",filterList,constMask);

    // setup terminal output more filter
    colWidth = MA.getL("/NC",DEFAULT_COL_WIDTH); // Number of cols in more filter
    numRows  = MA.getL("/NR",M.terminal.getRows());  // Number of rows in more filter
    if (MA.find("/NR")) M.terminal.setRows(numRows); // Number of rows in more filter

    // new jdk8+ pure reflection code
    if (pureReflectionCode) {
      try {
        queryPureReflection();
      }
      catch (Exception e) {M.warning("New Query via Reflection code Exception, please report "+e);}
      return FINISH;
    }
	// limited functionality in jdk7+
    if (jdk7plus) {
      fieldMask = constMask = methodMask = F_ALL;
    }

    if (MA.getState("/DEBUG")) {
      System.out.println("Query Name="+csName+" matche="+memberName+" MethodMask="+methodMask
                        +" FieldMask="+fieldMask+" all="+all);
    }

    // get the object
    if (csName.length()==0) {                     // no arg
      M.error("No object specified");
    } else if ((object=MR.get(resName))!=null) {   // try result
      clas = object.getClass();
    } else {
      try {                                       // try class name
        clas = Class.forName(csName);
      } catch (Exception e) {                     // try command arg
        Args targs = Shell.parseCommand(M,csName);
        Command command = Shell.loadCommand(M,targs);
        switch(command.support) {
          case 'P': clas = command.getClass(); object = command; break;
          case 'I': clas = command.getClass(); object = command; break;
          default:  clas = getClass(csName); break;
        }
      }
    }

    if (clas==null) {
      M.error("Query Object "+csName+" not found");
    } else {
        String fileName  = getJavaDocsFilename(clas.getName().trim());
        if (fileName == null) fileName = "API location.";
        M.info("For best viewing, view API in web browser at "+fileName);
      if (jdk7plus) {  // for JDK 7+, let the user know the functionality is limited and point them to API web page
        M.warning("Use of QUERY with JDK 7+ has limited functionality.");
      }
    }

    // perform the query
    if (listName!=null) {
      doQueryClass (clas, memberName);
      MR.put(listName,listValue);
    }
    else {
      doQuery(clas, object);
    }

    return FINISH;
  }

  /** This option (potential fix) is a work in prgress */
  private void queryPureReflection () {
    if (!resultOnly) M.more(Terminal.INIT);
    else listValue = "";
    // get the Class we are querying
    Class<?> clazz=null;
    if (csName.length()==0) {                                   // no arg
      M.error("No object specified");
    }
    Object result = MR.get(resName);

    if (result!=null) {                // try result
      clazz = result.getClass();
    } else {
      try {                                                     // try class name
        clazz = Class.forName(csName);
      } catch (Exception e) {                                   // try command arg
        Args targs = Shell.parseCommand(M,csName);
        Command command = Shell.loadCommand(M,targs);
        switch(command.support) {
          case 'P': clazz = command.getClass(); break;
          case 'I': clazz = command.getClass(); break;
          default:  clazz = getClass(csName);   break;
        }
      }
    }

    if (clazz==null) { M.error("Query Object "+csName+" not found"); }

    setupMatchingRequirements();
    processLinkInChain (clazz, result, "ClassName");

    if (!resultOnly) M.more(Terminal.EXIT);
    else {
      if (listValue!="") listValue = listValue.substring(0, listValue.length()-1);
      MR.put(listName,listValue);
    }
  }

  private void setupMatchingRequirements () {
    int iAsterisk = memberName.indexOf('*');
    if (iAsterisk==0) { // grab everything
      startsWith = exactMatch = false;  // global matching
    }
    else if (iAsterisk>0) {
      startsWith = true;
      exactMatch = false;
      memberName = memberName.substring(0,iAsterisk).toLowerCase();
      constMask = F_NONE; // finding a member, not a constructor
    }
    else {
      if ("<init>".equals(memberName.toLowerCase())) {
        // special case, just want constructors
        methodMask = fieldMask = F_NONE;
        constMask |= F_PUBLIC;
        startsWith = exactMatch = false; // want all constructors
      }
      else {
        startsWith = false;
        exactMatch = true;
        memberName = memberName.toLowerCase();
        constMask = F_NONE;
      }
    }
  }

  private void processLinkInChain (Class<?> clazz, Object object, String title) {
    Object nextObj = nextChainedObject(object);

    if (!resultOnly)  classInheritanceViaReflection(clazz,title);
    if (Keyable.class.isAssignableFrom(clazz)) {
      if (!resultOnly) M.more(" *** Class implements Keyable, setKey and getKey may override methods ***");
    }
    if (constMask   != F_NONE)           constructorsViaReflection(clazz);
    if (methodMask  != F_NONE)           methodsViaReflection(clazz);
    if ((methodMask&F_INHERIT_INTER)!=0) inheritedMethodsViaReflection(clazz);
    if (fieldMask   != F_NONE)           fieldsViaReflection(clazz);

    if ((!resultOnly || exactMatch) && (nextObj != null)) {
      if (!resultOnly) M.more("");
      processLinkInChain(nextObj.getClass(), nextObj, "Chained to Class");
    }
  }

  private void classInheritanceViaReflection (Class<?> clazz, String title) {
    String implement = "";
    Class<?>[] interfaceArray = clazz.getInterfaces();
    if (interfaceArray.length!=0) {
      implement = Arrays.toString(interfaceArray).replaceAll("interface ","");
      implement = " implements " + implement.substring(1, implement.length()-1);
    }
    String extend = "";
    Class<?> superClass = clazz.getSuperclass();
    if ((superClass!=null) && (java.lang.Object.class!=superClass)) {
      extend = " extends "+superClass.toString().replace("class ","");
    }
    String listing = clazz.toString()+extend+implement;
    if (!resultOnly) M.more(title+" -> "+listing);
  }

  private void constructorsViaReflection (Class<?> clazz) {
    String cnstrHeader = "Constructors - all declared with options "+Parser.mask2s(filterList,constMask&F_ACCESS);
    boolean listPublicProtected = (constMask&F_PUBLIC) != 0;
    boolean listDefaultPrivate  = (constMask&F_PRIVATE) != 0;
    Constructor<?>[] cnstrs = clazz.getDeclaredConstructors();
    boolean fullSignature = (constMask&F_FULLSIG)!=0;
    if (cnstrs.length>0) {
      if (!resultOnly) M.more("\n"+cnstrHeader);
      boolean firstConstr = true;
      String cname = "";
      for (Constructor<?> cnstr : cnstrs) {
        if (firstConstr) {
          cname = cnstr.getName();
          int lastDot = cname.lastIndexOf('.');
          cname = cname.substring(lastDot+1); // we already have package information, so strip that
          firstConstr = false;
        }
        int modifiers = cnstr.getModifiers();
        if ((modifiers&MODIFIER_PUBLIC_PROTECTED)!=0) {
          if (!listPublicProtected) continue;
        }
        else if (!listDefaultPrivate) continue;
        if (fullSignature) {
          // Modifier portion
          String modStr = Modifier.toString(modifiers);
          if (modStr!="") modStr += " ";
          // Signature portion
          String signature = getSignatureListing(cname, cnstr.getParameters());
          String listing =  modStr + signature;
          if (!resultOnly) M.more(INDENT+listing);
          else listValue += listing + ",";
        }
        else {
          String listing = cname;
          if (!resultOnly) M.more(INDENT+listing,colWidth);
          else listValue += listing + ",";
        }
      }
      if (!resultOnly && !fullSignature) M.more(Terminal.FLUSH);
    }
  }

  private void methodsViaReflection (Class<?> clazz) {
    String methodHeader = "Methods - all declared with options "+Parser.mask2s(filterList,methodMask&F_ACCESS);
    boolean listPublicProtected = (methodMask&F_PUBLIC) != 0;
    boolean listDefaultPrivate  = (methodMask&F_PRIVATE) != 0;
    ArrayList<String> namesToSort = new ArrayList<>(); // only use when not listing full signature
    try {
      Method[] mthds = clazz.getDeclaredMethods();
      if (mthds.length>0) {
        if (!resultOnly) M.more("\n"+methodHeader);
        for (Method mthd : mthds) {
          int modifiers = mthd.getModifiers();
          if ((modifiers&MODIFIER_PUBLIC_PROTECTED)!=0) {
            if (!listPublicProtected) continue;
          }
          else if (!listDefaultPrivate) continue;
          String mname = mthd.getName();
          if (startsWith && !mname.toLowerCase().startsWith(memberName)) continue;
          if (exactMatch && !mname.toLowerCase().equals(memberName)) continue;
          if ((methodMask&F_FULLSIG)!=0) {
            // Modifier portion
            String modStr = Modifier.toString(modifiers);
            if (modStr!="") modStr += " ";
            // Signature portion
            String signature = getSignatureListing(mname, mthd.getParameters());
            // return type portion
            String rtype = mthd.getReturnType().toString();
            rtype = rtype.replace("class ", "");
            String listing = modStr + rtype + " " + signature;
            if (!resultOnly) M.more(INDENT+listing);
            else listValue += listing + ",";
          }
          else {
            namesToSort.add(mname);
          }
        }
        if (!namesToSort.isEmpty()) sortAndListNames(namesToSort);
      }
    }
    catch (Exception e) {M.warning("New Query via Reflection Method code Exception, please report "+e);}
  }

  private String getSignatureListing (String name, Parameter[] params) {
    String paramStr = "";
    if (params.length>0) {
      paramStr = Arrays.toString(params);
      paramStr = paramStr.substring(1,paramStr.length()-1);
      paramStr = paramStr.replaceAll("class ","");
    }
    return name + "(" + paramStr + ")";
  }

  private void fieldsViaReflection (Class<?> clazz) {
    String fieldHeader = "Fields - all declared with options "+Parser.mask2s(filterList,fieldMask&F_ACCESS);
    boolean needHeader = !resultOnly;
    boolean listPublicProtected = (fieldMask&F_PUBLIC) != 0;
    boolean listDefaultPrivate  = (fieldMask&F_PRIVATE) != 0;
    ArrayList<String> namesToSort = new ArrayList<>(); // only use when not listing full signature
    Field[] fields = clazz.getDeclaredFields();
    if (fields.length>0) {
      for (Field fld : fields) {
        int modifiers = fld.getModifiers();
        if ((modifiers&MODIFIER_PUBLIC_PROTECTED)!=0) {
          if (!listPublicProtected) continue;
        }
        else if (!listDefaultPrivate) continue;
        // field is one that matches access modifiers in mask
        String fname = fld.getName();
        if (startsWith && !fname.toLowerCase().startsWith(memberName)) continue;
        if (exactMatch && !fname.toLowerCase().equals(memberName)) continue;
        if (needHeader) {M.more("\n"+fieldHeader); needHeader = false;}
        if ((fieldMask&F_FULLSIG)!=0) {
          // Modifier portion
          String modStr = Modifier.toString(modifiers);
          if (modStr!="") modStr += " ";
          // Type portion
          String ftype = fld.getType().toString();
          ftype = ftype.replace("class ", "");
          // Value if static final
          String value = "";
          if (((modifiers&Modifier.FINAL)!=0) && ((modifiers&Modifier.STATIC)!=0)) {
            fld.setAccessible(true);
            try {
              value = " = "+fld.get(null).toString();
            }
            catch (Exception e) {}
          }
          String listing = modStr + ftype + " " + fname + value;
          if (!resultOnly) M.more(INDENT+listing);
          else listValue += listing + ",";
        }
        else {
          namesToSort.add(fname);
        }
      }
      if (!namesToSort.isEmpty()) sortAndListNames(namesToSort);
    }
  }

  private void inheritedMethodsViaReflection (Class<?> clazz) {
    String methodHeader = "Inherited Methods - declared with options "+Parser.mask2s(filterList,methodMask&F_INHERIT_INTER);
    boolean needHeader = !resultOnly;
    boolean notListInherited = (methodMask&F_INHERIT) == 0;
    boolean notListInterface = (methodMask&F_INTER)   == 0;
    ArrayList<String> namesToSort = new ArrayList<>(); // only use when not listing full signature
    try {
      Method[] mthds = clazz.getMethods();
      if (mthds.length>0) {
        for (Method mthd : mthds) {
          Class<?> mthdClass = mthd.getDeclaringClass();
          if (mthdClass.equals(clazz)) continue;
          if (java.lang.Object.class==mthdClass) continue;
          boolean isInterface = mthdClass.isInterface();
          if (notListInherited && !isInterface) continue;  // is inherited and not listing inherited
          // always list 'default' interface methods, but list other interface methods only if that option is selected
          if (notListInterface && isInterface && !mthd.isDefault()) continue;
          String mname = mthd.getName();
          if (startsWith && !mname.toLowerCase().startsWith(memberName)) continue;
          if (exactMatch && !mname.toLowerCase().equals(memberName)) continue;
          if (needHeader) {M.more("\n"+methodHeader); needHeader=false;}
          if (userFullSig) {
            // Modifier portion
            int modifiers = mthd.getModifiers();
            String modStr = Modifier.toString(modifiers);
            // Signature portion
            String signature = getSignatureListing(mname, mthd.getParameters());
            // Return type portion
            String rtype = mthd.getReturnType().toString().replace("class ", "");
            String listing = modStr + " " + rtype + " " + signature;
            if (!resultOnly) M.more(INDENT+listing);
            else listValue += listing + ",";
          }
          else {
            namesToSort.add(mname);
          }
        }
        if (!namesToSort.isEmpty()) sortAndListNames(namesToSort);
      }
    }
    catch (Exception e) {M.warning("New Query via Reflection Inherited Method code Exception, please report "+e);}
  }

  private void sortAndListNames (ArrayList<String> namesToSort) {
    if (!namesToSort.isEmpty()) {
      // we will go ahead and sort these, since we are just listing method names
      String[] sortedNames = namesToSort.toArray(new String[namesToSort.size()]);
      Arrays.sort(sortedNames);
      for (String listing : sortedNames) {
        if (!resultOnly) M.more(INDENT+listing,colWidth);
        else listValue += listing + ",";
      }
    }
    if (!resultOnly) M.more(Terminal.FLUSH);
  }

  /** Gets the next Object down the chain from the given Object
      @param object the Object to be analyzed
      @return the next Object down the chain from the given Object
   */
  public Object nextChainedObject (Object object) {
    Object nextObject = null;
    if (object instanceof Chainable) {
      nextObject = ((Chainable)object).getNextLink();
    }
    return nextObject;
  }

  /** Use trial new reflection code only
   *  @param enable use new reflection code only
   */
   public static void setPureReflectionCodeOnly (boolean enable) {
     pureReflectionCodeOnly = enable;
   }

  /** Are we using only the new pure reflection code
    * @return true if using only the trial code
    */
  public static boolean isPureReflectionCodeOnly () {
    return Query.pureReflectionCodeOnly;
  }

  /** Get the API filename for the class being queried
      @param fileName String form of the class name
      @return String with path to API (e.g. /nmroot/htdocs/api/sys/nxm/sys/lib/Table.html
      @since NeXtMidas 3.3.1 (added for Bug 2653)
   */
  private String getJavaDocsFilename (String fname) {
    // this code moved from getJavadocs() so it can be used in process() also
    if (fname==null) return null;
    String fileName = fname;
    if (fileName.startsWith("nxm.")) {
      final String opt = fileName.substring(4, fileName.indexOf('.', 4));
      fileName = Shell.getNmRoot()+
          "htdocs" + java.io.File.separator +
          "api"    + java.io.File.separator +
          opt      + java.io.File.separator +
          fileName;
      fileName  = fileName.replace('.', java.io.File.separatorChar) + ".html";
    } else {
      fileName = null;
    }
    return fileName;
  }

  /** Handles the query.
      @param clas   The class.
      @param object The object (used when finding chainable methods).
   */
  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  public void doQuery (Class<?> clas, Object object) {
    M.terminal.more(Terminal.INIT);
    doQuery(clas, object, "ClassName");
    M.terminal.more(Terminal.EXIT);
  }

  /** Handles the query. Used only by doQuery(..).
      @param clas   The class.
      @param object The object.
      @param title  The title of the class ("ClassName" or "Chained to Class").
   */
  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  private void doQuery (Class<?> clas, Object object, String title) {
    Object nextObj = nextChainedObject(object);

    M.more(title+" -> "+clas.getName());  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
    if (Keyable.class.isAssignableFrom(clas)) {
      M.more(" *** Class implements Keyable, setKey and getKey may override methods ***");
    }
    doQueryClass (clas, memberName);
    M.more(Terminal.FLUSH);

    if (nextObj != null) {
      M.more("");
      doQuery(nextObj.getClass(), nextObj, "Chained to Class");
    }
  }

  /** Lists all the constructors, methods and fields.
      @param clazz   The class.
      @param name    Name of the members to show ("*" for all).
   */
  @SuppressWarnings({"unchecked", "deprecation"})
  private void doQueryClass (Class<?> clazz, String name) {
    String constHeader  = "Constructors - all declared with options "+Parser.mask2s(filterList,constMask);
    String methodHeader = "Methods - all declared with options "+Parser.mask2s(filterList,methodMask);
    String fieldHeader  = "Fields - all declared with options "+Parser.mask2s(filterList,fieldMask);

    List<Constructor<?>> consts  = new ArrayList<>();
    List<Method> methods = new ArrayList<>();
    List<Field> fields = new ArrayList<>();

    if (name.indexOf("*")>=0) {

      /* Add the public ones */
      consts.addAll(Arrays.asList(clazz.getConstructors()));
      methods.addAll(Arrays.asList(clazz.getMethods()));
      fields.addAll(Arrays.asList(clazz.getFields()));

      /* Add the private, protected ones. */
      consts.addAll(  (List<Constructor<?>>) removePublic(clazz.getDeclaredConstructors()));
      methods.addAll( (List<Method>) removePublic(clazz.getDeclaredMethods()));
      fields.addAll(  (List<Field>) removePublic(clazz.getDeclaredFields()));

      /* Sort */
      String[] sortedConsts = sortNames(clazz,consts.toArray(new Member[0]),  name, constMask);
      String[] sortedMethods= sortNames(clazz,methods.toArray(new Member[0]), name, methodMask);
      String[] sortedFields = sortNames(clazz,fields.toArray(new Member[0]),  name, fieldMask);

      /* List everything */
      listInfo(constHeader,  sortedConsts,  clazz, constMask);
      listInfo(methodHeader, sortedMethods, clazz, methodMask);
      listInfo(fieldHeader,  sortedFields,  clazz, fieldMask);
    }
    else if (clazz.equals(nxm.sys.lib.Table.class) || clazz.equals(nxm.sys.lib.KeyVector.class)) {
      M.more(" *** Only getKey and setKey can be used ***");  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      M.more(Terminal.FLUSH);
    }
    else if (clazz.isArray()) {
      M.more(" *** Only getSize() and array access methods can be used ***");  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      M.more(Terminal.FLUSH);
    }
    else {
      String     key     = (name.equals("*"))? null: name;
      Class<?>[] types   = null;
      Member[]   members = KeyObject.findMembers(clazz, key, types, false);

      for (Member member : members) {
        if (member instanceof Constructor) consts.add((Constructor<?>)member);
        else if (member instanceof Method) methods.add((Method)member);
        else if (member instanceof Field)  fields.add((Field)member);
      }

      /* Sort */
      String[] sortedConsts = sortNames(clazz,consts.toArray(new Member[0]),  name, constMask);
      String[] sortedMethods= sortNames(clazz,methods.toArray(new Member[0]), name, methodMask);
      String[] sortedFields = sortNames(clazz,fields.toArray(new Member[0]),  name, fieldMask);

      /* List everything */
      listInfo(constHeader,  sortedConsts,  clazz, constMask);
      listInfo(methodHeader, sortedMethods, clazz, methodMask);
      listInfo(fieldHeader,  sortedFields,  clazz, fieldMask);
    }

  }

  /** Filters out all public members.
      @param members The items.
      @return A list of the non-public members.
   */
  private static List<? extends Member> removePublic (Member[] members) {
    List<Member> list = new ArrayList<>();

    for (Member member: members) {
      if (!Modifier.isPublic(member.getModifiers())) {
        list.add(member);
      }
    }

    return list;
  }


  /** Lists the information about a set of constructors/methods/fields.
      @param header The header.
      @param list   The list of the ones to list.
      @param clazz  The relevant class.
   */
  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  private void listInfo (String header, String[] list, Class<?> clazz, int mask) {

    if (mask==0) return;

    if (listName!=null) {
      for (int i=0; i<list.length; i++) {
        if (listValue==null) listValue=list[i];
        else listValue = listValue+","+list[i];
      }
      return;
    }

    M.more(" " + header + " (" + list.length + ") -> ");  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813

    for (int i=0; i<list.length; i++) {
      /* List the name */
      M.more("  "+list[i],colWidth);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
    }
    M.more(Terminal.FLUSH);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
  }

  private static Class<?>[] getTypes (String args) {
    Class<?>[] params = null;

    args = args.trim();

    if (args.startsWith("(") && args.endsWith(")")) {
      args = args.substring(1,args.length()-1);
    }

    String[] paramStrs=args.split(",");

    if ((paramStrs.length == 1) && (paramStrs[0].trim().length() == 0)) {
      params = new Class<?>[0];

    } else {
      params = new Class<?>[paramStrs.length];

      for (int i = 0; i < params.length; i++) {
        params[i] = getClass(paramStrs[i].trim());

        if (params[i] == null) {
          Shell.warning("Unknown type '" + paramStrs[i] + "'.");
          params = null;
          break;
        }
      }
    }

    return params;
  }

  /** Load and sort an array of Methods, Fields or Classes into an ascending list of String names.
      @param objArray   The list of members to sort.
      @param memberName The name of the member to get.
      @param mask       The mask of filter options.
      @return The sorted list of members that match the given name.
   */
  private static String[] sortNames (Class<?> clazz, Member[] objArray, String memberName, int mask) {
    // Load an array of Strings with the names
    ArrayList<String> nameList = new ArrayList<>();

    final boolean all     = memberName.equals("*");         /* List all */
    final boolean exact   = (memberName.indexOf('(') >= 0); /* Exact match */
    final boolean fullSig = (mask&F_FULLSIG)!=0;

    int istar = memberName.indexOf("*");
    if (istar>0) memberName = memberName.substring(0,istar); /* match up to the "*" */

    /* Get the list of param names if provided. */
    Class<?>[] params = null;
    if (exact) {
      final int pStart = memberName.indexOf('(');
      final int pEnd   = memberName.indexOf(')');
      if (pEnd > 0) {
        params = getTypes(memberName.substring(pStart+1,pEnd));
      }
      memberName = memberName.substring(0, pStart).trim();
    }

    final String memberNameUC = memberName.toUpperCase();
    final String memberNameCS = memberName;

    try {
      for (int i = 0; i < objArray.length; i++) {
        Member member =  objArray[i];
        String sig  = member.toString();      // The member signature
        String name = member.getName();       // The member name
        int mods    = member.getModifiers();  // The member modifiers
        String signame = fullSig? sig : name;
        boolean isInherited = member.getDeclaringClass() != clazz;
        boolean isInterface = isInherited && member.getDeclaringClass().isInterface();
        if (isInterface) isInherited = false;

        if (member instanceof Constructor) name = "<init>";

             if (Modifier.isPrivate(mods) && (mask&F_PRIVATE)==0);
        else if (Modifier.isPublic(mods) && (mask&F_PUBLIC)==0);
        else if (isInherited && (mask&F_INHERIT)==0);
        else if (isInterface && (mask&F_INTER)==0);
        else if (all) {
          nameList.add(signame);
        }
        else if ((member instanceof Field) && name.equals(memberNameCS)) {
          nameList.add(signame);
        }
        else if (exact && name.equals(memberNameCS)) {
          if (params != null) {
            Class<?>[] methParams = null;
            if (member instanceof Method) {
              methParams = ((Method)member).getParameterTypes();
            }
            else if (member instanceof Constructor) {
              methParams = ((Constructor<?>)member).getParameterTypes();
            }
            else {
              nameList.add(sig);
              break;
            }
            if (methParams.length == params.length) {
              boolean match = true;
              for (int j = 0; j < params.length; j++) {
                match = match && params[j].equals(methParams[j]);
              }
              if (match) nameList.add(signame);
            }
          } else {
            nameList.add(signame);
          }
        }
        else if ((!exact) && name.toUpperCase().startsWith(memberNameUC)) {
          nameList.add(signame);
        } else {
          /* Not a Match */
        }
      }
    } catch(IllegalArgumentException e) {
      Shell.warning("Can not load an array of " + objArray.getClass()); // stack trace not needed
    }

    String sortedList[] = nameList.toArray(new String[0]);
    java.util.Arrays.sort(sortedList); // Sort the list of names

    return sortedList;
  }

  /** Gets the class for a given name. <br>
      If given the name of a primitive the class representing that primitive will be
      returned. If given a name that is not fully resolved (like "String") it will try to
      resolve it (to "java.lang.String") from a set of known packages.
      @param str The name of the class.
      @return The class or null if not found.
   */
  private static Class<?> getClass (String str) {
    final String[] PACKAGE_SEARCH = { "", /* No Package */
                                      "java.lang.", "java.util.",
                                      "nxm.sys.inc.", "nxm.sys.lib.",
                                      "nxm.sys.libg.", "nxm.sys.libm.",
                                      "nxm.sys.net." };
    Class<?> clazz = null;

    /* Convert primitives to their wrapper classes. */
    if (str.equals("long"   )) clazz = java.lang.Long.TYPE;
    if (str.equals("int"    )) clazz = java.lang.Integer.TYPE;
    if (str.equals("short"  )) clazz = java.lang.Short.TYPE;
    if (str.equals("char"   )) clazz = java.lang.Character.TYPE;
    if (str.equals("byte"   )) clazz = java.lang.Byte.TYPE;
    if (str.equals("double" )) clazz = java.lang.Double.TYPE;
    if (str.equals("float"  )) clazz = java.lang.Float.TYPE;
    if (str.equals("boolean")) clazz = java.lang.Boolean.TYPE;


    /* Get the class based on the name, prefix the known packages if
       not initially found.
     */
    for (int i = 0; (clazz == null) && (i < PACKAGE_SEARCH.length); i++) {
      try {
        clazz = Class.forName(PACKAGE_SEARCH[i] + str);

      } catch(Exception e) {
        /* Ignore */
      }
    }

    return clazz;
  }
}
