package nxm.sys.lib;

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.lang.reflect.Method;
import java.util.ArrayList;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.IDable;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.ShellParent;
import nxm.sys.lib.FileName.FNCase;

/** Utility methods required when interfacing with X-Midas both in XBC and XMJVM. These methods
    should be in libxm, but are not because we need to prevent any build dependencies on libxm
    when not building under X-Midas or XBC. <br>
    <br>
    <b>These methods/constants should ONLY be used by the SYS and XBC option trees, if you use
    this class outside of SYS or XBC expect it to break in the future without notice. You have
    been warned!</b>

    @author  J. Czechowski IV
    @since   NeXtMidas 2.1.1
 */
public final class XmUtil {
  // JC4: Note this class is intentionally arranged by feature mapped from X-Midas to NeXtMidas.
  //      All methods and variables are grouped this way for ease-of-use.

  /** Prevent instantiation of this class since everything is static. */
  private XmUtil() { }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // STATUS
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** The shell parent when in X-Midas. */
  private static final String XMSHELLPARENT = "nxm.sys.libxm.XmShellParent";

  /** Indicates if X-Midas is running. */
  private static boolean isXMidasRunning() {
    ShellParent shellParent = Shell.getCurrent().parent;

    return (shellParent != null)
        && (shellParent.getClass().getName().equals(XMSHELLPARENT));
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // MATH.FOR FUNCTIONS
  //  These are used in some of the code ported from X-Midas Fortran/C++ to Java, including
  //  DPCO;DSP.
  //  These functions are not typically used within XBC since XBC maintains a Fortran copy
  //  of math.for which is more efficient to use when called from Fortran/C++ code.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Internal use only: In interp(..) and interpd(..), replace 'ARRAY(a,b)'
   *  with 'ARRAY[_idx(NCOMP,a,b)]' as this function will handle the array
   *  conversions required.
   */
  private static int _idx (int NCOMP, int a, int b) {
    // In X-Midas we have TVIN(NCOMP,*) and TVOUT(NCOMP,*), here we use
    // TVIN[x*NCOMP] and TVOUT[x*NCOMP]
    if ((a < 1) || (a > NCOMP)) {
      throw new ArrayIndexOutOfBoundsException(a-1);
    }
    if (b < 1) {
      throw new ArrayIndexOutOfBoundsException(b-1);
    }
    return (a-1) + (b-1)*NCOMP;
  }

  /** SPLINE/LINEAR/HOLD INTERPOLATION ROUTINE. This function perform multiple,
   *  hold, linear, or cubic-spline interpolations on a vector in a tabulated
   *  table of vectors. It does not perform extrapolation. If extrapolation is
   *  requested, the output vector is equated to that at the nearest edge of the
   *  table. Execution is not halted. <br>
   *  <br>
   *  If interpolation immediately proximate to the beginning or end of
   *  the table is requested, linear (vice cubic-spline interpolation) is
   *  used and the error flag is set to note that fact. <br>
   *  <br>
   *  <b>WARNING:</b> <i>This is a direct port of the corresponding Fortran
   *  function in X-Midas. The input/output arrays are assumed to be in
   *  column-major order (matching Fortran) rather than the standard row-major
   *  order used by Java.</i>
   *  @param NIN     is the number of vectors entered in the table to be
   *                 interpolated.  Since linear interpolation is used at the
   *                 extreme ends of the table, for maximum accuracy the table
   *                 would include at least two entries smaller than the smallest
   *                 abscissa for which interpolation is requested and two values
   *                 greater than the greatest abscissa for which interpolation is
   *                 requested.  If maximum accuracy is not achieved in all the
   *                 requested  interpolations, the error flag is set to indicate
   *                 that fact.
   *  @param TSTRI   is the start abscissa value corresponding to the table of
   *                 vectors to be interpolated.
   *  @param TDELI   is the abscissa delta value corresponding to the table of
   *                 vectors to be interpolated.
   *  @param TVIN    is the table of tabulated vectors for which interpolation
   *                 will be done.  The table is arranged so that all the components
   *                 of each vector are stored contiguously in memory.
   *  @param NOUT    is the number of abscissa values for which interpolation is
   *                 requested.
   *  @param TSTRO   is the start abscissa value corresponding to the table of
   *                 vectors to be output.
   *  @param TDELO   is the abscissa delta value corresponding to the table of
   *                 vectors to be output.
   *  @param TVOUT   is the output table containing the NOUT interpolated vectors.
   *                 The table is arranged so that all the components of each
   *                 vector are stored contiguously in memory.  The i-th row of
   *                 the table contains the i-th components of the vectors;  the
   *                 j-th columns contains the j-th vector, corresponding to the
   *                 abscissa value TSTRO + TDELO*(j-1).  Thus the  dimensions of
   *                 this table are TVOUT(NCOMP, NOUT).
   *  @param NCOMP   is the length of the vector (i.e., the number of components).
   *  @param NORDER  the highest order of interpolation to be used
   *                 (0-Hold, 1-Linear, or 3-Spline)
   *
   *  @return NERR  is the error flag.  If more than a single condition is
   *                encountered during the execution of the subroutine, only
   *                the last non-zero value is reported by the flag.
   *                Its interpretation is as follows:
   *                <pre>
   *                  0  => No error was encountered; maximum interpolation
   *                        accuracy was achieved for all requested
   *                        interpolations.
   *
   *                  1  => Linear interpolation (vice cubic-spline) was
   *                        used for one or more of the requested
   *                        interpolations.
   *
   *                  2  => Extrapolation was requested one or more times.
   *                        That is, the value for which interpolation was
   *                        requested fell outside the tabulated table.  In
   *                        this case, the interpolated vector was set equal
   *                        to that at the nearest edge of the table.
   */
  private static int interpd (int NIN,  double TSTRI, double TDELI, double[] TVIN,
                              int NOUT, double TSTRO, double TDELO, double[] TVOUT,
                              int NCOMP, int NORDER) {
    final int MAXL = 100;
    // Allow the arrays below to be accessed one-based (+1) so we don't need to
    // change the code more than we need to from the Fortran.
    double[] A0      = new double[MAXL+1];
    double[] A1      = new double[MAXL+1];
    double[] A2      = new double[MAXL+1];
    double[] A3      = new double[MAXL+1];
    int      NERR    = 0;
    int      KLAST   = -1;
    double   TDELINV = 1.0/TDELI;
    boolean  HOLD    = false;
    boolean  LINEAR  = false;

    // Begin Looping over NOUT vectors
    for (int NVEC = 1; NVEC <= NOUT; NVEC++) {
      // Calculate appropriate interval
      double TT = TSTRO + (NVEC-1)*TDELO;
      int    K  = (int)((TT-TSTRI)*TDELINV + 1.0);
             K  = max(K, 0);
             K  = min(K, NIN);
      int    KK = max(K, 1);
      double T  = ( TT - ((K-1)*TDELI+TSTRI) )*TDELINV;

      // Calculate coefficients
      if (K != KLAST) {
        HOLD   = (NORDER == 0) || (K == 0) || (K == NIN);
        LINEAR = (NORDER == 1) || (K == 1) || (K == NIN-1);

        for (int N = 1; N <= NCOMP; N++) {
          double aa0 = TVIN[_idx(NCOMP,N,KK)];
          A0[N] = aa0;
          if (!HOLD) {
            double yy1 = TVIN[_idx(NCOMP,N,K+1)];
            A1[N] = yy1 - aa0;
            if (!LINEAR) {
              double aa1 = 0.5*( yy1-TVIN[_idx(NCOMP,N,K-1)] );
              A1[N] = aa1;
              double aa3 = 0.5*( TVIN[_idx(NCOMP,N,K+2)]+3.0*aa0 ) - yy1 - yy1 + aa1;
              A3[N] = aa3;
              A2[N] = (yy1 - aa1 - aa3 - aa0);
            }
          }
        }
      }

      // Calculate interpolated values
      for (int N = 1; N <= NCOMP; N++) {
        double s = A0[N];
        if (!HOLD) {
          s = (s + A1[N]*T);
          if (!LINEAR) {
            s = (s + T*T*( A2[N] + A3[N]*T ));
          }
        }
        TVOUT[_idx(NCOMP,N,NVEC)] = s;
      }

      // Update last and error parameters
      KLAST = K;
      if (HOLD) NERR = 2;
      if (LINEAR) NERR = max(NERR, 1);
    }

    return NERR;
  }

  /** SPLINE/LINEAR/HOLD INTERPOLATION ROUTINE. This is identical to
   *  {@link #interpd(int,double,double,double[],int,double,double,double[],int,int)}
   *  except that <code>float</code> is used in place of <code>double</code>.
   */
  private static int interp (int NIN,  double TSTRI, double TDELI, float[] TVIN,
                             int NOUT, double TSTRO, double TDELO, float[] TVOUT,
                             int NCOMP, int NORDER) {
    final int MAXL = 100;
    // Allow the arrays below to be accessed one-based (+1) so we don't need to
    // change the code more than we need to from the Fortran.
    float[]  A0      = new float[MAXL+1];
    float[]  A1      = new float[MAXL+1];
    float[]  A2      = new float[MAXL+1];
    float[]  A3      = new float[MAXL+1];
    int      NERR    = 0;
    int      KLAST   = -1;
    double   TDELINV = 1.0/TDELI;
    boolean  HOLD    = false;
    boolean  LINEAR  = false;

    // array(a,b) -> array((a-1) + (b-1)*NCOMP)

    // Begin Looping over NOUT vectors
    for (int NVEC = 1; NVEC <= NOUT; NVEC++) {
      // Calculate appropriate interval
      double TT = TSTRO + (NVEC-1)*TDELO;
      int    K  = (int)((TT-TSTRI)*TDELINV + 1.0);
             K  = max(K, 0);
             K  = min(K, NIN);
      int    KK = max(K, 1);
      double T  = ( TT - ((K-1)*TDELI+TSTRI) )*TDELINV;

      // Calculate coefficients
      if (K != KLAST) {
        HOLD   = (NORDER == 0) || (K == 0) || (K == NIN);
        LINEAR = (NORDER == 1) || (K == 1) || (K == NIN-1);

        for (int N = 1; N <= NCOMP; N++) {
          float aa0 = TVIN[_idx(NCOMP,N,KK)];
          A0[N] = aa0;
          if (!HOLD) {
            float yy1 = TVIN[_idx(NCOMP,N,K+1)];
            A1[N] = yy1 - aa0;
            if (!LINEAR) {
              double aa1 = 0.5*( yy1-TVIN[_idx(NCOMP,N,K-1)] );
              A1[N] = (float)aa1;
              double aa3 = 0.5*( TVIN[_idx(NCOMP,N,K+2)]+3.0*aa0 ) - yy1 - yy1 + aa1;
              A3[N] = (float)aa3;
              A2[N] = (float)(yy1 - aa1 - aa3 - aa0);
            }
          }
        }
      }

      // Calculate interpolated values
      for (int N = 1; N <= NCOMP; N++) {
        float s = A0[N];
        if (!HOLD) {
          s = (float)(s + A1[N]*T);
          if (!LINEAR) {
            s = (float)(s + T*T*( A2[N] + A3[N]*T ));
          }
        }
        TVOUT[_idx(NCOMP,N,NVEC)] = s;
      }

      // Update last and error parameters
      KLAST = K;
      if (HOLD) NERR = 2;
      if (LINEAR) NERR = max(NERR, 1);
    }

    return NERR;
  }

  /** Wrapper to <code>interpd(..)</code> that uses a {@link Data} buffer rather than
   *  a Java array.
   */
  /** Wrapper to <code>interpd(..)</code> that uses a {@link Data} buffer rather than
   *  a Java array.
   *
   *  @param NIN    is the number of vectors entered in the table to be interpolated.
   *  @param TSTRI  is the start abscissa value corresponding to the table of vectors to be interpolated.
   *  @param TDELI  is the abscissa delta value corresponding to the table of vectors to be interpolated.
   *  @param TVIN   is the table of tabulated vectors for which interpolation will be done.
   *  @param NOUT   is the number of abscissa values for which interpolation is requested.
   *  @param TSTRO  is the start abscissa value corresponding to the table of vectors to be output.
   *  @param TDELO  is the abscissa delta value corresponding to the table of vectors to be output.
   *  @param TVOUT  is the output table containing the NOUT interpolated vectors.
   *  @param NCOMP  is the length of the vector (i.e., the number of components).
   *  @param NORDER the highest order of interpolation to be used(0-Hold, 1-Linear, or 3-Spline)
   *  @return NERR is the error flag.
   */
  public static int interpd (int NIN,  double TSTRI, double TDELI, Data TVIN,
                             int NOUT, double TSTRO, double TDELO, Data TVOUT,
                             int NCOMP, int NORDER) {
    double[] _TVIN  = TVIN.castD(true);
    double[] _TVOUT = TVOUT.castD(false);

    int NERR = interpd(NIN, TSTRI, TDELI, _TVIN, NOUT, TSTRO, TDELO, _TVOUT, NCOMP, NORDER);

    TVIN.uncast(_TVIN, false);
    TVOUT.uncast(_TVOUT, true);

    return NERR;
  }

  /** Wrapper to <code>interp(..)</code> that uses a {@link Data} buffer rather than
   *  a Java array. Reference, {@link #interpd(int, double, double, double[], int, double, double, double[], int, int)},
   *  for more detailed @param and @return explanations
   *  @param  NIN     is the number of vectors entered in the table to be
   *                  interpolated.
   *  @param  TSTRI   is the start abscissa value corresponding to the table of
   *                  vectors to be interpolated.
   *  @param  TDELI   is the abscissa delta value corresponding to the table of
   *                  vectors to be interpolated.
   *  @param  TVIN    is the table of tabulated vectors for which interpolation
   *                  will be done. (Stored in a Java Data Object)
   *  @param  NOUT    is the number of abscissa values for which interpolation is
   *                  requested.
   *  @param  TSTRO   is the start abscissa value corresponding to the table of
   *                  vectors to be output.
   *  @param  TDELO   is the abscissa delta value corresponding to the table of
   *                  vectors to be output.
   *  @param  TVOUT   is the output table containing the NOUT interpolated vectors.
   *                  (Stored in a Java Data Object)
   *  @param  NCOMP   is the length of the vector (i.e., the number of components).
   *  @param  NORDER  the highest order of interpolation to be used
   *
   *  @return NERR    is the error flag.
   */
  public static int interp (int NIN,  double TSTRI, double TDELI, Data TVIN,
                            int NOUT, double TSTRO, double TDELO, Data TVOUT,
                            int NCOMP, int NORDER) {
    float[] _TVIN  = TVIN.castF(true);
    float[] _TVOUT = TVOUT.castF(false);

    int NERR = interp(NIN, TSTRI, TDELI, _TVIN, NOUT, TSTRO, TDELO, _TVOUT, NCOMP, NORDER);

    TVIN.uncast(_TVIN, false);
    TVOUT.uncast(_TVOUT, true);

    return NERR;
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // MESSAGES
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Size of the message header in bytes for non-heterogeneous (type not 'H') messages. */
  public static final int  LENGTH_MQH   = 32;

  /** Size of the message header in bytes for heterogeneous (type == 'H') messages. */
  public static final int  LENGTH_MQH_H = 120; // additional int*2, char*86 fields

  /** Size of the message header in 8-byte slots for non-heterogeneous (type not 'H') messages. */
  public static final byte SLOTS_MQH    = 4;   // = LENGTH_MQH / 8

  /** Size of the message header in 8-byte slots for heterogeneous (type == 'H') messages. */
  public static final byte SLOTS_MQH_H  = 15;  // = LENGTH_MQH_H / 8

  /** The theoretical max message data size in bytes. */
  public static final int  LENGTH_MQD   = 8192; // This is hard-coded in X-Midas 3.8.3

  private static final XmMessageHandler[] MSG_HANDLER = createXmMsgHandlers();

  /** Creates the message handlers. */
  private static XmMessageHandler[] createXmMsgHandlers () {
    XmMessageHandler[] handlers = new XmMessageHandler[128];

    for (int i = 0; i < handlers.length; i++) {
      handlers[i] = (isXMidasRunning())? new XmMessageHandler(i) : null;
    }
    return handlers;
  }

  /** Gets the C -or- C++ definition matching a message type specifier.
      @param type   Midas type definition from messages.cfg
      @param name   name of variable for output
      @param cpp    true if outputting C++ type definition, otherwise false for C type definition
      @return string with C or C++ type and name for inclusion in header file
      @since NeXtMidas 3.1.2
   */
  static String getCMsgTypeDef (String type, String name, boolean cpp) {
    switch (type.charAt(0)) {
      case DataTypes.BYTE:   return "int_1        "+name;
      case DataTypes.INT:    return "int_2        "+name;
      case DataTypes.LONG:   return "int_4        "+name;
      case DataTypes.XLONG:  return "int_8        "+name;
      case DataTypes.FLOAT:  return "real_4       "+name;
      case DataTypes.DOUBLE: return "real_8       "+name;
      case DataTypes.ASCII:    if (cpp) return "fstring<8>   "+name;
                else     return "char         "+name+"[8]";
      case DataTypes.ASCII256: if (cpp) return "fstring<256>   "+name; // 3.9.4 NXM-3570
      else     return "char         "+name+"[256]";
      case DataTypes.STRING:   if (cpp) return "fstring<"+getBytesForMsgType(type)+">  "+name;
                else     return "char         "+name+"["+getBytesForMsgType(type)+"]";
      case 'U': // FALLTHROUGH: Supported for unconfigured messages only.
      default:  throw new MidasException("Unknown data type '"+type+"'.");
    }
  }

  /** Gets the Fortran definition matching a message type specifier.
     @param type The string to compare with to determine return statement
     @return Returns Fortran message type
   */
  public static String getFortranMsgTypeDef (String type) {
    switch (type.charAt(0)) {
      case DataTypes.BYTE:     return "byte";
      case DataTypes.INT:      return "integer*2";
      case DataTypes.LONG:     return "integer*4";
      case DataTypes.XLONG:    return "integer*8"; // since 3.1.2
      case DataTypes.FLOAT:    return "real*4";
      case DataTypes.DOUBLE:   return "real*8";
      case DataTypes.ASCII:    return "character*8";
      case DataTypes.ASCII256: return "character*256";
      case DataTypes.STRING:   return "character*"+getBytesForMsgType(type);
      case 'U': // FALLTHROUGH: Supported for unconfigured messages only.
      default:  throw new MidasException("Unknown data type '"+type+"'.");
    }
  }

  /** Gets the number of bytes for a given message type specifier.
      @param type The string to compare with to determine return statement
      @return Returns bytes for message type
   */
  public static int getBytesForMsgType (String type) {
    char msgType = type.charAt(0);
    if (Data.validType(msgType)) return Data.getBPS(msgType);
    switch (type.charAt(0)) {
      case 'U': // FALLTHROUGH: same as 'S'
      case 'S': return (type.length() == 1)? 1 : Convert.o2l(type.substring(2,type.length()-1));
      default:  throw new MidasException("Unknown data type '"+type+"'.");
    }
  }

  /** Gets a message handler that will send messages to the given X-Midas ID. This method is safe
      to use even if X-Midas is not running (it just returns null in that case).
      @param id The ID string in the form <code>"XM.&lt;num&gt;"</code>.
      @return null if X-Midas is not running or if the ID does not match a valid X-Midas ID,
              otherwise returns the X-Midas ID.
   */
  public static MessageHandler getXmMessageHandler (Object id) {
    if (id instanceof XmMessageHandler) return (XmMessageHandler)id; // XmMessageHandler passed in
    if (id == null) return null;         // null object passed

    String str = id.toString();
    if ((str.length() < 4) || !str.toUpperCase().startsWith("XM.")) return null; // invalid syntax

    String num = str.substring(3);
    if (!StringUtil.isInteger(num)) return null; // not a number
    int xmId = Convert.s2l(num);
    if ((xmId < 0) || (xmId > 127)) return null; // outside of valid range

    return MSG_HANDLER[xmId]; // Always use same object for each XM ID since "==" checks are common
  }

  /** Implement a MessageHandler for sending messages to X-Midas. */
  private static class XmMessageHandler implements MessageHandler, IDable {
    /** Numeric X-Midas ID (0..127) */
    final int id;

    public XmMessageHandler (int xmId) {
      this.id = xmId;
    }
    @Override
    public int processMessage (Message msg) {
      try {
        Class<?> clazz = Class.forName(XMSHELLPARENT);
        Method method  = clazz.getMethod("sendXmMessage", Integer.TYPE, Message.class);
        method.invoke(null, id, msg);
        return 1;

      } catch (Exception e) {
        throw new MidasException("Message: Unable to send message '"+msg+"' to X-Midas: "+e, e);
      }
    }
    @Override public String getID ()    { return "XM."+id; }
    @Override public String toString () { return "XM."+id; }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // GRAPHICS
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** The set of X-Midas symbols names. */
  private static final String[] xmSymbolNames
        = { "NULL", "DOT", "CIRCLE", "SQUARE", "PLUS", "CROSS", "UTRI", "DTRI" };

  /** Gets the name of the NeXtMidas symbol that matches an X-Midas symbol ID. <br>
      This uses the definitions from xstructures.h in X-Midas 3.8.3.
      @param id The X-Midas symbol ID.
      @return The NeXtMidas name for the given symbol or <code>null</code> if the ID is not
              recognized. Note that this will return <code>"NULL"</code> if the ID is zero
              (this matches the NeXtMidas name meaning "no symbol").
      @since NeXtMidas 2.3.0
  */
  public static String getXmSymbolName(final int id) {
    if ((id >= 0) && (id < xmSymbolNames.length)) {
      return xmSymbolNames[id];
    } else {
      return null; /* Invalid name. */
    }
  }

  /** Gets the name of the NeXtMidas symbol names for the recognized X-Midas symbol IDs. <br>
      <br>
      This returns an array such that <code>getXmSymbolName(id)</code> is identical to
      <code>getXmSymbolNames()[id]</code> for all valid ID values.
      @return The NeXtMidas names for the X-Midas symbols.
      @since NeXtMidas 2.3.0
  */
  public static String[] getXmSymbolNames() {
    return xmSymbolNames;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // AUXILLIARY
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Named AUX that can not convert to a string */
  public  static final int     NAMED_AUX     = Integer.MIN_VALUE;
  /** Named AUX that can not convert to number */
  public  static final String  NAMED_AUX_STR = Integer.toString(NAMED_AUX);
  /** Using "old" X-Midas (no AUX names) with NeXtOpt */
  private static final boolean OLD_NEXTOPT   = true;

  /** Highest numeric AUX value */
  public  static final int MAX_AUX_NUM  = 0x00FFFFFF;
  /** Bit flag for a AUX alias defined by X-Midas */
  public  static final int XM_AUX_FLAG  = 0x01000000;
  /** Bit flag for a AUX alias defined by NeXtMidas */
  public  static final int NXM_AUX_FLAG = 0x02000000;
  /** Bit flag for a AUX alias defined by NeXtMidas with old X-Midas ver */
  public  static final int OLD_AUX_FLAG = 0x04000000;

  /** Checks an AUX name. First it strips of any leading '-' character. The if the name is numeric
      and out of range, it will be resolved using {@link #getAuxName(int)}, otherwise the name is
      just returned. Unlike {@link #getAuxName} and {@link #getAuxNumber}, this allows the aux name
      to have a "+" or "-" prefix (which is preserved), for use by the AUXILIARY command.
      @param name The name (could be the string form of a numeric alias).
      @return The name of the AUX.
      @throws MidasException If the name is illegal.
   */
  public static String checkAuxName (String name) {
    String  prefix  = "";
    String  aux     = name;
         if (aux.startsWith("-")) { aux = aux.substring(1); prefix = "-"; }
    else if (aux.startsWith("+")) { aux = aux.substring(1); prefix = "+"; }

    if (name.equals(NAMED_AUX_STR)) {
      name = null; // error
    }
    else if (StringUtil.isInteger(aux)) {
      int num = Convert.o2l(aux);
      name = prefix + getAuxName(num);
    }
    return name;
  }

  /** Converts AUX number to name. This method is the reverse of the {@link #getAuxNumber(String)}
      method.
      @param num The AUX number.
      @return The name of the AUX.
      @throws MidasException If the number is illegal.
   */
  public static String getAuxName (int num) {
    return getAuxName(num,true);
  }

  /** Converts AUX number to name. This method is the reverse of the {@link #getAuxNumber(String)}
      method.
      @param num The AUX number.
      @param error If true, throw an exception when not found.
      @return The name of the AUX or null if not found.
   */
  public static synchronized String getAuxName (int num, boolean error) {
    String name = nxmAuxNumToName(num);

    if ((name == null) && Shell.isNeXtOpt()) {
      if (OLD_NEXTOPT) {
             if (num ==  0)         name = "CWD";      // Special case, old X-Midas.
        else if (num == 98)         name = "HOMEPATH"; // Special case, old X-Midas.
        else if (num == 99)         name = "DAT";      // Special case, old X-Midas.
        else if (num == 0x04000000) name = "RAM";      // Special case, old X-Midas for NeXtMidas.
        else if (num == 0x04000001) name = "XMPIPE";   // Special case, old X-Midas for NeXtMidas.
        else if (num == 0x04000002) name = "HOME";     // Special case, old X-Midas for NeXtMidas.
        else throw new MidasException("Can not map AUX number to name in X-Midas at this time.");
      }
      else {
        throw new MidasException("Can not map AUX number to name in X-Midas at this time. Non-OLD_NEXTOPT version not implemented.");
      }
    }

    if (error && (name==null)) {
      throw new MidasException("Illegal AUX number "+num);
    }
    return name;
  }

  /** Converts AUX name to number.
      @param name The AUX name.
      @return The number of the AUX or
   */
  public static synchronized int getAuxNumber (String name) {
    if (name.equals("READ"))  throw new MidasException("Invalid AUX name '"+name+"'.");
    if (name.equals("WRITE")) throw new MidasException("Invalid AUX name '"+name+"'.");
    if (name.equals("ALL"))   throw new MidasException("Invalid AUX name '"+name+"'.");

    int num = nxmAuxNameToNum(name);
    if ((num < 0) && Shell.isNeXtOpt()) {
      if (OLD_NEXTOPT) {
             if (name.equals("CWD"))         num =  0;         // Special case, old X-Midas.
        else if (name.equals("HOMEPATH"))    num = 98;         // Special case, old X-Midas.
        else if (name.equals("DAT"))         num = 99;         // Special case, old X-Midas.
        else if (name.equals("RAM"))         num = 0x04000000; // Special case, old X-Midas for NeXtMidas.
        else if (name.equals("XMPIPE"))      num = 0x04000001; // Special case, old X-Midas for NeXtMidas.
        else if (name.equals("HOME"))        num = 0x04000002; // Special case, old X-Midas for NeXtMidas.
        else if (StringUtil.isInteger(name)) num = Convert.o2l(name);
        else throw new MidasException("Can not map AUX name '"+name+"' X-Midas AUX number at this time.");
      }
      else {
        throw new MidasException("Can not map AUX name '"+name+"' X-Midas AUX number at this time. Non-OLD_NEXTOPT version not implemented.");
      }
    }
    return num;
  }

  /** Only used by the AUX command.
      @param name The string to compare with to determine alias
      @return alias Returns alias for name
   */
  public static String getAuxAlias (String name) {
    String alias = null;
         if (name.equals("READ"))     throw new MidasException("Invalid AUX name '"+name+"'.");
    else if (name.equals("WRITE"))    throw new MidasException("Invalid AUX name '"+name+"'.");
    else if (name.equals("ALL"))      throw new MidasException("Invalid AUX name '"+name+"'.");
    else if (name.equals("0"))        alias = "CWD";
    else if (name.equals("98"))       alias = "HOMEPATH";
    else if (name.equals("99"))       alias = "DAT";
    else if (name.equals("CWD"))      alias = "0";
    else if (name.equals("HOMEPATH")) alias = "98";
    else if (name.equals("DAT"))      alias = "99";
    else if (name.startsWith("_") && name.endsWith("_")) alias = null;   // ignore special (internal use) AUX entries (Bug 2481 & 2482)
    else if (OLD_NEXTOPT)             alias = null; // do nothing
    else if (Shell.isNeXtOpt()) {
      throw new MidasException("getAuxAlias(): Can not map AUX number to name in X-Midas at this time.");
    }

    if ((alias != null) && auxExists(alias)) {
      alias = null;  // Should not need an alias.
    }
    return alias;
  }

  /** Standard AUX name to number conversion. */
  private static int nxmAuxNameToNum (String name) {
    int num;
         if (name.equals("CWD")      && !auxExists("0"))  num = 0;
    else if (name.equals("HOMEPATH") && !auxExists("98")) num = 98;
    else if (name.equals("DAT")      && !auxExists("99")) num = 99;
    else if (StringUtil.isNumber(name))                   num = Convert.s2l(name);
    else                                                  num = NAMED_AUX;
    return num;
  }

  /** Standard AUX number to name conversion. */
  private static String nxmAuxNumToName (int num) {
    String name;
         if ((num ==  0) && !auxExists("0"))  name = "CWD";
    else if ((num == 98) && !auxExists("98")) name = "HOMEPATH";
    else if ((num == 99) && !auxExists("99")) name = "DAT";
    else                                      name = Integer.toString(num);
    return name;
  }

  /** Checks to see if a given AUX is defined in the AUX table. */
  private static boolean auxExists (String aux) {
    Table resTable = Shell.getSharedMidasContext().getResults();
    Table auxTable = resTable.getTable("AUX");

    return auxTable.containsKey(aux);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // GENMSG (matches X-Midas's GENMSGINC functionality + more for NeXtMidas)
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Generate flag for {@link #genmsg}: Generate tbl file for NeXtMidas to use. */
  public static final int GENMSG_TBL = 0x01;
  /** Generate flag for {@link #genmsg}: Generate Fortran include files. */
  public static final int GENMSG_INC = 0x02;

  /** Provides the functionality of the X-Midas GENMSGINC for XBC and also creates the TBL files
      required for converting NeXtMidas to/from X-Midas messages.
      @param midas  The Midas context to use.
      @param xmdisk The path to $XMDISK if run from X-Midas (i.e. for NeXtOpt), if not run from
                    X-Midas (i.e. XBC) this should be null.
      @param opt    The current option tree (i.e. the destination).
      @param fmt    The output format (bitmask of {@link #GENMSG_TBL}, {@link #GENMSG_INC}).
      @param path   The current path.
   */
  public static void genmsg (Midas midas, String xmdisk, String opt, String path, int fmt) {
    if (opt == null)       throw new MidasException("GenMsg: Unknown option OPT="+opt);
    if (opt.length() <= 0) throw new MidasException("GenMsg: Unknown option OPT="+opt);

    // ---- Open the input files ----------------------------------------------
    // Open the input files in order of importance, this should ultimately look like:
    //       XBC: OPT, [other opts], SYS, XBC   (here SYS=NeXtMidas SYS, XBC=X-Midas SYS via XBC)
    //   NeXtOpt: OPT, [other opts], SYS        (here SYS=X-Midas SYS)
    String addOpt  = (xmdisk == null)? "SYS,XBC" : "SYS";
    Table  inFiles = new Table();
    Parser parser  = new Parser(opt + "," + path + "," + addOpt);

    parser.clean(true);
    for (int i = 1; i <= parser.elements(); i++) {
      String inOpt = parser.get(i).toUpperCase();

      if (inOpt.length() > 1) {
        TextFile tf = openFile(midas, xmdisk, "messages.cfg", "cfg", inOpt, true);
        if (tf == null) {
          // optional file not found, ignore
        }
        else if (inFiles.containsKey(inOpt)) {
          tf.close(); // discard duplicates
        }
        else {
          inFiles.put(inOpt, tf);
        }
      }
    }

    // ---- Process the input files -------------------------------------------
    Table    msgdefs = new Table();  // The message definitions.
    String[] opts    = inFiles.getKeys();

    for (int i = opts.length-1; i >= 0; i--) {
      TextFile file = (TextFile)inFiles.get(opts[i]);
      processInputFile(midas, msgdefs, file, opts[i]);
      file.close();
    }

    // ---- Write the output files --------------------------------------------
    if ((fmt & GENMSG_TBL) != 0) {
      if (xmdisk == null) {
        writeMessagesTbl(midas, xmdisk, msgdefs, opt, path, inFiles.getKeyList());  // used with XBC
      } else {
        writeQmessagesTbl(midas, xmdisk, msgdefs, opt, path, inFiles.getKeyList()); // used with NeXtOpt
      }
    }
    if ((fmt & GENMSG_INC) != 0) {
      String[] configOpts = inFiles.getKeys(); // option tree's in path with messages.cfg
      writeQmessagesInc(midas, xmdisk, msgdefs, opt); // J. Czechowski, this file is so trivial, why auto generate it? -NTN
      writeQmessdeclInc(midas, xmdisk, msgdefs, opt, configOpts);
      writeQmessdefInc(midas, xmdisk, msgdefs, opt, configOpts);
      writeQmessdeclHeader(midas, xmdisk, msgdefs, opt, configOpts);       // since 3.1.2: generate qmessdecl.h C/C++ header
      writeQmessdefHeader(midas, xmdisk, msgdefs, opt, configOpts, false); // since 3.1.2: generate qmessdef.h  C header
      writeQmessdefHeader(midas, xmdisk, msgdefs, opt, configOpts, true);  // since 3.1.2: generate qmessdef.hh C++ header
    }
  }

  /**  Process an input file for genmsg. */
  private static void processInputFile (Midas midas, Table msgdefs, TextFile file, String opt) {
    Table  curdef  = null;         // The current definition.
    Table  curtags = null;         // The current definition's tags.
    String shared  = null;         // The current shared attributes (null if after SHARED tag).
    String line    = file.read();
    String next    = file.read();

    while (line != null) {
      // ---- CLEAN THE LINE ------------------------------------------------
      line = line.trim();
      line = line.replace('\t', ' ');

      if (line.startsWith("&")) {
        throw new MidasException("CONVERT: Can not handle line-continuations yet.");
      }

      // ---- REMOVE COMMENTS & MULTIPLE DEFS -------------------------------
      int commentStart = line.indexOf('!');
      String comment; // The comment for the line
      String more;    // Used when multiple definitions are on a line.

      if (commentStart >= 0) {
        comment = line.substring(commentStart+1).trim();
        line    = line.substring(0,commentStart).trim().replace('"', '\'');
      } else {
        line    = line.trim();
        comment = null;
      }

      // ---- PROCESS A LINE ------------------------------------------------
      while ((line != null) && (line.length() > 0)) {
        int commaPos = line.indexOf(',');
        if (commaPos > 0) {
          more = line.substring(commaPos+1).trim();
          line = line.substring(0,commaPos).trim();
        } else {
          more = null;
        }

        String lineUC = line.toUpperCase();

        if (line.length() == 0) {
          // Blank entry

        } else if (lineUC.startsWith("NAME ")) {
          if (curdef != null) updateTypeStr(curdef); // done with last definition

          curdef  = new Table();
          curtags = new Table();
          shared  = "";
          String tag = lineUC.substring(5).trim();
          if (msgdefs.containsKey(tag)) {
            midas.warning("Message /"+tag+"/ overridden by definition in "+opt+" option.");
          }
          msgdefs.setKey(tag, curdef);
          curdef.setKey("TAGS", curtags);

          String name = tag; // orig: name = lineUC.substring(5).trim();
          if (name.endsWith("INFO")) name = "=" + name.substring(0, name.length()-4);
          if (name.length() > 8)     name = name.substring(0,8);
          curdef.setKey("NAME", name);

          if (comment != null) curdef.setKey("COMMENT", comment);

        } else if ((curdef == null) || (curtags == null)) { // All entries below must follow a NAME
          throw new MidasException("Illegal definition "+line);

        } else if (lineUC.startsWith("USES ")) {
          shared = copySharedTags(msgdefs, curtags, shared, lineUC.substring(5).trim());

        } else if (lineUC.startsWith("INFOALIAS ")) {
          curdef.setKey("INFOALIAS", lineUC.substring(10).trim());

        } else if (lineUC.equals("SHARED")) {
          curdef.setKey("SHARED", shared);
          shared = null;

        } else {
          int nameStart = line.indexOf(':');

          Table  tbl  = new Table();
          String type = lineUC.substring(0,nameStart);
          String name = lineUC.substring(nameStart+1);

          tbl.setKey("TYPE", type);
          if (comment != null) tbl.setKey("COMMENT", comment);

          curtags.setKey(name, tbl);

          if (shared != null) {
            shared = (shared.length() == 0)? name : shared+","+name;
          }
        }
        line = more;
      }
      if (curdef != null) updateTypeStr(curdef); // done with last definition

      line = next;
      next = file.read();
    }
  }

  /** Updates the type string in a definition for genmsg.
      @param def The definition.
   */
  private static void updateTypeStr (Table def) {
    Table         tags         = def.getTable("TAGS");
    String[]      tagNames     = tags.getKeys();
    String        lastTypeStr  = null;
    int           lastTypeMult = 0;
    int           byteCount    = 0;
    char          type         = ' ';
    StringBuilder types        = new StringBuilder();

    for (int j = 0; j < tagNames.length; j++) {
      String typeStr = tags.getTable(tagNames[j]).getS("TYPE");

      byteCount += XmUtil.getBytesForMsgType(typeStr);

      if (j == 0) {
        if (typeStr.length() == 1) {
          type = typeStr.charAt(0);
        } else {
          type = 'H';
        }
      } else if ((typeStr.length() == 1) && (typeStr.charAt(0) == type)) {
        // Type is good.
      } else {
        type = 'H';
      }

      if (lastTypeStr == null) {
        lastTypeStr  = typeStr;
        lastTypeMult = 1;

      } else if (typeStr.equals(lastTypeStr)) {
        lastTypeMult++;

      } else {
        if (lastTypeStr != null) {
          if (lastTypeMult > 1) {
            types.append(lastTypeMult);
          }
          types.append(lastTypeStr);
        }
        lastTypeStr  = typeStr;
        lastTypeMult = 1;
      }
    }
    if (lastTypeStr != null) {
      if (lastTypeMult > 1) {
        types.append(lastTypeMult);
      }
      types.append(lastTypeStr);
    }

    def.setKey("TYPE",   Character.toString(type));

    if (type == 'H') {
      def.setKey("NSLOTS",  Integer.toString((byteCount+7)/8 + XmUtil.SLOTS_MQH_H));
      def.setKey("NHSLOTS", Integer.toString(XmUtil.SLOTS_MQH_H));
      def.setKey("TYPES",   types.toString());
    } else {
      def.setKey("NSLOTS",  Integer.toString((byteCount+7)/8 + XmUtil.SLOTS_MQH));
      def.setKey("NHSLOTS", Integer.toString(XmUtil.SLOTS_MQH));
    }
  }

  /** Copies shared tags to the current definition for genmsg.
      @param name The name of the existing definition to use.
   */
  private static String copySharedTags (Table msgdefs, Table curtags, String shared, String name) {
    Table def = msgdefs.getTable(name);

    if (def != null) {
      String sharedTags = def.getS("SHARED");

      if ((sharedTags != null) && (sharedTags.length() > 0)) {
        Table    tags     = def.getTable("TAGS");
        String[] tagNames = sharedTags.split(",");

        for (int i = 0; i < tagNames.length; i++) {
          curtags.setKey(tagNames[i], tags.getTable(tagNames[i]));
        }
        if (shared != null) {
          shared = (shared.length() == 0)? sharedTags : shared+", "+sharedTags;
        }
      }
    } else {
      throw new MidasException("Can not include "+name+" for USES "+name);
    }
    return shared;
  }

  /** Opens an input or output file for genmsg.
      @param name The name of the output file.
   */
  private static TextFile openFile (Midas midas, String xmdisk, String name, String area,
                                    String opt, boolean input) {
    Table  allOpts = OptionTree.getAllOptions(midas);
    String fname   = null;
    int    flags   = (input)? BaseFile.INPUT|BaseFile.NOABORT
                            : BaseFile.OUTPUT|TextFile.NOABORT|BaseFile.MKDIR;
    String optUC   = opt.toUpperCase();

    opt  = opt.toLowerCase();
    area = area.toLowerCase();

    if ((xmdisk != null) && opt.equals("sys")) {           // Special case X-Midas SYS for NeXtOpt
      fname = xmdisk + "/xm/"+area+"/"+name;
    }
    else if ((xmdisk != null) && opt.equals("nxm")) {      // Special case NeXtMidas SYS for NeXtOpt
      fname = "nxm.sys."+area+"."+name;
    }
    else if (allOpts.containsKey(optUC)) {                 // Option tree available in NeXtMidas
      fname = "nxm."+opt+"."+area+"."+name;
    }
    else if (xmdisk != null) {                             // X-Midas option tree, not yet in NeXtMidas
      String optPath = Import.getEnv("XMAREA_"+optUC);
      if (optPath != null) {
        midas.warning("Option tree "+optUC+" not defined in NeXtMidas, using "+optPath+" for now.");
        fname = optPath+"/"+area+"/"+name;
      }
    }

    if (fname == null) {
      throw new MidasException("Option tree "+optUC+" undefined.");
    }

    FileName fnFname = new FileName(fname,FNCase.KeepCase);
    TextFile file = new TextFile(fnFname);
    file.open(flags);

    if (!file.open()) {
      if (!input) throw new MidasException("GENNMMSG: Can not open output file "+file.getURL());
      file = null;
    }
    else if (!input) {
      midas.info("Creating "+file.getURL());
    }

    return file;
  }

  /** Writes out the first Fortran include file for genmsg. */
  private static void writeMessagesTbl (Midas midas, String xmdisk, Table msgdefs, String opt, String path, String configOpts) {
    TextFile tf = openFile(midas, xmdisk, "messages.tbl", "cfg", opt, false);
    tf.writeln("!");
    tf.writeln("! ** This file was autogenerated by CONVERT "+opt+" MSGCFG2TBL **");
    tf.writeln("! ** DO NOT MODIFY! **");
    tf.writeln("!");
    tf.writeln("! Contains the NeXtMidas message mappings for X-Midas messages.");
    tf.writeln("! These mappings are used when running an X-Midas command from");
    tf.writeln("! NeXtMidas using the XBC option tree. Please see qmessages.tbl");
    tf.writeln("! (if present) for message mapping used when NeXtMidas is run as");
    tf.writeln("! an X-Midas option tree.");
    tf.writeln("!");
    tf.writeln("! FOR     OPT : "+opt);
    tf.writeln("! CURRENT PATH: "+path);
    tf.writeln("! Option trees with messages.cfg in PATH: "+configOpts);
    tf.writeln("!");
    msgdefs.toTextFile(tf);
    tf.close();
  }

  /** Writes out the first Fortran include file for genmsg. */
  private static void writeQmessagesTbl (Midas midas, String xmdisk, Table msgdefs, String opt, String path, String configOpts) {
    TextFile tf = openFile(midas, xmdisk, "qmessages.tbl", "cfg", opt, false);
    tf.writeln("!");
    tf.writeln("! ** This file was autogenerated by CONVERT "+opt+" MSGCFG2TBL **");
    tf.writeln("! ** DO NOT MODIFY! **");
    tf.writeln("!");
    tf.writeln("! Contains the NeXtMidas message mappings for X-Midas messages.");
    tf.writeln("! These mappings are used when NeXtMidas is running an X-Midas");
    tf.writeln("! option tree. Please see messages.tbl (if present) for message");
    tf.writeln("! mapping used by the XBC option tree.");
    tf.writeln("!");
    tf.writeln("! FOR     OPT : "+opt);
    tf.writeln("! CURRENT PATH: "+path);
    tf.writeln("! Option trees with messages.cfg in PATH: "+configOpts);
    tf.writeln("!");
    msgdefs.toTextFile(tf);
    tf.close();
  }

  /** Writes out the first Fortran include file for genmsg. */
  private static void writeQmessagesInc (Midas midas, String xmdisk, Table msgdefs, String opt) {
    TextFile tf = openFile(midas, xmdisk, "qmessages.inc", "inc", opt, false);
    tf.writeln("c************");
    tf.writeln("c  Description: Contains common X-Midas system message definitions,");
    tf.writeln("c  declared structures of their types, and the bit values used by MSGMASK");
    tf.writeln("c************");
    tf.writeln("");
    tf.writeln("");
    tf.writeln("\tinclude 'qmessdef.inc'");
    tf.writeln("\tinclude 'qmessdecl.inc'");
    tf.writeln("\tinclude 'msgmask.inc'");
    tf.close();
  }

  /** Write C/C++ comments for opt of generated header file for MSGCFG2INC function.
      @param tf  Text file to write to (must be opened for output)
      @param opt Option tree this is for
      @param configOpts String array of configured option tree in path with messages.cfg
      @since NeXtMidas 3.1.2
   */
  private static void writeFortranHeaderComments (TextFile tf, String opt, String[] configOpts) {
    tf.writeln("c");
    tf.writeln("c Automatically generated by CONVERT "+opt+" MSGCFG2INC");
    tf.writeln("c");
    tf.writeln("c Configuration: "+opt);
    for (String option : configOpts) {
      tf.writeln("c   Option "+option+" from "+Shell.getOptPath(option,"cfg")+"messages.cfg");
    }
  }

  /** Writes out the second Fortran include file for genmsg. */
  private static void writeQmessdeclInc (Midas midas, String xmdisk, Table msgdefs, String opt, String[] configOpts) {
    TextFile tf = openFile(midas, xmdisk, "qmessdecl.inc", "inc", opt, false);
    String[] keys = msgdefs.getKeys();

    writeFortranHeaderComments(tf, opt, configOpts);
    tf.writeln("");
    for (String key : keys) {
      String name = "MQH_"+key;

      if (name.length() < 15) {
        tf.writeln("\trecord /"+name+"/\t\t"+name);
      } else {
        tf.writeln("\trecord /"+name+"/\t"+name);
      }
    }
    tf.writeln("");
    tf.writeln("\trecord /MQ_HEADER/\tMQH");
    tf.writeln("\trecord /MQ_DATA/\tMQD");
    tf.close();
  }

  /** Writes out the third Fortran include (message definition) file for genmsg. */
  private static void writeQmessdefInc (Midas midas, String xmdisk, Table msgdefs, String opt, String[] configOpts) {
    TextFile tf = openFile(midas, xmdisk, "qmessdef.inc", "inc", opt, false);
    final int INFO = 0; // Value for info field.
    final int MODE = 0; // Value for mode field.

    ArrayList<String> mapping = new ArrayList<>();
    String[] keys = msgdefs.getKeys();
    int lengthMqh = 0;
    int lengthMqd = 0;

    writeFortranHeaderComments(tf, opt, configOpts);
    tf.writeln("");
    tf.writeln("\tinclude 'queue.inc'");
    tf.writeln("");
    for (int i = 0; i < keys.length; i++) {
      Table    def      = msgdefs.getTable(keys[i]);
      Table    tags     = def.getTable("TAGS");
      String[] tagNames = tags.getKeys();
      int      ndata    = tagNames.length;
      char     type     = (def.getS("TYPE", "")+" ").charAt(0);
      int      nslots   = def.getI("NSLOTS");
      int      nhslots  = def.getI("NHSLOTS");
      String   name     = def.getS("NAME");
      String   alias    = def.getS("INFOALIAS", "info").toLowerCase();
      String   types    = def.getS("TYPES", "");
      int      ntype    = types.length();

      lengthMqh = Math.max(lengthMqh, nhslots*8);
      lengthMqd = XmUtil.LENGTH_MQD; // Always use theoretical max data size (same as X-Midas)

      tf.writeln("\tSTRUCTURE /MQH_"+keys[i]+"/");
      tf.writeln("\t  integer*4\treader");
      tf.writeln("\t  integer*2\tnslots\t/"+nslots+"/");
      tf.writeln("\t  character*1\ttype\t/'"+type+"'/");
      tf.writeln("\t  byte\t\tnhslots\t/"+nhslots+"/");
      tf.writeln("\t  integer*4\tsender");
      tf.writeln("\t  integer*4\tndata\t/"+ndata+"/");
      tf.writeln("\t  integer*4\t"+alias+"\t/"+INFO+"/"); // info -OR- infoalias
      tf.writeln("\t  integer*4\tmode\t/"+MODE+"/");
      tf.writeln("\t  character*8\tname\t/'"+name+"'/");
      if (type == 'H') {
        tf.writeln("\t  integer*2\tntype\t/"+ntype+"/");
        tf.writeln("\t  character*86\ttypes\t/'"+types+"'/");
      }
      tf.writeln("\tEND STRUCTURE");
      tf.writeln("");
      if (tagNames.length > 0) {
        tf.writeln("\tSTRUCTURE /MQD_"+keys[i]+"/");
        for (String tagName : tagNames) {
          String typeDef = XmUtil.getFortranMsgTypeDef(tags.getTable(tagName).getS("TYPE"));
          tf.writeln("\t  "+typeDef+"\t"+tagName.toLowerCase());
        }
        tf.writeln("\tEND STRUCTURE");

        mapping.add("\t    MAP");
        mapping.add("\t      RECORD /MQD_"+keys[i]+"/ "+keys[i]);
        mapping.add("\t    END MAP");
      }
      tf.writeln("");
    }
    tf.writeln("\tinteger*4\tL_mqh, L_mqd");
    tf.writeln("\tparameter (L_mqh="+lengthMqh+")");
    tf.writeln("\tparameter (L_mqd = "+lengthMqd+")");
    tf.writeln("");
    tf.writeln("\tSTRUCTURE /MQ_DATA/");
    tf.writeln("\t  UNION");
    for (String mappingStr : mapping) {
      tf.writeln(mappingStr);
    }
    tf.writeln("\t    MAP");
    tf.writeln("\t      byte\tbbuf(L_mqd)");
    tf.writeln("\t    END MAP");
    tf.writeln("\t    MAP");
    tf.writeln("\t      integer*2\tibuf(L_mqd/2)");
    tf.writeln("\t    END MAP");
    tf.writeln("\t    MAP");
    tf.writeln("\t      integer*4\tlbuf(L_mqd/4)");
    tf.writeln("\t    END MAP");
    tf.writeln("\t    MAP");
    tf.writeln("\t      real*4\tfbuf(L_mqd/4)");
    tf.writeln("\t    END MAP");
    tf.writeln("\t    MAP");
    tf.writeln("\t      real*8\tdbuf(L_mqd/8)");
    tf.writeln("\t    END MAP");
    tf.writeln("\t    MAP");
    tf.writeln("\t      character*8\tabuf(L_mqd/8)");
    tf.writeln("\t    END MAP");
    tf.writeln("\t    MAP");
    tf.writeln("\t      character*(L_mqd)\tsbuf");
    tf.writeln("\t    END MAP");
    tf.writeln("\t  END UNION");
    tf.writeln("\tEND STRUCTURE");
    tf.close();
  }

  /** converts filename (root + extension) into name for #ifndef and #define for
      auto generated C and C++ header files. This replaces all dots ('.') with
      underscores ('_') from the filename and prepends the "XM_" prefix string
      and appends an underscore at the end. Everything is UPPER-CASED before returning.
      @param filename root + extension of file name (do NOT include full path)
      @return name that is good for C/C++ #ifndef and #define preprocessor
      @since NeXtMidas 3.1.2
   */
  private static String getPoundDefineName (String filename) {
    return "XM_" + filename.replace('.','_').toUpperCase() + "_";
  }

  /** Convert input string to "char array" string used in C code with the specified
      output len in the form of: {'char0', 'char1', ..., 'charN'}.
      If str is shorter than outlen, then it will be filled with the space char.
      @param str    input string
      @param outlen number to chars in output string
      @since NeXtMidas 3.1.2
   */
  private static String toCharArray (String str, int outlen) {
    StringBuilder sb = new StringBuilder(outlen*4 + 2); // size perfectly for output
    sb.append('{');
    char ch;
    int strlen = str.length();
    ch = (strlen > 0)? str.charAt(0) : ' '; // set first item
    sb.append('\'').append(ch).append('\'');
    for (int i=1; i<outlen; i++) {
      ch = (i < strlen)? str.charAt(i) : ' ';
      sb.append(",'").append(ch).append('\'');
    }
    sb.append('}');
    return sb.toString();
  }


  /** Write C/C++ comments for opt of generated header file for MSGCFG2INC function.
      @param tf  Text file to write to
      @param opt Option tree this is for
      @param configOpts String array of configured option tree in path with messages.cfg
      @since NeXtMidas 3.1.2
   */
  private static void writeCHeaderComments (TextFile tf, String opt, String[] configOpts) {
    tf.writeln("/*");
    tf.writeln(" * Automatically generated by CONVERT "+opt+" MSGCFG2INC");
    tf.writeln(" *");
    tf.writeln(" * Configuration: "+opt);
    for (String option : configOpts) {
      tf.writeln(" *   Option "+option+" from "+Shell.getOptPath(option,"cfg")+"messages.cfg");
    }
    tf.writeln(" */");
  }

  /** Writes out the C/C++ include header for the message declaration file for genmsg.
      @since NeXtMidas 3.1.2
   */
  private static void writeQmessdeclHeader (Midas midas, String xmdisk, Table msgdefs, String opt, String[] configOpts) {
    final String poundDefineName = getPoundDefineName("qmessdecl.h");
    TextFile tf = openFile(midas, xmdisk, "qmessdecl.h", "inc", opt, false);

    tf.writeln("#ifndef "+poundDefineName);
    tf.writeln("");
    writeCHeaderComments(tf, opt, configOpts);
    tf.writeln("");
    tf.writeln("#ifdef __cplusplus");
    tf.writeln("#include \"qmessdef.hh\"");
    tf.writeln("#else");
    tf.writeln("#include \"qmessdef.h\"");
    tf.writeln("#endif");
    tf.writeln("");

    String[] keys = msgdefs.getKeys();
    tf.writeln("#ifdef __cplusplus");
    tf.writeln("");
    for (String key : keys) {
      tf.writeln("static mqh_"+StringUtil.padRight(key,23)+" MQH_"+key+";");
    }
    tf.writeln("");
    tf.writeln("static MQ_HEADER MQH;");
    tf.writeln("static MQ_DATA   MQD;");
    tf.writeln("");
    tf.writeln("#else");
    tf.writeln("");
    for (String key : keys) {
      tf.writeln("static mqh_"+StringUtil.padRight(key,23)+" MQH_"+key+" = mqh_"+key+"_INIT;");
    }
    tf.writeln("");
    tf.writeln("static mq_HEADER MQH;");
    tf.writeln("static mq_DATA   MQD;");
    tf.writeln("");
    tf.writeln("#define mqd MQD.MQD_UNION");
    tf.writeln("#endif");
    tf.writeln("");
    tf.writeln("#define "+poundDefineName);
    tf.writeln("#endif /* "+poundDefineName + " */");
    tf.close();
  }

  /** Writes out the C/C++ include header for the message definition file for genmsg.
      @since NeXtMidas 3.1.2
   */
  private static void writeQmessdefHeader (Midas midas, String xmdisk, Table msgdefs, String opt, String[] configOpts, boolean cpp) {
    final String outfilename     = "qmessdef" + (cpp? ".hh" : ".h");
    final String poundDefineName = getPoundDefineName(outfilename);
    TextFile tf = openFile(midas, xmdisk, outfilename, "inc", opt, false);
    final int INFO   = 0; // Value for info field.
    final int MODE   = 0; // Value for mode field.
    final int SENDER = 0; // Value for sender field.
    final int READER = 0; // Value for reader field.

    ArrayList<String> mapping = new ArrayList<>();
    String[] keys = msgdefs.getKeys();
    int lengthMqh = 0;
    int lengthMqd = 0;

    if (cpp) tf.writeln("// -*- C++ -*-");
    tf.writeln("#ifndef "+poundDefineName);
    tf.writeln("");
    writeCHeaderComments(tf, opt, configOpts);
    tf.writeln("");
    if (cpp) tf.writeln("#include <queue.h>");
    else     tf.writeln("#include \"queue.h\"");
    tf.writeln("");
    for (String key : keys) {
      Table    def      = msgdefs.getTable(key);
      Table    tags     = def.getTable("TAGS");
      String[] tagNames = tags.getKeys();
      int      ndata    = tagNames.length;
      char     type     = (def.getS("TYPE", "")+" ").charAt(0);
      int      nslots   = def.getI("NSLOTS");
      int      nhslots  = def.getI("NHSLOTS");
      String   name     = def.getS("NAME");
      String   alias    = def.getS("INFOALIAS", "info").toLowerCase();
      String   types    = def.getS("TYPES", "");
      int      ntype    = types.length();

      lengthMqh = Math.max(lengthMqh, nhslots*8);
      lengthMqd = XmUtil.LENGTH_MQD; // Always use theoretical max data size (same as X-Midas)

      if (cpp) { // for C++ header
        tf.writeln("");
        tf.writeln("struct mqh_"+key+" : public MQ_HEADER {");
        if (!"info".equals(alias)) {
          tf.writeln("  int_4& "+alias+"; // .info field alias");
          tf.writeln("  mqh_"+key+" () : ");
          tf.writeln("  "+alias+"(info)");
        } else {
          tf.writeln("  mqh_"+key+" ()");
        }
        tf.writeln("  {");
        tf.writeln("    nslots = "+nslots+";");
        tf.writeln("    type = '"+type+"';");
        tf.writeln("    nhslots = "+nhslots+";");
        tf.writeln("    ndata = "+ndata+";");
        tf.writeln("    "+alias+" = "+INFO+";"); // info -OR- infoalias
        tf.writeln("    mode = "+MODE+";");
        tf.writeln("    name = \""+name+"\";");
        if (type == 'H') {
          tf.writeln("    ntype = "+ntype+";");
          tf.writeln("    types = \""+types+"\";");
        }
        tf.writeln("  }");
        tf.writeln("};");
      }
      else {    // for C Header
        tf.writeln("");
        tf.writeln("typedef struct");
        tf.writeln("{");
        tf.writeln("  int_4        reader;");
        tf.writeln("  int_2        nslots;");
        tf.writeln("  char         type;");
        tf.writeln("  int_1        nhslots;");
        tf.writeln("  int_4        sender;");
        tf.writeln("  int_4        ndata;");
        tf.writeln("  int_4        "+alias+";"); // info -OR- infoalias
        tf.writeln("  int_4        mode;");
        tf.writeln("  char         name[8];");

        if (type == 'H') {
          tf.writeln("  int_2        ntype;");
          tf.writeln("  char         types[86];");
        }
        tf.writeln("} mqh_"+key+";");
        tf.writeln("");
        tf.writeln("#define mqh_"+key+"_INIT {"+READER+","+nslots+","+type+","+nhslots+","
           +SENDER+","+ndata+","+INFO+","+MODE+","+toCharArray(name,8)+","+ntype+","+toCharArray(types,86));
      }
      tf.writeln("");
      if (tagNames.length > 0) {
        if (cpp) tf.writeln("struct MQD_"+key+" {");
        else     tf.writeln("typedef struct \n{");

        for (String tagName : tagNames) {
          String tagType = tags.getTable(tagName).getS("TYPE");
          String typeDef = XmUtil.getCMsgTypeDef(tagType, tagName.toLowerCase(), cpp);
          tf.writeln("  "+typeDef+";");
        }
        if (cpp) tf.writeln("};");
        else     tf.writeln("} MQD_"+key+";");

        mapping.add("      MQD_"+StringUtil.padRight(key,23)+" "+key+";");
      }
      tf.writeln("");
    } // end for (String key : keys) loop
    tf.writeln("");
    tf.writeln("#define L_mqh "+lengthMqh); // const int_4
    tf.writeln("#define L_mqd "+lengthMqd); // const int_4
    tf.writeln("");
    tf.writeln("");
    if (cpp) {
      tf.writeln("union MQ_DATA {");
    }
    else {
      tf.writeln("typedef struct");
      tf.writeln("{");
      tf.writeln("  union");
      tf.writeln("  {");
    }

    for (String mappingStr : mapping) {
      tf.writeln(mappingStr);
    }
    tf.writeln("");
    tf.writeln("      int_1                   bbuf[L_mqd];");
    tf.writeln("      int_2                   ibuf[L_mqd/2];");
    tf.writeln("      int_4                   lbuf[L_mqd/4];");
    tf.writeln("      real_4                  fbuf[L_mqd/4];");
    tf.writeln("      real_8                  dbuf[L_mqd/8];");
    if (cpp) {
      tf.writeln("      fstring<8>              abuf[L_mqd/8];");
      tf.writeln("      fstring<L_mqd>          sbuf;");
      tf.writeln("}; // end union MQ_DATA");
    }
    else {
      tf.writeln("      char                    abuf[L_mqd];");
      tf.writeln("      char                    sbuf[L_mqd];");
      tf.writeln("  } MQD_UNION;");
      tf.writeln("#ifdef __cplusplus");
      tf.writeln("} MQ_DATA;");
      tf.writeln("#else");
      tf.writeln("} mq_DATA;");
      tf.writeln("#endif");
    }

    tf.writeln("");
    tf.writeln("#define "+poundDefineName);
    tf.writeln("#endif /* "+poundDefineName + " */");
    tf.close();
  } //  end writeQmessdefHeader

}
