package nxm.sys.libxm;

import static nxm.sys.lib.XmUtil.LENGTH_MQD;
import static nxm.sys.lib.XmUtil.LENGTH_MQH;
import static nxm.sys.lib.XmUtil.LENGTH_MQH_H;
import static nxm.sys.lib.XmUtil.SLOTS_MQH;
import static nxm.sys.lib.XmUtil.SLOTS_MQH_H;
import static nxm.sys.lib.XmUtil.getBytesForMsgType;

import java.util.ArrayList;
import java.util.Hashtable;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Message;
import nxm.sys.lib.Message.XmMsgType;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Native;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.XMValue;
import nxm.sys.lib.XmUtil;

/** Utilities useful for converting X-Midas messages to NeXtMidas messages and NeXtMidas
    messages to X-Midas messages. This is independent from all other XBC and libxm libraries
    (other than {@link XmUtil} so that it can be used from XBC or when running NeXtMidas as
    an X-Midas option tree.<br>
    <span style="color:red">Do NOT use this class directly, this class is
    for use by the SYS option tree ONLY and is subject to change without notice!</span>

    @author  J. Czechowski IV
    @since   NeXtMidas 2.1.1
 */
@InternalUseOnly
public final class XmMsgHandler {
  /** FYI: zero-length arrays are immutable. For more info see Effective Java Item 27:  */
  private static final String[] EMPTY_STRING_ARRAY = new String[0];

  // The next 5 lines must exactly match messages.cfg and NM_NDATA must equal the number of entries
  private static final String NM_NAME_TYPE    = "S[36]";
  private static final String NM_INFO_TYPE    = "L";
  private static final String NM_DATA_TYPE    = "S[4096]";
  private static final String NM_FID_TYPE     = "S[32]";
  private static final String NM_TID_TYPE     = "S[32]";
  private static final int    NM_NDATA        = 5;
  private static final String NM_TYPES        = NM_NAME_TYPE + NM_INFO_TYPE + NM_DATA_TYPE + NM_FID_TYPE + NM_TID_TYPE;
  private static final int    NM_NAME_LENGTH  = XmUtil.getBytesForMsgType(NM_NAME_TYPE);
  private static final int    NM_INFO_LENGTH  = XmUtil.getBytesForMsgType(NM_INFO_TYPE);
  private static final int    NM_DATA_LENGTH  = XmUtil.getBytesForMsgType(NM_DATA_TYPE);
  private static final int    NM_FID_LENGTH   = XmUtil.getBytesForMsgType(NM_FID_TYPE);
  private static final int    NM_TID_LENGTH   = XmUtil.getBytesForMsgType(NM_TID_TYPE);
  private static final int    NM_NAME_OFFSET  = 0;
  private static final int    NM_INFO_OFFSET  = NM_NAME_OFFSET + NM_NAME_LENGTH;
  private static final int    NM_DATA_OFFSET  = NM_INFO_OFFSET + NM_INFO_LENGTH;
  private static final int    NM_FID_OFFSET   = NM_DATA_OFFSET + NM_DATA_LENGTH;
  private static final int    NM_TID_OFFSET   = NM_FID_OFFSET  + NM_FID_LENGTH;
  private static final int    NM_LENGTH       = NM_TID_OFFSET  + NM_TID_LENGTH;


  /** All handlers (NeXtMidas)        */ private static final Table     nmHandlers = new Table();
  /** All handlers (X-Midas)          */ private static final Table     xmHandlers = new Table();
  /** The current option tree         */ private        final String    opt;
  /** Running from X-Midas?           */ private        final boolean   xmidas;
  /** The message definitions         */ private              Table     msgDefs    =        null;
  /** Maps message name to definition */ private Hashtable<String,Table> mapping   =        null;

  /** Creates a new instance.
      @param opt The option tree the message handler will use the definition for.
   */
  private XmMsgHandler (String opt, boolean xmidas) {
    this.opt    = opt;
    this.xmidas = xmidas;
  }

