package nxm.xbc.lib;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

import nxm.sys.inc.DataTypes;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileResource;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.IOResource;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.Keywords.Key;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Native;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.lib.XmUtil;

/** A collection of wrapper methods called from the native (Fortran/C/C++)
    M$ and m_ calls into the NeXtMidas APIs (Java).<br>
    <br>
    Note that all of the methods called from native code are marked package-private and take in
    a "long st" as the first argument (this is used for generating stack traces in the native code).
    <br>
    Internal utility methods are private. The only public method are for internal use by XBC and SYS
    only. The methods in this class are subject to change or removal without notice. <br>
    <br>
    Methods that mimic a functionality in X-Midas keep the X-Midas name (e.g. put_keydata(..) maps
    to m$put_keydata which is called via nm_put_keydata in xmlibc.c). These methods typically have
    little or no documentation here since their functionality is documented in the X-Midas API.

    @author  J. Czechowski IV
    @since   NeXtMidas 2.5.1
 */
public class NmJackets {

  /* The following flags match X-Midas and are used ONLY by getKeyDataJNI */
  private static final int MK_Normal    =   0; // Normal, mode=0
  private static final int MK_Main      =   1; // Access main header only.
  private static final int MK_Ext       =   2; // ? (JC4: Access extended header only?)
  private static final int MK_Noscope   =   4; // Override current scope.
  private static final int MK_Next      =   8; // Get next key. Use key name="**INIT**" to init.
  private static final int MK_Typeasis  =  16; // Keep type as in header.
  private static final int MK_Typetag   =  32; // Convert type and prefix key name with "<type>:".
  private static final int MK_Replace   =  64; // Replace existing keyword if it matches.
  private static final int MK_Index     = 128; // Use hcb.ext_next as an index into keyword array.
  private static final int MK_Nextmatch = 256; // ?
  private static final int MK_Last      = 512; // ?

  private Keywords.Iterator jniKWI = null; // Iterator used from JNI by getKeyDataJNI.
  private HPrimitive HP;
  private int        ldirectoryContext = -1;   // Context counter for m_ldirectory (-1=NotReset)
  private DataFile[] openFiles;
  private int        openFilesLength;

  /** Basic constructor for the class. This is only used from {@link HPrimitive}. */
  NmJackets (HPrimitive HP) {
    this.HP = HP;
  }

  /** Handle an exception caught in here. */
  private void handle (long st, Throwable t) {
    try {
      printStackTrace(st,t);
      HP.setInterrupt(1);

    } catch (Throwable e) {
      // Note this has special handling since if we catch an error here, something is
      // very wrong and we need to print directly to the terminal.
      System.err.println("Fatal error in "+getClass());
      e.printStackTrace();
      System.err.println("Subsequent to "+t);
      t.printStackTrace();
    }
  }

  /** Print an exception caught in here. */
  private void printStackTrace (long st, Throwable t) {
    String[] nativeStack = HPrimitive.getStackTrace(st);

    if ((nativeStack != null) && (nativeStack.length > 0)) {
      t.setStackTrace(mergeStackTraces(t.getStackTrace(), nativeStack));
    }
    HP.M.printStackTrace(t);
  }