  /** Gets the handler to use for a given option tree.
      @param opt The option tree the message handler will use the definition for.
      @return A message handler for the given option.
   */
  public static XmMsgHandler getHandlerFor (String opt) {
    return getHandlerFor(opt,false);
  }

  /** Gets the handler to use for a given option tree.
      @param opt The option tree the message handler will use the definition for.
      @return A message handler for the given option.
   */
  static XmMsgHandler getHandlerFor (String opt, boolean xmidas) {
    opt = opt.toUpperCase();

    XmMsgHandler handler;
    if (xmidas) handler = (XmMsgHandler)xmHandlers.get(opt);
    else        handler = (XmMsgHandler)nmHandlers.get(opt);

    if (handler == null) {
      handler = new XmMsgHandler(opt,xmidas);

      if (xmidas) xmHandlers.setKey(opt, handler);
      else        nmHandlers.setKey(opt, handler);
    }

    return handler;
  }


  /** Gets the message definition for a given name.
      @param name The name of the message.
      @return The appropriate message definition or null if none is found.
   */
  private Table getMsgDef (String name) {
    if (msgDefs == null) {
      String fname = (xmidas)? "qmessages.tbl": "messages.tbl";
      String pkgName = "nxm."+opt.toLowerCase()+".cfg."+fname;
      msgDefs = Convert.o2t(pkgName, Shell.getSharedMidasContext());
      if (msgDefs == null) {
        throw new MidasException("Unable to find/convert message definition file = '"
                                 +pkgName+"'. Run GENMSGTBL <option> to create.");
      }
      mapping = new Hashtable<>(msgDefs.size());

      String[] keys = msgDefs.getKeys();
      for (int i = 0; i < keys.length; i++) {
        Table def = msgDefs.getTable(keys[i]);
        mapping.put(def.getS("NAME"), def);
      }
    }

    return mapping.get(name);
  }

  /** Converts a configured X-Midas message to a NeXtMidas message.
      @param msg The X-Midas message.
      @return A NeXtMidas message corresponding to this X-Midas message.
   */
  public final Message xmMsgToNm (XmMessage msg) {
    return xmMsgToNm(msg.getHeader(), msg.getData());
  }

  /** Converts a configured X-Midas message to a NeXtMidas message.
      @param header The message header.
      @param data   The message data.
      @return A NeXtMidas message corresponding to this X-Midas message.
   */
  public Message xmMsgToNm (byte[] header, byte[] data) {
    if (header   ==   null) throw new MidasException("Invalid null header for X-Midas message.");
    if (header.length < 32) throw new MidasException("Invalid header length for X-Midas message.");

    String name = Convert.unpackS(header, 24, 8);    // message name string
    if (name.equals("NM")) return fromTranslatedXmMsg(header, data);

    Table def = getMsgDef(name);
    if (def != null) return fromConfiguredXmMsg(header, data, def);

    return fromUnconfiguredXmMsg(header, data);
  }

  /** Converts a configured X-Midas message to a NeXtMidas message.
      @param msg The NeXtMidas message.
      @return A NeXtMidas message corresponding to this X-Midas message.
   */
  public XmMessage nmMsgToXm (Message msg) {
    XmMsgType xmt = msg.getXmMessageType();
    Table     def = null;

    if (xmt == XmMsgType.AUTO) {
      def = getMsgDef(msg.getName());
      xmt = (def != null)?  XmMsgType.CONFIGURED :  XmMsgType.UNCONFIGURED;
    }
    else if (xmt == XmMsgType.CONFIGURED) { // 2011-03-17 Need to get message definition for CONFIGURED Messages (BUG 2370)
      def = getMsgDef(msg.getName());
    }

    switch (xmt) {
      case NEXTMIDAS:    return toTranslatedXmMsg(msg);
      case CONFIGURED:   return toConfiguredXmMsg(msg,def);
      case UNCONFIGURED: return toUnconfiguredXmMsg(msg);
      default: throw new MidasException("Illegal X-Midas message type "+xmt+".");
    }
  }

  /** Queries the handler for information about a given message.
      @param name The name of the message.
      @return An array of the lines to display or null if the message is not found.
   */
  public String[] queryMessage (String name) {
    String[] retVal = null;
    Table    def    = getMsgDef(name);

    if (def != null) {
      ArrayList<String> lines = new ArrayList<>();
      Table    keys      = def.getTable("TAGS");
      String[] keyNames  = keys.getKeys();
      String   infoAlias = def.getS("INFOALIAS", null);
      char     type      = def.getS("TYPE").charAt(0);

      lines.add("Reader         : " + "n/a");
      lines.add("NSlots         : " + def.getI("NSLOTS"));
      lines.add("Type           : " + type);
      lines.add("NHSlots        : " + def.getI("NHSLOTS"));
      lines.add("Sender         : " + "n/a");
      lines.add("NData          : " + keyNames.length);
      lines.add("Info           : " + "n/a");
      if (infoAlias != null) {
        lines.add("  InfoAlias    : " + infoAlias);
      }
      lines.add("Mode           : " + "n/a");
      lines.add("Name           : " + def.getS("NAME"));
      if (type == 'H') {
        String types = def.getS("TYPES");
        lines.add("NType          : " + types.length());
        lines.add("Types          : " + types);
      }
      if (keyNames.length > 0) {
        lines.add("Data           : ");
        for (int i = 0; i < keyNames.length; i++) {
          Table keyDef = keys.getTable(keyNames[i]);
          String comment = keyDef.getS("COMMENT", "");

          if (comment.length() > 0) { comment = "  ! "+comment; }

          lines.add("    "+StringUtil.padRight(keyDef.getS("TYPE")+":",6)
                          +StringUtil.padRight(keyNames[i],9)+comment);
        }
      }

      retVal = lines.toArray(EMPTY_STRING_ARRAY);
    }

    return retVal;
  }

  /** Converts an un-configured message data to an object. This is only used by xmMsgToNm(..) */
  private Message fromTranslatedXmMsg (byte[] header, byte[] data) {
    TranslatedMessage m = new TranslatedMessage(header, data);
    return m.toNmMsg();
  }

  /** Converts a configured message data to a table. This is only used by xmMsgToNm(..) */
  private Message fromConfiguredXmMsg (byte[] header, byte[] data, Table def) {
    XmMessage m        = new XmMessage(header, data);
    Table     tbl      = new Table();
    Table     keys     = def.getTable("TAGS");
    String[]  keyNames = keys.getKeys();
    int       offset   = 0;

    for (int i = 0; i < keyNames.length; i++) {
      String type  = keys.getTable(keyNames[i]).getS("TYPE");
      Object value = unpack(type, data, offset);

      tbl.setKey(keyNames[i], value);
      offset += getBytesForMsgType(type);
    }
    return m.toNmMsg(tbl);
  }

  /** Converts an un-configured message data to a table. This is only used by xmMsgToNm(..) */
  private Message fromUnconfiguredXmMsg (byte[] header, byte[] data) {
    XmMessage m       = new XmMessage(header, data);
    Table     tbl     = new Table();
    int       offset  = 0;
    String    types   = m.getTypes();
    String    typeStr = Character.toString(m.getType());
    String[]  expandedTypes = EMPTY_STRING_ARRAY;

    if (types == null) { types = typeStr; }

    if (m.getType() == 'H') {
      expandedTypes = expandTypesStr(types);
      typeStr = "A";
    }
    for (int i = 0; i < m.getNData(); i++) {
      if (i < expandedTypes.length) typeStr = expandedTypes[i];
      Object val = unpack(typeStr, data, offset);
      String key = "VAL"+(i+1);

      tbl.setKey(key, val);
      offset += getBytesForMsgType(typeStr);
    }
    return m.toNmMsg(tbl);
  }