  /** Merges a new stack trace with a native stack trace. This is only called from handle(..). */
  private StackTraceElement[] mergeStackTraces (StackTraceElement[] oldStack, String[] nativeStack) {

    StackTraceElement[] newStack = oldStack;
    try {
      int index = -1;

      // Find insertion index
      String clazz = HPrimitive.class.getName();
      for (int i = 0; (i < oldStack.length) && (index == -1); i++) {
        if (oldStack[i].getClassName().equals(clazz) && oldStack[i].getMethodName().equals("exec")) {
          index = i;
        }
      }

      // Insert native stack into the stack trace
      if (index != -1) {
        newStack = new StackTraceElement[oldStack.length + nativeStack.length];
        System.arraycopy(oldStack, 0,     newStack, 0, index);
        System.arraycopy(oldStack, index, newStack, index+nativeStack.length, oldStack.length-index);
        for (int i = 0; i < nativeStack.length; i++) {
          int    openp  = nativeStack[i].indexOf('(');
          int    closep = nativeStack[i].indexOf(')');
          String fname  = (openp < 0)? "" : nativeStack[i].substring(0, openp);
          String method = (openp < 0)? "" : nativeStack[i].substring(openp+1,closep);
          int    plus   = method.indexOf('+');
          int    slash  = fname.lastIndexOf('/');

          if (plus  > 0) method = method.substring(0,plus);
          if (slash > 0) fname  = fname.substring(slash+1);

          if (method.length() == 0) method = "<unknown>";
          if (fname.length()  == 0) method = "<unknown>";

          newStack[i+index] = new StackTraceElement(clazz, method, fname, -1); // use Java 5 API
        }
      }
    } catch (Exception e) {
      //e.printStackTrace();
      newStack = oldStack;
    }
    return newStack;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use MA
  //////////////////////////////////////////////////////////////////////////////////////////////////
  String  apick (long st, int i) { try { return HP.MA.getS(i); } catch (Throwable t) { handle(st,t); return null;  } }
  String  upick (long st, int i) { try { return HP.MA.getU(i); } catch (Throwable t) { handle(st,t); return null;  } }
  double  dpick (long st, int i) { try { return HP.MA.getD(i); } catch (Throwable t) { handle(st,t); return 0;     } }
  int     lpick (long st, int i) { try { return HP.MA.getL(i); } catch (Throwable t) { handle(st,t); return 0;     } }
  boolean spick (long st, int i) { try { return HP.MA.getZ(i); } catch (Throwable t) { handle(st,t); return false; } }
  /** Gets the command line parameter at a given position as a time value (double[]).
     @return double[3], where double[0] is the precision (0 if entry is not present,
             2 for picosecond [1e-12] precision, never 1 for microsecond [1e-6]
             precision since the {@link Time} class provides picosecond precision).
             double[1] is the whole seconds, and double[2] is the fractional seconds.
     @since NeXtMidas 2.7.0
   */
  double[] tpick (long st, int i) {
    double results[] = {0,0,0}; // Initialize results[]
    try {
      Time t = HP.MA.getTime(i);
      if (t != null) {
        results[1] = t.getWSec(); // whole seconds
        results[2] = t.getFSec(); // fractional seconds
        results[0] = 2; // Since Time class provides picosecond precision, hence never 1
      }
    } catch (Throwable t) { handle(st,t); }
    return results;
  }

  boolean ppick (long st, int i) {
    try {
      // X-Midas does not treat a switch like a parameter
      if (i>HP.MA.numberOf()) return false;
      String key = HP.MA.getKey(i);
      return HP.MA.isPresent(key);
    } catch (Throwable t) {
      handle(st,t);
      return false;
    }
  }

  String apickpost (long st, String arg, int flag) {
    try { return HP.MA.resolveString(arg,flag); }
    catch (Throwable t) { System.out.println("KKKK"); handle(st,t); return null;  }
  }

  double  get_dswitch (long st, String s) { try { return HP.MA.getD(s); } catch (Throwable t) { handle(st,t); return 0;     } }
  int     get_switch  (long st, String s) { try { return HP.MA.getL(s); } catch (Throwable t) { handle(st,t); return 0;     } }
  boolean get_sswitch (long st, String s) { try { return HP.MA.getZ(s); } catch (Throwable t) { handle(st,t); return false; } }

  boolean get_pswitch (long st, String s) {
    try {
      return HP.MA.isPresent(s);
    } catch (Throwable t) {
      handle(st,t);
      return false;
    }
  }

  String  get_uswitch_def (long st, String s, String  d) { try { return HP.MA.getS(s,d); } catch (Throwable t) { handle(st,t); return null;  } }
  double  get_dswitch_def (long st, String s, double  d) { try { return HP.MA.getD(s,d); } catch (Throwable t) { handle(st,t); return 0;     } }
  int     get_switch_def  (long st, String s, int     d) { try { return HP.MA.getL(s,d); } catch (Throwable t) { handle(st,t); return 0;     } }
  boolean get_sswitch_def (long st, String s, boolean d) { try { return HP.MA.getZ(s,d); } catch (Throwable t) { handle(st,t); return false; } }

  int number_parameters (long st) {
    try {
      return HP.MA.getNArgs();
    } catch (Throwable t) {
      handle(st,t);
      return 0;
    }
  }

  int pipe_mode (long st) {
    try {
      return HP.M.pipeMode;
    } catch (Throwable t) {
      handle(st,t);
      return 0;
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use MR
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets a result.
      @param key    The result name.
      @return null on error or if the result is not found, otherwise returns the value packed into
              the buffer where buf[0] is the type and buf[1] to buf[buf.length-1] is the bytes
              representing the value of the result.
   */
  byte[] get_result (long st, String key) {
    try {
      byte[] buf  = null;
      Object res  = HP.MR.get(key);
      Data   data = null;

      if (res instanceof Table) {
        // ignore possible global tables that X-Midas shouldn't be aware of
        return null;
      }

      if (res != null) {
        if(res instanceof Data) {
          data = (Data)res;
        } else {
          data = new Data(res);
        }

        if ((data.type == DataTypes.ASCII) || (data.type == DataTypes.STRING)) {
          buf = ("A" + data.toString()).getBytes();
        }
        else if (data.type == DataTypes.ASCII256) {  // 3.9.4 NXM-3570 - TODO could add test, but is a Data object
          buf = ("Z" + data.toString()).getBytes();
        }
        else {
          buf = new byte[data.buf.length+1];
          buf[0] = data.type;
          System.arraycopy(data.buf, 0, buf, 1, buf.length-1);
        }
      }
      return buf;
    } catch (Throwable t) {
      handle(st,t);
      return null;
    }
  }

  /** Puts a result.
      @param key    The result name.
      @param itype  The result type ('D','F','A',etc).
      @param buffer The result.
      @return Number of bytes stored or -1 if key is invalid, -2 on all other errors.
   */
  int put_result (long st, String key, byte itype, byte[] buffer) {
    try {
      int bytes = -2;
      if ((DataTypes.DATA_TYPE_LIST.indexOf((char)itype) < 0) && (itype != ' ')) {
        HP.M.warning("Can not set result type '"+(char)itype+"' from JNI code.");
        bytes = -2;
      }
      else if ((key == null) || (key.length() < 1)) {
        bytes = -1;
      }
      else if (Data.isString(itype)) {
        Data val = new Data(new String(buffer),itype==DataTypes.ASCII256);
        HP.MR.put(key, val);
        bytes = buffer.length;
      }
      else if (itype == ' ') { // Used by M$STR2RESULT
        //Data val = new Data(new String(buffer)); // Does not work, see Bug 328
        HP.MR.put(key, new String(buffer));
        bytes = buffer.length;
      }
      else {
        int  ele = (buffer.length / Data.getBPS(itype));
        Data val = Data.fromBuffer(buffer, Shell.rep, 0, ele, itype);
        HP.MR.put(key, val);
        bytes = ele * Data.getBPS(itype);
      }
      return bytes;
    } catch (Throwable t) {
      handle(st,t);
      return -2;
    }
  }

  /** Removes a result. */
  void del_result (long st, String key) {
    try {
      HP.MR.remove(key);
    } catch (Throwable t) {
      handle(st,t);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use M
  //////////////////////////////////////////////////////////////////////////////////////////////////

  void info      (long st, String msg) { try { HP.M.message(msg,Midas.INFO,     HP); } catch (Throwable t) { handle(st,t); } }
  void warning   (long st, String msg) { try { HP.M.message(msg,Midas.WARNING,  HP); } catch (Throwable t) { handle(st,t); } }
  void deprecate (long st, String msg) { try { HP.M.message(msg,Midas.DEPRECATE,HP); } catch (Throwable t) { handle(st,t); } }
  void error     (long st, String msg) { try { HP.M.message(msg,Midas.ERROR,    HP);
                                               HP.setInterrupt(1);
                                             } catch (Throwable t) { handle(st,t); } }

  int getPipeMode  (long st) { try { return HP.M.pipeMode; } catch (Throwable t) { handle(st,t); return 0; } }
  int getHostState (long st) { try { return HP.hostState;  } catch (Throwable t) { handle(st,t); return 0; } }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use MQ
  //   -- Currently in HPrimitive... should move them here in the future.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  int get_msg (long st, long a, long b, int c, float d, int e) {
    return HP.getXmMsg(a,b,c,d,e);
  }

  int put_msge(long st, long a, long b, int c, int d) {
    return HP.putXmMsg(a,b,c,d);
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that are in HPrimitive
  //   -- Currently in HPrimitive... should move them here in the future.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  void runGC (long st) {
    try {
      System.gc();
    } catch (Throwable t) {
      handle(st,t);
    }
  }

  String command_name (long st) {
    try {
      return HP.getName();
    } catch (Throwable t) {
      handle(st,t);
      return null;
    }
  }

  /** Gets the command ID if it is a number, otherwise -1. */
  int id (long st) {
    try {
      String id = HP.getID();
      return (StringUtil.isInteger(id))? Convert.s2l(id) : -1;
    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  void setHostState (long st, int i) {
    try {
      HP.setHostStateJNI(i);
    } catch (Throwable t) {
      handle(st,t);
    }
  }

  /** Searches for a string in the cache. This method takes in the characters for a string from the
      native code (up to 16 characters long) with 0 values for all extra fields.
      @return The pointer to the cached string or 0 if a new one needs to be allocated.
   */
  long getString (long st, byte c0, byte c1, byte c2, byte c3, byte c4, byte c5, byte c6, byte c7,
                           byte c8, byte c9, byte cA, byte cB, byte cC, byte cD, byte cE, byte cF) {
    try {
      return HP.getStringJNI(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, cA, cB, cC, cD, cE, cF);
    } catch (Throwable t) {
      handle(st,t);
      return 0;
    }
  }

  /** Puts a string in the cache. If passed a non-string the pointer will be returned unmodified.
      If passed a string and the cache is not full the string will be added to the cache and 0 will
      be returned. If the cache is full the string will be added to the cache and a pointer to the
      oldest element in the cache (which is now being dropped from the cache) will be returned.
      @param obj     The object.
      @param pointer The C pointer to the object.
      @return The pointer to the object to delete.
   */
  long putString (long st, Object obj, long pointer) {
    try {
      return HP.putStringJNI(obj, pointer);
    } catch (Throwable t) {
      handle(st,t);
      return 0;
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use Convert, StringUtil, or Sedit
  //////////////////////////////////////////////////////////////////////////////////////////////////

  double s2d (long st, String s) { try { return Convert.s2d(s); } catch (Throwable t) { handle(st,t); return 0; } }

// JC4: Consider adding these...
//  String subs (long st, String in_str, String string1, String string2) {
//    try {
//      return StringUtil.replaceFirst(in_str, string1, string2);
//
//    } catch (Throwable t) {
//      handle(st,t);
//      return null;
//    }
//  }
//
//  String gsub (long st, String in_str, String string1, String string2) {
//    try {
//      return StringUtil.replaceAll(in_str, string1, string2);
//
//    } catch (Throwable t) {
//      handle(st,t);
//      return null;
//    }
//  }

  /** Pass-through to Sedit.head(String) for M$HEAD implementation.
      @since NeXtMidas 3.1.2
   */
  String sedit_head (long st, String s) { try { return FileUtil.head(s); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.tail(String) for M$TAIL implementation.
      @since NeXtMidas 3.1.2
   */
  String sedit_tail (long st, String s) { try { return FileUtil.tail(s); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.root(String) for M$ROOT implementation.
      @since NeXtMidas 3.1.2
   */
  String sedit_root (long st, String s) { try { return FileUtil.root(s); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.ext(String) for M$EXT implementation.
      @since NeXtMidas 3.1.2
   */
  String sedit_ext (long st, String s) { try { return FileUtil.ext(s); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.trim(String) for M$TRIMS implementation.
      @since NeXtMidas 3.1.2
  */
  String sedit_trims (long st, String s, String p1, String p2) { try { return StringUtil.trim(s, p1, p2); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.getSubStringBetween(String) for M$BETWEEN implementation.
      @since NeXtMidas 3.1.2
  */
  String sedit_between (long st, String s, int min, int max) { try { return StringUtil.between(s, min, max); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.nelements(int) for M$NELEMENTS implementation.
      @since NeXtMidas 3.5.0
  */
  int sedit_nelements (long st, String s, String d) { try { return StringUtil.nelements(s, d); } catch (Throwable t) { handle(st,t); return 0; } }

  /** Pass-through to Sedit.getWordByElement(String) for M$ELEMENT implementation.
      @since NeXtMidas 3.5.0
  */
  String sedit_element (long st, String s, int n, String d) { try { return StringUtil.getWordByElement(s, n, d); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.getWordByString(String) for M$WORD implementation.
  @since NeXtMidas 3.5.0
  */
  String sedit_word (long st, String s, String w) { try { return StringUtil.getWordByString(s, w); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to Sedit.getOccurrenceOf(int) for M$NSEARCH implementation.
  @since NeXtMidas 3.5.0
  */
  int sedit_nsearch (long st, String s, String w, int n) { try { return StringUtil.getOccurrenceOf(s, w, n); } catch (Throwable t) { handle(st,t); return 0; } }

  /** Pass-through to Sedit.getStringInRange(String) for M$RANGE implementation.
  @since NeXtMidas 3.5.0
  */
  String sedit_range (long st, String s, int a, int b) { try { return StringUtil.getStringInRange(s, a, b); } catch (Throwable t) { handle(st,t); return null; } }

  /** Pass-through to String.startsWith(boolean) for M$STARTSWITH implementation.
  @since NeXtMidas 3.5.0
  */
  boolean sedit_startswith (long st, String s1, String prefix) { try { return s1.startsWith(prefix); } catch (Throwable t) { handle(st,t); return false; } }

  /** Pass-through to String.endsWith(boolean) for M$ENDSWITH implementation.
  @since NeXtMidas 3.5.0
  */
  boolean sedit_endswith (long st, String s1, String suffix) { try { return s1.endsWith(suffix); } catch (Throwable t) { handle(st,t); return false; } }

  /** Pass-through to Sedit.nform(String) for M$NFORMAT implementation.
  @since NeXtMidas 3.5.0
  */
  String sedit_nformat (long st, String sdata, String format) { try { return StringUtil.nform(Double.parseDouble(sdata), format); } catch (Throwable t) { handle(st,t); return null; } }
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use Time
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** m$now implementation */
  double[] now (long st) {
    double retTime[] = {0,0}; // Initialize retTime[]
    try {
      Time curTime = Time.currentTime();
      retTime[0] = curTime.getWSec(); // whole seconds
      retTime[1] = curTime.getFSec(); // fractional seconds
    } catch (Throwable t) { handle(st,t); }
    return retTime;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use Keywords
  //   -- Currently in HPrimitive... should move them here in the future.
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Translates X-Midas scope flags to NeXtMidas. Used ONLY by getKeyDataJNI and putKeyDataJNI */
  private int getJniScope(int mode) {
    if ((mode & MK_Main   ) != 0) return Keywords.MAIN;
    if ((mode & MK_Ext    ) != 0) return Keywords.EXTENDED;
    if ((mode & MK_Noscope) != 0) return Keywords.SCOPE;
    return Keywords.SCOPE;
  }


  byte[] get_keydata (long st, int id, byte[] key, int mode, byte type) {
    try {
      Keywords kw      = getKeywords(id);
      byte[]   buf     = null;
      String   keyName = Convert.unpackS(key, 0, key.length);
      Key      val     = null;

      if ((mode & MK_Index) != 0) {
        Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Index flag not supported at this time.");

      } else if ((mode & MK_Nextmatch) != 0) {
        Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Nextmatch flag not supported at this time.");

      } else if ((mode & MK_Last) != 0) {
        Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Last flag not supported at this time.");

      } else if ((mode & MK_Next) != 0) {
        if (keyName.equals("**INIT**")) {
          jniKWI = kw.iterator();
        } else if (jniKWI == null) {
          /* This would cause X-Midas to occasionally crash... but we can do better. */
          Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Next flag used before **INIT**.");
          jniKWI = kw.iterator();
        }

        if (jniKWI.hasNext()) {
          val = jniKWI.next();
        }
        // JC4: This should set hcb.ext_next <= if found in main header, > 0 extended header.
      } else{
        val = kw.find(keyName, getJniScope(mode));
      }

      if (val != null) {
        kw.extractName(val);
        kw.extractData(val);

        Object data = val.value;
        if ((mode & MK_Typetag) != 0) {
          keyName = "" + val.getType() + ":" + val.name;
          data    = Convert.o2o(val.value, (char)type, null);
        } else if ((mode & MK_Typeasis) != 0) {
          type = (byte)val.getType();
        } else if ((mode & MK_Next) != 0) {
          keyName = val.name; // Using the iterator overwrites key name with new name.
        }

        if (type == DataTypes.ASCII) {
          buf = ("A" + Convert.o2s(data)).getBytes();

        } else {
          Parser parser = new Parser(data.toString());
          int    nele   = parser.elements();
          int    bps    = Data.getBPS(type);
          int    buflen = bps*nele+1;
          int    offset = 1; // Byte 0 is type char.

          buf = new byte[buflen];

          for (int i = 1; i <= nele; i++) {
            String str = parser.get(i);
            switch(type) {
              case DataTypes.DOUBLE: Convert.packD(buf, offset, Convert.o2d(str)); break;
              case DataTypes.FLOAT:  Convert.packF(buf, offset, Convert.o2f(str)); break;
              case DataTypes.XLONG:  Convert.packX(buf, offset, Convert.o2x(str)); break;
              case DataTypes.LONG:   Convert.packL(buf, offset, Convert.o2l(str)); break;
              case DataTypes.INT:    Convert.packI(buf, offset, Convert.o2i(str)); break;
              case DataTypes.BYTE:   Convert.packB(buf, offset, Convert.o2b(str)); break;
              default:  Shell.getSharedMidasContext().warning("Keywords: Unknown keyword type '"+(char)type+"'.");
            }
            offset+=bps;
          }
        }
        buf[0] = type;
      }

      Arrays.fill(key, (byte)' ');
      for(int i = 0; i < keyName.length() && i < key.length; i++) {
        key[i] = (byte)keyName.charAt(i);
      }
      return buf;

    } catch (Throwable t) {
      handle(st,t);
      return null;
    }

  }

  /** Gets the value of a key. This is ONLY used by XBC from JNI.
      @param key  The name of the key (note that this may be modified).
      @param mode The mode flags.
      @param type The buffer type.
      @return The number of bytes put (-1 on error).
   */
  int put_keydata (long st, int id, byte[] key, byte[] buf, int mode, byte type) {
    try {
      Keywords kw = getKeywords(id);
      int bytes = -1;

      String keyName = Convert.unpackS(key, 0, key.length);;
      int    flags   = Keywords.ADD;

      if ((mode & MK_Replace) != 0) {
        flags = Keywords.REPLACE;
      }

      if ((mode & MK_Main) != 0) {
        kw.setScope("MAIN");
      } else if ((mode & MK_Ext) != 0) {
        kw.setScope("EXT");
      } else if ((mode & MK_Noscope) != 0) {
        /* This is never used in the X-Midas baseline, so just ignore it for now. */
        Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Noscope flag not supported at this time.");
      }

      if ((mode & MK_Index) != 0) {
        Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Index flag not supported at this time.");

      } else {
        bytes = buf.length;
        kw.set(keyName, type, buf, bytes, flags);
      }
      return bytes;

    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  /** Gets the value of a key. This is ONLY used by XBC from JNI.
      @param key  The name of the key (note that this may be modified).
      @param mode The mode flags.
      @return The number of bytes removed (-1 if not found).
   */
  int del_keydata (long st, int id, byte[] key, int mode) {
    try {
      Keywords kw      = getKeywords(id);
      int      bytes   = -1;
      String   keyName = Convert.unpackS(key, 0, key.length);;
      byte[]   buf     = null;

      if ((mode & MK_Main) != 0) {
        kw.setScope("MAIN");
      } else if ((mode & MK_Ext) != 0) {
        kw.setScope("EXT");
      } else if ((mode & MK_Noscope) != 0) {
        /* This is never used in the X-Midas baseline, so just ignore it for now. */
        Shell.getSharedMidasContext().warning("Keywords: X-Midas MK_Noscope flag not supported at this time.");
      }

      buf = get_keydata(st, id, key, mode, DataTypes.ASCII);
      if(buf != null) {
        bytes = buf.length;
        kw.delete(keyName);
      } else {
        bytes = -1;
      }
      return bytes;

    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  int scope_keyword (long st, int id, String scope) {
    try {
      return getKeywords(id).setScope(scope);
    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // AUX Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  private static final int MAX_AUX = 127; // Max aux number.

  /** Gets the list of AUX entries used to populate the fields in Mc.
      @return { writeAux, readAux1, readAux2, ..., readAuxN } or null on error. (Never returns an
              array of length 0.)
   */
  int[] getAuxEntries (long st) {
    try {
      String   writeAux = HP.MR.getS("AUX.WRITE");
      String[] readAux  = HP.M.io.getReadAux();
      int[]    entries  = new int[readAux.length + 1];

      entries[0] = XmUtil.getAuxNumber(writeAux);
      for (int i = 0; i < readAux.length; i++) {
        entries[i+1] = XmUtil.getAuxNumber(readAux[i]);
      }
      return entries;

    } catch (Throwable t) {
      handle(st,t);
      return null;
    }
  }

  int valid_aux (long st, int num) {
    try {
      int valid;
      if ((num == 0) || (num == 99) || (num == 98)) {
        valid = 2;
      }
      else if (num < 0) {
        valid = -1;
      }
      else if (num > MAX_AUX) {
        try {
          XmUtil.getAuxName(num); // Throws exception if not defined
          valid = 4;
        } catch (Exception e) {
          valid = -4;
        }
      }
      else {
        valid = 0;
        String name = XmUtil.getAuxName(num).toUpperCase();
        if (Shell.getAuxPath(name) != null) valid = 1;
      }
      return valid;

    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  String get_aux_path (long st, int num) {
    try {
      return Shell.getAuxPath(Integer.toString(num));
    } catch (Throwable t) {
      handle(st,t);
      return null;
    }
  }

  int add_read_aux (long st, int num, int pos) {
    try {
      String[] readAux = HP.M.io.getReadAux();
      String   name    = XmUtil.getAuxName(num,false);
      int      status;

      if ((name == null) || (Shell.getAuxPath(name) == null)) {
        status = -2;
      } else {
        boolean found = false;
        for (int i = 0; !found && (i < readAux.length); i++) {
          found = readAux[i].equals(name);
        }
        if (found) {
          status = 0;
        } else {
          status = 1;

          if (pos == 0)     pos = readAux.length;
          else if (pos < 0) pos = Math.max(0, readAux.length+pos);
          else              pos = Math.min(readAux.length, pos) - 1;

          StringBuffer read = new StringBuffer();
          int i = 0;
          for (; i < pos; i++)            read.append('|').append(readAux[i]);
                                          read.append('|').append(name);
          for (; i < readAux.length; i++) read.append('|').append(readAux[i]);
          setReadAux(read);
        }
      }
      return status;

    } catch (Throwable t) {
      handle(st,t);
      return -2;
    }
  }

  int del_read_aux (long st, int num) {
    try {
      String[] readAux = HP.M.io.getReadAux();
      String   name    = XmUtil.getAuxName(num,false);
      int      status;

      if ((name == null) || (Shell.getAuxPath(name) == null)) {
        status = -2;
      } else {
        status = 0;

        StringBuffer read = new StringBuffer();
        for (int i = 0; i < readAux.length; i++) {
          if (readAux[i].equals(name)) {
            status = 1;
          } else {
            read.append('|').append(readAux[i]);
          }
        }
        setReadAux(read);
      }
      return status;

    } catch (Throwable t) {
      handle(st,t);
      return -2;
    }
  }

  void clear_read_auxlist (long st) {
    try {
      HP.MR.put("AUX.READ", "");

    } catch (Throwable t) {
      handle(st,t);
    }
  }

  int define_aux (long st, int num, String path) {
    try {
      String name  = null;
      int    valid = -2;

      if ((num == 0) || (num == 98) || (num == 99)) {
        valid = -3;
      }
      else if (num < 0) {
        valid = -2;
      }
      else if (num > MAX_AUX) {
        try {
          name = XmUtil.getAuxName(num); // Throws exception if not defined
        } catch (Exception e) {
          valid = -4;
        }
      }
      else {
        name = Integer.toString(num);
      }

      if (name != null) {
        name  = XmUtil.checkAuxName(name);
        valid = (Shell.getAuxPath(name) != null)? 2 : 1;

        if (path.length() > 0) { // Add/Set AUX
          HP.MR.put("AUX."+name, path);
        }
        else {                   // Remove AUX
          if (valid == 2) HP.MR.remove("AUX."+name);
        }
      }
      return valid;

    } catch (Throwable t) {
      handle(st,t);
      return -2;
    }
  }

  /** Sets the read aux. Used by del_read_aux(..) and add_read_aux(..) */
  private void setReadAux (StringBuffer read) {
    if (read.length() > 0) {
      HP.MR.put("AUX.READ", read.substring(1)); // Start at 1 to remove initial ','
    } else {
      HP.MR.put("AUX.READ", "");
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that use DataFile
  //   See m_open(..) in xmlibc.c for discussion about the meaning of id= and hcb->lph=.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets a DataFile given the applicable ID. */
  private synchronized DataFile getDataFile (int id) {
    // The order of the next three checks is important!
    if (id == 0) {
      throw new MidasException("Attempting to access uninitialized DataFile (ID="+id+"). This is "
                             + "probably due to accessing a file before it has been opened.");
    }
    else if ((openFiles == null) || (id >= openFiles.length) ||  (id <= -openFiles.length)) {
      // Just junk in the HCB (or someone playing with hcb->lph), this should be rare.
      throw new MidasException("Attempting to access uninitialized DataFile (ID="+id+").");
    }
    else if (id < 0) {
      throw new MidasException("Attempting to access uninitialized DataFile (ID="+id+"). This is "
                             + "probably due to accessing a file after it has been closed.");
    }

    DataFile df = openFiles[id];
    if (df == null) {
      // This condition should be extremely rare. It represents a case where NmJacket.close(..)
      // was called but either it or m$close(..) did not complete normally (m$close sets id=-id
      // after the close).
      throw new MidasException("Attempting to access DataFile (ID="+id+") after it has been closed.");
    }
    return df;
  }

  /** Clears out the DataFile with the given ID (this can be called more than once). */
  private synchronized DataFile remDataFile (int id) {
    DataFile df = getDataFile(id); // <-- Throws exception if id is invalid
    openFiles[id] = null;          // <-- id is ok, remove it from array
    return df;
  }

  /** Adds a new DataFile returning the applicable ID. */
  private synchronized int addDataFile (DataFile df) {
    if (openFiles == null) {
      // Never use id=0, always go id=1..N. We do this for two reasons: (1) Fortran initializes the
      // entire HCB to 0 (so we don't want to use that value), and (2) we now set id=-id on M$CLOSE
      // to allow us to find cases where a file is read after a close was done. As such, openFiles[0]
      // is never used (always null), this decision was intentional since it allows us to still use
      // openFiles[id] rather than openFiles[id-1].
      openFilesLength = 1;
      openFiles = new DataFile[8];
    } else if (openFilesLength >= openFiles.length) {
      DataFile[] temp = new DataFile[openFiles.length + 8];
      System.arraycopy(openFiles, 0, temp, 0, openFiles.length);
      openFiles = temp;
    }
    int id = openFilesLength;
    openFiles[id] = df;
    openFilesLength++;

    return id;
  }

  /** Gets a DataFile's Keywords given the applicable ID. */
  private Keywords getKeywords (int id) {
    return getDataFile(id).getKeywordsObject();
  }

  String test_format (long st, String format, String formatList) {
    try {
      return HP.MA.testFormat(format, formatList);
    } catch (Throwable t) {
      handle(st,t);
      return null;
    }
  }

  boolean existence (long st, byte[] fname, int dir) {
    try {
      String jname = xmFileNameToNm(new String(fname));
      IOResource io = HP.M.io.findResource(jname, dir);
      jname = toJniUrl(io.getURL());

      for (int i = 0; i < fname.length; i++) {
        fname[i] = (byte)((i < jname.length())? jname.charAt(i): ' ');
      }
      return io.exists();

    } catch (Throwable t) {
      handle(st,t);
      return false;
    }
  }

  int alloc (long st) {
    try {
      DataFile df = new DataFile();
      df.initMain();
      return addDataFile(df);
    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  int init (long st, int id, String filename, String types, String formats, int flags) {
    if (id <= 0) id = alloc(st); // reusing an old HCB
    try {
      DataFile df = getDataFile(id);
      String   fname = xmFileNameToNm(filename);
      df.init(HP, fname, types, formats, flags|DataFile.NATIVE);
      return id;
    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  /** Copier for XBC's Fortran HCB blocks - used for JNI */
  void copyMainX (long st, long phcb) {
    try {
      DataFile df = new DataFile();
      df.initMain();
      copyMain(df,phcb,1);
    } catch (Throwable t) {
      handle(st,t);
    }
  }

  /** Copier for XBC's Fortran HCB blocks - used for JNI */
  void copyMain (long st, int id, long phcb, int dir) {
    try {
      DataFile df = getDataFile(id);
      copyMain(df,phcb,dir);
    } catch (Throwable t) {
      handle(st,t);
    }
  }

  /** Wrapper to {@link DataFile#propagate(DataFile, int)}.
      @since NeXtMidas 3.7.1
   */
  int propagateExtKeywords (long st, int idSrc, int idDest) {
    try {
      getDataFile(idDest).propagate(getDataFile(idSrc), DataFile.PM_KEYWORDSEXT);
      return 0;
    } catch (Throwable t) {
      handle(st,t);
      return -idSrc;
    }
  }

  /** Wrapper to {@link DataFile#read(long, int, int, boolean)}. This will
      automatically convert data from the file's data rep to the local
      machine's data rep if they are different. */
  int read (long st, int id, long lbuf, double offset, int bytes) {
    try {
      DataFile df = getDataFile(id);
//      Data data = df.getDataBuffer(elements);
//      int  nele = df.read(data);
//      Native.ja2p(data.buf, 0, lbuf, 0, Math.min(bytes,data.buf.length));
//      int  nele = df.read(lbuf,0,bytes);
//      return elements;
      if (offset>=0) df.seek(offset); // negative offset reads at current seek position
      return df.read(lbuf, 0, bytes, true);
    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  /** Wrapper to {@link DataFile#write(long, int, int, boolean)}. This will
      automatically convert data from the local machine's data rep to the
      file's data rep if they are different. */
  int write (long st, int id, long lbuf, double offset, int bytes) {
    try {
      DataFile df = getDataFile(id);
//      Data data = df.getDataBuffer((int)(bytes/df.getBPE()));
//      Native.p2ja(lbuf, 0, data.buf, 0, Math.min(bytes,data.buf.length));
//      return df.write(data);
      if (offset>=0) df.seek(offset); // negative offset writes at current seek position
      return df.write(lbuf,0,bytes, true);
    } catch (Throwable t) {
      handle(st,t);
      return -1;
    }
  }

  /** adds a control widget */
  void winit (long st, int cid, String key) {
    try {
      if (HP.M.macro != null) HP.M.macro.addControlWidget(HP,cid,key);
    } catch (Throwable t) {
      handle(st,t);
    }
  }

  /** Special getURL method for use by XBC. */
  String getURL (long st, int id) {
    try {
      DataFile df = getDataFile(id);
      return toJniUrl(df.getURL());
    } catch (Throwable t) {
      handle(st,t);
      return null;
    }
  }

  boolean open      (long st, int id)           { try { return getDataFile(id).open();    } catch (Throwable t) { handle(st,t); return false; } }
  void    close     (long st, int id)           { try { remDataFile(id).close();          } catch (Throwable t) { handle(st,t);               } }
  void    setDFS    (long st, int id, int n)    { try { getDataFile(id).setDFS(n);        } catch (Throwable t) { handle(st,t);               } }
  void    flush     (long st, int id)           { try { getDataFile(id).flush();          } catch (Throwable t) { handle(st,t);               } }
  int     getBPS    (long st, int id)           { try { return getDataFile(id).getBPS();  } catch (Throwable t) { handle(st,t); return 0;     } }
  int     getSPA    (long st, int id)           { try { return getDataFile(id).getSPA();  } catch (Throwable t) { handle(st,t); return 0;     } }
  int     getAPE    (long st, int id)           { try { return getDataFile(id).getAPE();  } catch (Throwable t) { handle(st,t); return 0;     } }
  double  getBPE    (long st, int id)           { try { return getDataFile(id).getBPE();  } catch (Throwable t) { handle(st,t); return 0;     } }
  double  getSize   (long st, int id)           { try { return getDataFile(id).getSize(); } catch (Throwable t) { handle(st,t); return 0;     } }
  double  avail     (long st, int id)           { try { return getDataFile(id).avail();   } catch (Throwable t) { handle(st,t); return 0;     } }
  int     skip      (long st, int id, int n)    { try { return getDataFile(id).skip(n);   } catch (Throwable t) { handle(st,t); return 0;     } }
  int     seek      (long st, int id, double d) { try { return getDataFile(id).seek(d);   } catch (Throwable t) { handle(st,t); return 0;     } }
  double  getSeek   (long st, int id)           { try { return getDataFile(id).seek();    } catch (Throwable t) { handle(st,t); return 0;     } }

  /** Copier for XBC's Fortran HCB blocks - used for JNI */
  private void copyMain (DataFile df, long phcb, int dir) {
    if (dir>0) {
      Native.ja2p(getNativeHeader(df),0,phcb,0,512);
    } else if (dir<0) {
      byte[] buf = new byte[512];
      Native.p2ja(phcb,0,buf,0,buf.length);
      setNativeHeader(df,buf);
    } else {
      throw new MidasException("NmJackets.copyMain: Invalid direction, "+dir+".");
    }
  }

  /** Gets the header in native format. */
  private byte[] getNativeHeader (DataFile df) {
    byte from = (byte)df.getHeadRep().charAt(0);
    byte to   = DataFile.testRep(Shell.osrep);
    byte[] buf;

    if (to == from) {
      buf = df.hb;
    } else {
      buf = new byte[512];
      System.arraycopy(df.hb, 0, buf, 0, buf.length);
      df.convertHeaderRep(buf, from, to);
    }
    return buf;
  }

  /** Sets the header based on a buffer in native format (note that buf will be modified). */
  private void setNativeHeader (DataFile df, byte[] buf) {
    byte from = DataFile.testRep(Shell.osrep);
    byte to   = (byte)df.getHeadRep().charAt(0);

    if (to != from) df.convertHeaderRep (buf, from, to);
    System.arraycopy(buf, 0, df.hb, 0, buf.length);
  }

  /** Converts a normal URL to one that can be used by XBC. */
  private String toJniUrl (String url) {
    if (url == null) { url = ""; }

    if (url.indexOf(':') > 2) { /* Allow for a drive letter on Windows. */
      if (url.toLowerCase().startsWith("file:")) {
        url = url.substring(5);
      } else {
        HP.M.warning("DataFile: File name for '"+url+"' is a remote URL.");
      }
    }

    if (url.length() > 80) {
      HP.M.warning("DataFile: File name for '"+url+"' will be truncated to 80 characters.");
      url = url.substring(0, 80);
    }
    return url;
  }

  /** Converts a file name with X-Midas qualifiers to use NeXtMidas qualifiers. */
  private String xmFileNameToNm (String filename) {
    String fname = filename.trim();
    Table  quals = new Table();
    String trim  = null;
    int    index = fname.indexOf('{');

    if (index > 0) {
      /* Keep the NeXtMidas-style qualifiers. */
      int end = StringUtil.getClosingFensePos(fname,"{}",index);

      if (end < index) {
        HP.M.warning("DataFile: Missing '}' in "+filename);
      } else {
        quals = new Table(fname.substring(index, end+1));
        fname = fname.substring(0,index) + fname.substring(end+1);
      }
    }

    index = fname.indexOf('(');
    if (index > 0) {
      String opts  = fname.substring(index).trim();
      fname = fname.substring(0,index).trim();

      while(opts.length() > 0) {
        index = StringUtil.getClosingFensePos(opts,"()",1);

        if (index < 0) {
          HP.M.warning("DataFile: Missing ')' in "+filename);
          break;
        } else {
          String thisOpt = opts.substring(1,index).trim();
          opts = opts.substring(index+1).trim();
          index = thisOpt.indexOf('=');
          if (index > 0) {  /* Qualifier */
            quals.put(thisOpt.substring(0,index).trim().toUpperCase(),
                      thisOpt.substring(index+1).trim());
          } else {     /* Trimmer */
            if (trim == null) {
              trim = "("+thisOpt+")";
            } else {
              HP.M.warning("DataFile: Multiple trimmers found in "+filename);
            }
          }
        }
      }
    }
    if (trim != null)   fname += trim;
    if (quals.size()>0) fname += quals;

    return fname;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Other
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Supports the m$ldirectory method.
      @param path     (IN)    The Auxiliary path name.
      @param fname    (IN)    Root file name (can contain wildcards).
      @param ext      (IN)    File name extension (can contain wildcards).
      @param fullname (OUT)   Fullname returned (one entry).
      @param i        (I/O-1) Index where filename starts (set to -1 to reset context).
      @param j        (OUT-2) Index where filename + extension ends.
      @param k        (OUT-3) Length of entire string fullname.
      @return         (OUT-0) Completion status (=1 OK, =0 no more matches).
   */
  int[] ldirectory(long st, byte[] path, byte[] fname, byte[] ext, byte[] fullname, int i, int j, int k) {
    int status = 0;
    try {
      String termPath = FileName.terminatePath(unpackS(path));

      if (i == -1) { // Reset context.
        ldirectoryContext = 0;
      } else if (ldirectoryContext == -1) { // Not initialized yet.
        HP.M.warning("m_ldirectory called without being reset.");
        ldirectoryContext = 0;
      }
      if (ldirectoryContext == 0) {
        // If starting a new listing with a non-local path, print a warning.
        if (!(HP.M.io.findResource(termPath,-1,"","") instanceof FileResource)) {
          HP.M.warning("m_ldirectory called with non-local path '"+termPath+"', output may be unpredictable.");
        }
      }

      String[] matches = listFiles(termPath, unpackS(fname), unpackS(ext));


      if ((matches == null) || (ldirectoryContext >= matches.length)) {
        // No more matches
        status = 0;
      } else {
        String matchName = matches[ldirectoryContext]; ldirectoryContext++;
        packS(fullname, termPath + matchName);
        status = 1;
        i      = termPath.length() + 1;
        j      = termPath.length() + matchName.length();
        k      = j;
      }
    } catch (Throwable t) {
      handle(st,t);
    }
    return new int[] { status, i, j, k };
  }

  /** Special version of listFiles that includes "." and ".." for use by m_ldirectory. */
  private String[] listFiles (String path, String fname, String ext) {
    String[] matches = HP.M.io.listFiles(path, fname, ext);
    Arrays.sort(matches); // File names are sorted in X-Midas so sort them here too.

    if (fname.equals("*") && ext.equals("*")) {
      String[] matches2 = new String[matches.length+2];
      matches2[0] = ".";
      matches2[1] = "..";
      System.arraycopy(matches, 0, matches2, 2, matches.length);
      matches = matches2;
    }

    return matches;
  }

  /** Safely calls Shell.getOptPath(..) */
  void getOptPath (long st, ByteBuffer path, ByteBuffer opt, ByteBuffer area) {
    try {
      String str = Shell.getOptPath(getAsciiString(opt), getAsciiString(area));
      setAsciiString(path, str);
    } catch (Throwable t) {
      handle(st,t);
      setAsciiString(path, null);
    }
  }

  /** Initializes a byte buffer. */
  ByteBuffer byteBufferInit(long st, ByteBuffer buf) {
    buf.order(ByteOrder.nativeOrder());
    return buf;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Wrapper code for to make the members of this class accessible from C
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Only used by HPrimitive.java. */
  public synchronized void closeNmJackets () {
    if (openFiles != null) {
      for (int i = 0; i < openFiles.length; i++) {
        if (openFiles[i] != null) {
          if (openFiles[i].isOpen()) {
            HP.M.warning(HP.getName()+" exited without closing '"+openFiles[i].getName()+"'.");
            openFiles[i].close();
          }
          openFiles[i] = null;
        }
      }
      openFilesLength = 0;
    }
  }

  /** THIS METHOD IS ONLY USED BY BUILDOPT;XBC -- DO NOT USE. */
  public static void generateCode () {
    TextFile tf = new TextFile(Shell.getSharedMidasContext(), "nxm.xbc.lib.NmJackets.h");
    tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
    tf.writeln(header);
    tf.writeln(getWrapperCode());
    tf.close();
  }

  private static final String header = "\n"
      + "/* This file was auto-generated by NmJackets.java, do not modify. */\n"
      + "\n"
      + "\n"
      + "#define GET_METHOD(A,B,C) getMethod(env, A, B, C)\n"
      + "static void* getMethod(JNIEnv *env, jobject obj, char *name, char *sig) {\n"
      + "  void* mid = GetMethod(obj,name,sig);\n"
      + "  if (mid == NULL) {\n"
      + "    fprintf(stderr, \"ERROR: XMLIBC.C: Unable to get method ID for %s sig=%s\\n\", name, sig);\n"
      + "  }\n"
      + "  return mid;\n"
      + "}\n"
      + "\n";


  private static String getWrapperCode () {
    ArrayList<String> initCode = new ArrayList<>();
    ArrayList<String> varsCode = new ArrayList<>();
    ArrayList<String> defsCode = new ArrayList<>();
    Method[]          methods  = NmJackets.class.getDeclaredMethods();

    Arrays.sort(methods, new Comparator<Method>() {
      public int compare (Method a, Method b) {
        return (a.getName().compareTo(b.getName()));
      }
    });

    for (int i = 0; i < methods.length; i++) {
      Method m = methods[i];
      int mod = m.getModifiers();
      if (!Modifier.isPrivate(mod) && !Modifier.isProtected(mod) && !Modifier.isPublic(mod)
                                   && !Modifier.isStatic(mod)) {
        // public or package-private, non-static
        StringBuffer sig    = new StringBuffer();
        StringBuffer args   = new StringBuffer();
        StringBuffer params = new StringBuffer();
        Class<?>[]   types  = m.getParameterTypes();
        Class<?>     type   = m.getReturnType();
        String       name   = m.getName();

        if ((types.length < 1) || (types[0] != Long.TYPE)) {
          throw new MidasException("Error in definition of "+m+" expected first argument to be "
                                 + "'long st'.");
        }

        sig.append('(');
        params.append(",STACKTRACE");
        sig.append(getType(types[0]));
        for (int j = 1; j < types.length; j++) {
          if (j != 1) args.append(',');
          sig.append(getType(types[j]));
          params.append(",p").append(j);
          args.append('p').append(j);
        }
        sig.append(')');
        sig.append(getType(type));

        String mname = "Nm." + StringUtil.padRight(name,              18);  // Java method ID
        String fname = "nm_" + StringUtil.padRight(name+"("+args+")", 36);  // C function call
        String call  = "(*ctx->env)->"+getMethodName(type)+"(ctx->env, ctx->nm, "+mname+params+")";

        defsCode.add("#define "+fname+" "+call);
        initCode.add("  "+mname+" = GET_METHOD(xm,\""+name+"\",\""+sig+"\");");
        varsCode.add("  jmethodID "+name+";");
      }
    }

    StringBuffer str = new StringBuffer();
    str.append("static struct {\n");
    for (int i = 0; i < varsCode.size(); i++) {
      str.append(varsCode.get(i)).append('\n');
    }
    str.append("} Nm;\n");
    str.append("\n");
    for (int i = 0; i < defsCode.size(); i++) {
      str.append(defsCode.get(i)).append('\n');
    }
    str.append("\n");
    str.append("static void initMethods (JNIEnv *env, jobject xm) {\n");
    for (int i = 0; i < initCode.size(); i++) {
      str.append(initCode.get(i)).append('\n');
    }
    str.append("}\n");

    return str.toString();
  }

  /** Convert class to JNI type letter/string. */
  private static String getType (Class<?> clazz) {
    if (clazz == Double.TYPE   ) return "D";
    if (clazz == Float.TYPE    ) return "F";
    if (clazz == Long.TYPE     ) return "J";
    if (clazz == Integer.TYPE  ) return "I";
    if (clazz == Short.TYPE    ) return "S";
    if (clazz == Byte.TYPE     ) return "B";
    if (clazz == Character.TYPE) return "C";
    if (clazz == Boolean.TYPE  ) return "Z";
    if (clazz == Void.TYPE     ) return "V";  // used for return values
    if (clazz.isArray()        ) return clazz.getName().replace('.','/');
                                 return "L"+clazz.getName().replace('.','/')+";";
  }

  /** Convert class to name of JNI calling method. */
  private static String getMethodName (Class<?> clazz) {
    if (clazz == Double.TYPE   ) return "CallDoubleMethod ";
    if (clazz == Float.TYPE    ) return "CallFloatMethod  ";
    if (clazz == Long.TYPE     ) return "CallLongMethod   ";
    if (clazz == Integer.TYPE  ) return "CallIntMethod    ";
    if (clazz == Short.TYPE    ) return "CallShortMethod  ";
    if (clazz == Byte.TYPE     ) return "CallByteMethod   ";
    if (clazz == Character.TYPE) return "CallCharMethod   ";
    if (clazz == Boolean.TYPE  ) return "CallBooleanMethod";
    if (clazz == Void.TYPE     ) return "CallVoidMethod   ";
    if (clazz.isArray()        ) return "CallObjectMethod ";
                                 return "CallObjectMethod ";
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Convenience methods NOT called from native code.
  //////////////////////////////////////////////////////////////////////////////////////////////////


  private String getAsciiString(ByteBuffer buf) {
    String str = null;
    if (buf != null) {
      char[] chars = new char[buf.capacity()];
      for (int i = 0; i < chars.length; i++) {
        chars[i] = (char)(0x7F & buf.get(i));
      }
      str = new String(chars);
    }
    return str;
  }

  private int setAsciiString(ByteBuffer buf, String str) {
    if (str == null) { str = ""; }
    int len = Math.min(str.length(), buf.capacity()-1);

    if (buf != null) {
      for (int i = 0; i < len; i++) {
        char letter = str.charAt(i);
        if ((letter > 0x7F) || (letter < 0)) { letter = '?'; }
        buf.put(i, (byte)letter);
      }
      buf.put(len,(byte)0);
    }
    return len;
  }

  /** Pack a string into a byte buffer. */
  private void packS(byte[] buf, String str) {
    if (str.length() > buf.length) {
      HP.M.warning("Attempt to pack Java string '"+str+"' into a C/Fortran string that is only "
             +buf.length+" bytes long; string will be truncated to '"+str.substring(0,buf.length)+"'.");
    }
    Convert.packS(buf, 0, buf.length, str);
  }

  /** Unapck a string from a byte buffer. */
  private String unpackS(byte[] buf) {
    return Convert.unpackS(buf, 0, buf.length);
  }
}