  /** Expands the X-Midas types string for non-configured messages to remove any multipliers. This
      should only be called by xmMsgDataToTable(..).
   */
  private String[] expandTypesStr (String types) {
    ArrayList<String> list = new ArrayList<>();
    int mult = 0; // The multiplier.

    for (int i = 0; i < types.length(); i++) {
      char type  = types.charAt(i);
      int  open  = types.indexOf('[',i);
      int  close = types.indexOf(']',i);

      if ((type >= '0') && (type <= '9')) {
        mult = (mult * 10) + (type - '0');
      } else {
        String str = Character.toString(type);

        if (type == 'S') {
          if (open == i+1) {
            str = types.substring(i, close+1);
            i   = close;
          } else {
            str = "S[1]";
          }
        }

        list.add(str);
        for (int j = 1; j < mult; j++) {
          list.add(str);
        }
        mult = 0;
      }
    }
    if (mult != 0) {
      Shell.warning("XmMsgHandler: Bad types specifier '"+types+"' given.");
    }
    return list.toArray(EMPTY_STRING_ARRAY);
  }

  /** Converts a table to a message data. This is only used by nmMsgToXm(..) */
  private XmMessage toTranslatedXmMsg (Message msg) {
    TranslatedMessage m = new TranslatedMessage(msg);
    return (m.error)? null : m;
  }

  /** Converts a table to a message data. This is only used by nmMsgToXm(..) */
  private XmMessage toUnconfiguredXmMsg (Message msg) {
    Table  tbl     = Convert.o2t(msg.data);
    short  nslots  = SLOTS_MQH;
    char   type    = ' ';
    int    ndata   = 0;
    String types   = null;
    byte[] data    = new byte[LENGTH_MQD];

    if (tbl != null) {
      types = tbl.getS("TYPE", "");
      ndata = tbl.getL("ARGS", -1);

      if ((types == null) || (types.length() == 0)) {
        Shell.warning("XmMsgHandler: Invalid type definition TYPE='' for '"+msg.name+"' message, using TYPE='A'");
        types = String.valueOf((char)DataTypes.ASCII);
      }
      if (ndata < 1) {
        Shell.warning("XmMsgHandler: Invalid number of arguments ARGS="+ndata+" for '"+msg.name+"' message, using ARGS='1'");
        ndata = 1;
      }
      String typeStr = String.valueOf((char)DataTypes.ASCII);;
      int    offset  = 0;

      String[] expandedTypes = expandTypesStr(types);

      for (int i = 0; i < ndata; i++) {
        if (i < expandedTypes.length) { typeStr = expandedTypes[i]; }

        Object value = tbl.get("VAL"+(i+1));

        pack(typeStr, data, offset, value);
        offset += getBytesForMsgType(typeStr);
      }

      if (types.length() > 1) {
        nslots  = (short)((offset+7)/8 + SLOTS_MQH_H);
        type    = 'H';
      }
      else {
        nslots  = (short)((offset+7)/8 + SLOTS_MQH);
        type    = types.charAt(0);
        types   = null;
      }
    }

    return new XmMessage(msg, nslots, type, ndata, types, data);
  }

  /** Converts a table to a message data. This is only used by nmMsgToXm(..)
      @param msg Message to convert to configured X-Midas message
      @param def definition of configured messages (i.e. from qmessages.tbl)
   */
  private XmMessage toConfiguredXmMsg (Message msg, Table def) {
    Table  tbl     = Convert.o2t(msg.data);
    Table  tags    = def.getTable("TAGS");
    short  nslots  = def.getI("NSLOTS");
    char   type    = def.getS("TYPE").charAt(0);
    int    ndata   = (tags==null)? 0: tags.size();
    String types   = (type != 'H')? null : def.getS("TYPES");
    byte[] data    = new byte[LENGTH_MQD];

    int offset = 0;
    if (ndata > 0) { // 2011-03-17 prevent possible NPE when tags is null
      for (String key : tags.getKeys()) {
        String t = tags.getTable(key).getS("TYPE");
        Object v = tbl.get(key);

        pack(t, data, offset, v);
        offset += getBytesForMsgType(t);
      }
    }

    return new XmMessage(msg, nslots, type, ndata, types, data);
  }

  /** Unpacks an element from the data buffer.
      @param type    The type identifier (in the format used by X-Midas message definitions).
      @param data    The data buffer.
      @param offset  The offset into the buffer.
      @return The given data element.
   */
  private static Object unpack (String type, byte[] data, int offset) {
    switch (type.charAt(0)) {
      case DataTypes.BYTE:     return Convert.unpackB(data, offset);
      case DataTypes.INT:      return Convert.unpackI(data, offset);
      case DataTypes.LONG:     return Convert.unpackL(data, offset);
//TBD case 'X': return Convert.unpackX(data, offset);
      case DataTypes.FLOAT:    return Convert.unpackF(data, offset);
      case DataTypes.DOUBLE:   return Convert.unpackD(data, offset);
      case DataTypes.ASCII:    return Convert.unpackS(data, offset, 8);
      case DataTypes.ASCII256: return Convert.unpackS(data, offset, 256); // 3.9.4 NXM-3570
      case 'U': // FALLTHROUGH: same as 'S'
      case DataTypes.STRING:   return Convert.unpackS(data, offset, getBytesForMsgType(type));
      default:  throw new MidasException("XmMsgHandler.unpack: Unknown data type '"+type+"'.");
    }
  }

  /** Packs an element from the data buffer.
      @param types   The type identifier (in the format used by X-Midas message definitions).
      @param data    The data buffer.
      @param offset  The offset into the buffer.
      @param value   The data to insert.
   */
  private static void pack (String types, byte[] data, int offset, Object value) {
    char type = types.charAt(0);

    if (type == 'Z') {
      type  = DataTypes.ASCII;
      value = (Convert.o2z(value))? "TRUE" : "FALSE";
    }

    switch (type) {
      case DataTypes.BYTE:   Convert.packB(data, offset, Convert.o2b(value));                             break;
      case DataTypes.INT:    Convert.packI(data, offset, Convert.o2i(value));                             break;
      case DataTypes.LONG:   Convert.packL(data, offset, Convert.o2l(value));                             break;
//tbd      case 'X': Convert.packX(data, offset, Convert.o2x(value));                             break;
      case DataTypes.FLOAT:  Convert.packF(data, offset, Convert.o2f(value));                             break;
      case DataTypes.DOUBLE: Convert.packD(data, offset, Convert.o2d(value));                             break;
      case DataTypes.ASCII:  Convert.packS(data, offset, 8, Convert.o2s(value));                          break;
      case 'U': // FALLTHROUGH: same as 'S'
      case DataTypes.STRING: Convert.packS(data, offset, getBytesForMsgType(types), Convert.o2s(value));  break;
      default:  throw new MidasException("XmMsgHandler.pack: Unknown data type '"+types+"'.");
    }
  }

  /** Creates a new XmMessage from native pointers. Only HPrimitive in XBC should use this!
   *  @param headerPointer Native pointer to the header.
   *  @param dataPointer   Native pointer to the data
   *  @return the new XmMessage
   */
  public static XmMessage readNativeXmMsg (long headerPointer, long dataPointer) {
    return new XmMessage(headerPointer, dataPointer);
  }

  /** Writes an XmMessage to native pointers. Only HPrimitive in XBC should use this!
   *  @param xmMessage     XmMessage to be written
   *  @param headerPointer Native pointer to the header
   *  @param dataPointer   Native pointer to the data
   *  @param maxdata       Data length
   */
  public static void writeNativeXmMsg (XmMessage xmMessage, long headerPointer, long dataPointer, int maxdata) {
    xmMessage.copyToNative(headerPointer, dataPointer, maxdata);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Inner Classes
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Wrapper for an X-Midas message.<br>
      <span style="color:red">Do NOT use this class directly, this class is for use by the SYS and
      XBC option trees ONLY and is subject to change without notice!</span>
   */
  @InternalUseOnly
  public static class XmMessage {
    protected byte[] header; // The header.
    protected byte[] data;   // The data.

    /** Creates a new X-Midas message.
        @param header The header buffer.
        @param data   The data buffer.
     */
    XmMessage (byte[] header, byte[] data) {
      this.header = header;
      this.data   = data;
    }

    /** Creates a new X-Midas message.
        @param msg     NeXtMidas message to send to X-Midas.
        @param nslots  The total number of slots (header + data). (-1=Auto)
        @param type    Basic data type ('H' for heterogenous).
        @param ndata   The number of data elements.
        @param types   The non-homogeneous types specifier.
        @param data    The message data.
     */
    XmMessage (Message msg, int nslots, char type, int ndata, String types, byte[] data) {
      this(msg.getName(), msg.getInfo(), msg.getFromID(), nslots, type, ndata, types, data);
    }

    /** Creates a new X-Midas message.
        @param name    Message name to send to X-Midas.
        @param info    Message info to send to X-Midas.
        @param from    Message from string to send to X-Midas.
        @param nslots  The total number of slots (header + data). (-1=Auto)
        @param type    Basic data type ('H' for heterogenous).
        @param ndata   The number of data elements.
        @param types   The non-homogeneous types specifier.
        @param data    The message data.
     */
    XmMessage (String name, int info, String from, int nslots, char type, int ndata, String types, byte[] data) {
      int    sender  = (StringUtil.isInteger(from))? Convert.o2l(from) : 0;
      byte   nhslots = (type == 'H')? SLOTS_MQH_H : SLOTS_MQH;

      if (nslots < 0) {
        nslots = (((data.length+7)/8) + nhslots);
      }

      // Basic Error Checking
      if (name.length() > 8) {
        Shell.warning("Messsage name '"+name+"' exceeds X-Midas's 8-character limit, using '"
                     +name.substring(0,8)+"'.");
      }
      if ((type == 'H') && (types.length() > 86)) {
        throw new MidasException("Types string '"+types+"' for XM message '"+name+"' is too long.");
      }

      // Initialization
      this.header = new byte[nhslots*8];
      this.data   = data;

      // Set Header Fields
      Convert.packL(header,  0, 0); // not used
      Convert.packI(header,  4, (short)nslots);
      Convert.packB(header,  6, (byte)type);
      Convert.packB(header,  7, nhslots);
      Convert.packL(header,  8, sender);
      Convert.packL(header, 12, ndata);
      Convert.packL(header, 16, info);
      Convert.packL(header, 20, 0); // for future use
      Convert.packS(header, 24, 8, name);
      if (type == 'H') {
        Convert.packI(header, 32, (short)types.length());
        Convert.packS(header, 34, 86, types);
      }
    }

    /** Creates a new X-Midas message. The number of bytes to copy will be automatically determined.
        @param headerPointer Native pointer to the header.
        @param dataPointer   Native pointer to the data.
     */
    XmMessage (long headerPointer, long dataPointer) {
     this(headerPointer, -1, dataPointer, -1);
    }

    /** Creates a new X-Midas message.
        @param headerPointer Native pointer to the header.
        @param headerLength  Length of the header in bytes (-1 to automatically determine).
        @param dataPointer   Native pointer to the data.
        @param dataLength    Length of the data in bytes (-1 to automatically determine).
     */
    XmMessage (long headerPointer, int headerLength, long dataPointer, int dataLength) {
      if (headerLength < 0) {
        header = new byte[LENGTH_MQH]; // This is the min length.
        Native.p2ja(headerPointer, 0, header, 0, header.length);

        if (Convert.unpackB(header, 6) == 'H') { // Longer is length needed for type='H'
          header = new byte[LENGTH_MQH_H];
          Native.p2ja(headerPointer, 0, header, 0, header.length);
        }
      }
      else {
        header = new byte[headerLength];
        Native.p2ja(headerPointer, 0, header, 0, header.length);
      }

      if (dataLength < 0) {
        byte  nhslots = Convert.unpackB(header, 7);
        short nslots  = Convert.unpackI(header, 4);

        dataLength = Math.min(LENGTH_MQD, (nslots-nhslots)*8);
      }

      data = new byte[dataLength];
      Native.p2ja(dataPointer, 0, data, 0, dataLength);
    }

    /** Gets the header.
     *  @return the header */
    public byte[] getHeader() { return header; }

    /** Gets the data.
     *  @return the data */
    public byte[] getData() { return data; }

    /** Copies the data to a native pointer.
        @param headerPointer Native pointer to the header.
        @param dataPointer   Native pointer to the data.
     */
    void copyToNative (long headerPointer, long dataPointer) {
      copyToNative(headerPointer, dataPointer, LENGTH_MQD);
    }

    /** Copies the data to a native pointer.
        @param headerPointer Native pointer to the header.
        @param dataPointer   Native pointer to the data.
     */
    void copyToNative (long headerPointer, long dataPointer, int dataLength) {
      byte  nhslots = Convert.unpackB(header, 7);
      short nslots  = Convert.unpackI(header, 4);
      int   bytes   = Math.min(Math.min(LENGTH_MQD, dataLength),
                               Math.min(data.length, (nslots-nhslots)*8));

      Native.ja2p(header, 0, headerPointer, 0, header.length);
      Native.ja2p(data,   0, dataPointer,   0, bytes);
    }

    /** Converts a Table representation of an X-Midas Message into a NeXtMidas Message
     *  @param data the Table representation of an X-Midas Message
     *  @return the new NeXtMidas Message
     */
    public Message toNmMsg (Object data) {
      return new Message(getName(), getInfo(), data, getTo(), getFrom());
    }

    /** Get who the message is to
     *  @return the reader id
     */
    public int    getReader  () { return Convert.unpackL(header,  0); }
    /** msg to id, get # slots for entire msg
     *  @return the number of slots for the entire message
     */
    public short  getNSlots  () { return Convert.unpackI(header,  4); }
    /** Get message type
     *  @return the message type
     */
    public char   getType    () { return (char)Convert.unpackB(header,  6); }
    /** Get # slots for msg header only
     *  @return the number of slots for the message header only
     */
    public byte   getNHSlots () { return Convert.unpackB(header,  7); }
    /** Get who the message is from
     *  @return the sender id
     */
    public int    getSender  () { return Convert.unpackL(header,  8); }
    /** Get # data values
     *  @return the number of data values
     */
    public int    getNData   () { return Convert.unpackL(header, 12); }
    /** Get mesg data info field
     *  @return the message data info field
     */
    public int    getInfo    () { return Convert.unpackL(header, 16); }
    /** Get mesg data mode field
     *  @return the message data mode field
     */
    public int    getMode    () { return Convert.unpackL(header, 20); }
    /** Get message name string
     *  @return the message name string
     */
    public String getName    () { return Convert.unpackS(header, 24, 8); }
    /** Get the number of types in the message
     *  @return the number of types
     */
    public short  getNType   () { return (getType() != 'H')?    0 : Convert.unpackI(header, 32); }
    /** Get the types in the message
     *  @return the types in the message
     */
    public String getTypes   () { return (getType() != 'H')? null : Convert.unpackS(header, 34, getNType()); }
    /** Calls {@link XmUtil#getXmMessageHandler(Object)} to get who the message is from
     *  @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 sender ID
     */
    public Object getFrom    () { return XmUtil.getXmMessageHandler("XM."+getSender()); }
    /** Calls {@link XmUtil#getXmMessageHandler(Object)} to get who the message is to
     *  @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 reader ID
     */
    public Object getTo      () { return XmUtil.getXmMessageHandler("XM."+getReader()); }
  }

  /** Wrapper for a translated X-Midas message.<br>
      <span style="color:red">Do NOT use this class directly, this class is for use by the SYS and
      XBC option trees ONLY and is subject to change without notice!</span>
   */
  @InternalUseOnly
  public static class TranslatedMessage extends XmMessage {
    private boolean error;

    TranslatedMessage (byte[] header, byte[] data) {
      super(header, data);
      this.error = false;
    }

    TranslatedMessage (Message msg) {
      super("NM", 0, msg.getFromID(), -1, 'H', NM_NDATA, NM_TYPES, new byte[NM_LENGTH]);
      this.error = false;

      Object msgData = msg.getData();
      XMValue val = XMValue.toXMValue(msgData);
      String  str = val.toString();
      // XMValue.toXMValue can return the same object if it is already an XMValue
      // Otherwise, a new XMValue is created which could be an XMValueNative
      // Call delete to be safe and free any native pointer
      if (msgData != val) val.delete();

      if (msg.getName().length() > NM_NAME_LENGTH) {
        Shell.warning("XmMsgHandler: Message name '"+msg.getName()+"' exceeds max length of "+NM_NAME_LENGTH
                    + " characters and can not be sent to X-Midas.");
        error = true;
      }
      if (str.length() > NM_DATA_LENGTH) {
        Shell.warning("XmMsgHandler: Message NAME='"+msg.getName()+"' INFO="+msg.getInfo()+" "
                    + "DATA='"+msg.getData()+"' exceeds max serialized length of "+NM_DATA_LENGTH+" bytes "
                    + "and can not be sent to X-Midas.");
        error = true;
      }
      Convert.packS(data, NM_NAME_OFFSET, NM_NAME_LENGTH, msg.getName());
      Convert.packL(data, NM_INFO_OFFSET,                 msg.getInfo());
      Convert.packS(data, NM_DATA_OFFSET, NM_DATA_LENGTH, str);
      Convert.packS(data, NM_FID_OFFSET,  NM_FID_LENGTH,  msg.getFromID());
      Convert.packS(data, NM_TID_OFFSET,  NM_TID_LENGTH,  msg.getToID());
    }

    /** Get NeXtMidas Message name
     *  @return the NeXtMidas Message name
     */
    public String getNmName () { return Convert.unpackS(data, NM_NAME_OFFSET, NM_NAME_LENGTH); }
    /** Get NeXtMidas Message info
     *  @return the NeXtMidas Message info
     */
    public int    getNmInfo () { return Convert.unpackL(data, NM_INFO_OFFSET); }
    /** Get NeXtMidas Message data
     *  @return the NeXtMidas Message data
     */
    public String getNmData () { return Convert.unpackS(data, NM_DATA_OFFSET, NM_DATA_LENGTH); }
    /** Get NeXtMidas Message from (sender) id
     *  @return the NeXtMidas from (sender) id
     */
    public String getNmFID  () { return Convert.unpackS(data, NM_FID_OFFSET,  NM_FID_LENGTH);  }
    /** Get NeXtMidas Message to (reader) id
     *  @return the NeXtMidas Message to (reader) id
     */
    public String getNmTID  () { return Convert.unpackS(data, NM_TID_OFFSET,  NM_TID_LENGTH);  }

    /** Get NeXtMidas Message sender as an Object
     *  @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 Object getNmFrom () {
      if (getNmFID().length() > 0) {
        // This is still experimental!
        Shell.warning("XmMsgHandler: msg_fid='"+getNmFID()+"' not supported at the time.");
        return XmUtil.getXmMessageHandler("XM."+getNmFID());
      }
      return getFrom();
    }

    /** Get NeXtMidas Message sender as an Object
     *  @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 Object getNmTo () {
      if (getNmTID().length() > 0) {
        // This is still experimental!
        Shell.warning("XmMsgHandler: msg_tid='"+getNmTID()+"' not supported at the time.");
        return XmUtil.getXmMessageHandler("XM."+getNmTID());
      }
      return getTo();
    }

    /** Returns a newly-converted NeXtMidas Message
     *  @return the newly-converted NeXtMidas Message
     */
    public Message toNmMsg () {
      return new Message(getNmName(), getNmInfo(), getNmData(), getNmTo(), getNmFrom());
    }

    public String toString () {
      return "TranslatedMessage: "
           + " name = "+getNmName()
           + " info = "+getNmInfo()
           + " data = "+getNmData()
           + " fid = "+getNmFID()
           + " tid = "+getNmTID();
    }
  }
}
