package nxm.sys.net;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import nxm.sys.inc.Commandable;
import nxm.sys.inc.Constants;
import nxm.sys.inc.DataTypes;
import nxm.sys.inc.IDable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.LimitedFutureSupport;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.RmifInterface;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.CoreIO;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.MidasThread;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;

/** Access point for all RMIF communications into and out of this Java
    process. Each instance of this class contains a set of Remote objects that
    serve as connection points to remote nodes. RMIF packets are sent and
    received through this class to and from the remote nodes.  When an
    instance of this class is instantiated, it opens a single UDP socket on
    the local machine and begins listening for PDUs on that socket.<br>
    <br>
    Each datagram that is received on that socket is interpreted as an RMIF
    packet.  Each packet contains an opcode (instruction) along with
    parameters for that instruction.  These opcodes are converted into
    Messages and delivered to the listening processes.<br>
    <br>
    The following is the layout of an RMIF packet:
    <pre>
                  0                          31
                  +------+------+------+------+  ---
                  | func | flag | info | rep  |   |
                  +------+------+------+------+   | Main Header (8 bytes)
                  | seq  | try  | rpt  | adj  |   |
                  +------+------+------+------+  ---
                  |                           |   |
                  :       adjunct header      :   | Adjunct Header
                  |         (optional)        |   | (max of 127 bytes)
                  +---------------------------+  ---
                  |                           |   |
                  :            data           :   | Data
                  |                           |   | (max of 32 KiB bytes)
                  +---------------------------+  ---

        Main Header:
          byte func - Function opcode (see list below).
          byte flag - Format flag (see below).
          byte info - Info field (used for link test).
          byte rep  - Numeric representation, byte order ('I'=IEEE, 'E'=EEEI).
          byte seq  - Sequence number for RDP (0-127). Sequence numbers less
                      than 0 indicate protocol (other than RDP), times -1:
                        -1*{@link #UDP} = -1
                        -1*{@link #TCP} = -2 (future support)
                        -1*{@link #IDP} = -3 (future support)
          byte try  - Number of retries (initially 0).
          byte rpt  - Packet report (reserved for future use, always 0).
          byte adj  - Bytes in adjunct header (0-127). Values less than 0 (i.e.
                      -1 to -128) should not be used and are reserved.

        Format Flag (clarification added in NeXtMidas 2.3.0):
          Lower 4 bits indicate data type (these are mutually exclusive):
              {@link #TYPE_RAW}    = 0  - Raw data (type unknown / not set).
              {@link #TYPE_BINARY} = 1  - Data is binary.
              {@link #TYPE_XML}    = 2  - Data is XML.
              {@link #TYPE_ASCII}  = 3  - Data is ASCII.
              [Types 4-15 are reserved for future use]

          Upper 4 bits indicate fields in the adjunct header:
            This specifies the first N 8-byte values in the adjunct header
            using a bit mask. If multiple fields are included in the bit
            mask they appear in the adjunct header increasing order (e.g.
            ADJ_TIME comes before ADJ_OFFSET). Any remaining space in the
            adjunct header (adj-N*8 bytes) is user-specific:
                {@link #ADJ_TIME}   = 0x10  - TimeStamp  (double)
                {@link #ADJ_OFFSET} = 0x20  - FileOffset (double)
                [reserved] = 0x40  - [reserved for future use]
                {@link #ADJ_USER}   = 0x80  - UserType   (char[8])
            It is common for user-defined adjuncts to be immediately preceded
            by an UserType specifier which is an 8-byte Fortran string (padded
            with spaces) indicating the type of user data. The UserType string
            is user-defined, except names beginning with "NXM" or "XM" are
            reserved.

        Adjunct Header:
          byte[n]   - Any adjunct fields, often used for time code
                      or offset information. (See note about format
                      flag.)
        Data:
          byte[n]   - Remainder of packet, the included data.


        Function                    Purpose
        -----------------------------------------------------------------
        SET=101,     GET=102,       For results/message/parameter access.
        RET=103,     ACK=104,       For results/message/parameter access.
        OPEN=105,    CLOSE=106,     Open/close channel.
        SEND=107,    RECV=108,      Message access.
        PING=109,    PONG=110,      Keep alive checks.
        DHDR=111,    DBUF=112,      File data access.
        ADDR=113,    MODIFY=114,    Address, modify.
        OPENED=115,  CLOSED=116,    Response to OPEN,CLOSE.
        DBUFC=117,   DBUFD=118,     File data buffer, Compressed Direct.
        DBUFZ=119,   DBUFX=120,     File data buffer, Zipped eXtended.
        GETPROP=121, RETPROP=122,   Published keyname access.
        RECEIPT=123, LINKTEST=124,  Low-level RDP hooks.
        MFTP=125,                   Midas File Transfer Protocol command.
        DROPPED=0;                  Dropped remote state.
    </pre>
    @author Jeff Schoen / J. Czechowski IV / Neon Ngo
    @see nxm.sys.prim.rmif

    <pre>
    Message sequence to get a Remote up and running all RDP messages are
    responded to with a RECEIPT

        Client                                  Server
    PING OPEN (UDP)                         PONG  OPEN (UDP)
    OPEN      (RDP)                         OPENED     (RDP)
    SET       (RDP)                         ACK        (RDP)
    GET       (RDP)                         RET        (RDP)
    CLOSE     (RDP)


    Remotes can be in one of three states

    OPEN
    OPENED
    CLOSED
    </pre>

    NOTES:<br>
    There are not enough states for a Remote to stay in synch with a
    client/server when closing.  IE CLOSE should put a Remote into a CLOSE
    state and then when a CLOSED message is implemented correctly the Remote
    should transition to CLOSED.<br>
    The new State Machine type behavior does not allow remote reuse because
    you can not rely on the former state of the Remote since there are too
    many ways it is closed.  If the close methods are fixed we can reuse Remotes.
*/
public class Rmif implements RmifInterface {
  /** Size of RMIF main header (in bytes). */
  static final int HEADER_LEN = 8;
  /** Maximum size of RMIF adjunct header (in bytes). */
  static final int MAX_ADJUNCT_LEN = Byte.MAX_VALUE;
  /** Maximum size of data buffer in RMIF Packet (in bytes). SHOULD NOT be greater than
      65332 for IPv4 =  Max IPv4 packet len (65535)
                      - IPv4 header len (20)
                      - IPv4 max options len (40) (optional)
                      - UDP header len (8)
                      - RMIF main header len (8)
                      - max adjunct header len (127) (optional, normally 8 with just timecode).
      If IPv4 options is not used (+40) and if only timecode is specified in adjunct header (8),
      then practical limit is 65332 + IPv4 options (40) + practical adjunct len (127-8) = 65491.
    */
  static int MAXPKTBUF = 32768;

  /** Maximum number of members per Remote. */
  static int MAXMEMBER = 64;
  /** Maximum packet latency in seconds. */
  static int MAXLAT    = 2;

  /** channel flags types */
  public static final int MULTI=0x1;

  /** RDP packet queue (buffer) size for send and receive queues (per remote) */
  static final int RDP_PACKET_QUEUE_SIZE =  128;
  /** Maximum RDP sequence number (127). MUST be less than {@link #RDP_PACKET_QUEUE_SIZE} - 1. */
  static final int MAX_RDP_SEQ_NUM       =  RDP_PACKET_QUEUE_SIZE-1;
  /** Max number of outstanding RDP packets per Remote */
  public  static final int MAX_WINDOW            =   80; // MUST NOT be more than max RDP sequence number (127)
  /** Maximum overlapping sequence number (for wrap around handling) */
  static final int MAX_OVERLAP           =   10;

  /** Default value: Max number of outstanding RDP packets. */
  static final int DEFAULT_MAX_RETRY   =    5;
  /** Default Value: Max number of outstanding RDP packets. */
  @InternalUseOnly
  public static final int DEFAULT_MAX_WINDOW  =   12;
  /** Default Value: Max number of RDP packets on send queue. */
  static final int DEFAULT_MAX_QUEUE   = 1024;

  /** Use Legacy REMOTE_# style names, rather than REMOTE_ADDRESS_PORT style
   *  when running with default (not state machine message processing) mode*/
  static boolean defaultUsesLegacyRemoteNames = true;

  static boolean origmRemoteReuseEnabled     = true;
  static boolean origmAllOriginalMessages    = true;
  static boolean origmExtraLoggingEnabled    = false;

  // RmifSM (State Machine) overrides mthods and does not allow:
  // 1) remote reuse
  // 2) legacy remote names
  // 3) anything but full close and remote removal
  // The only options are logging one (Rmif methods used here)
  static boolean statemAllOriginalMessages   = false;
  static boolean statemExtraLoggingEnabled   = true;

  static boolean oldRemoteStorage = false; // for debug or emergency purposes only
  static boolean revertSafetyFixes = false; // revert 4.1.0 safety fixes contributed by Jeremy

  boolean stateBasedMsgProcessing = false; // RmifSM is what sets this to true
  boolean allOriginalMessages;
  boolean reuseRemotes;
  boolean supplimentalLogging;
  /** Use Legacy REMOTE_# style names, rather than REMOTE_ADDRESS_PORT style*/
  boolean useLegacyRemoteNames;

  // These fields should be private
  /**  Since NeXtMidas 2.5.0: Use <code>getChannels().getSize()</code>. */
  int nchannel=0;
  /** Since NeXtMidas 2.5.0: Use <code>getProperties().getSize()</code>. */
  int nproperty=0;

  DatagramSocket dsock;
  DatagramPacket dpacki;
  DatagramPacket dpacko;
  TextFile       logfile;
  PacketReader   packetReader;

  // Data compression look-up-table.
  byte[]  lutm = { 0,1,2,3,4,5,6,8,10,12,15,18,21,24,28,32 };

  byte[]  buffi;                  // Input data buffer (for reading from socket)
  byte[]  buffo;                  // Output data buffer (for writing to socket)
  byte[]  lutl;                   // Compression table (see initTables())
  byte[]  lutu;                   // Compression table (see initTables())
  byte[]  uncbuf = new byte[MAXPKTBUF+16]; // Uncompress buffer for data compression
  byte[]  cbuf   = new byte[MAXPKTBUF+16]; // Compress buffer for data compression
  boolean caseSensitive  = false; // Keep table keys case sensitive (for Jer)
  double  timePing       =   5;   // Time between pings (seconds)
  double  timeOut        =  16;   // Time out (seconds)
  double  timeCloseOut   =   1;   // Time to wait for remote connections to close (seconds)
  double  defaultTimeLost=   5;   // Default Time before declaring a RDP packet lost (seconds) default for new Remote(s)
  //jnuanes - we need to be able to set this before a remote is created so all messages will work
  double  defaultTimeRetry=  0.2;   // Default Time before resending packet (seconds) default for new Remote(s)
  int     socksenderr    =   0;   // Recent send errors (reset to 0 when send is successful)
  double  linkTime       =   0;   // Time of last call to getLinkBW() (MJS)
  int     sockrecverr    =   0;   // Recent recv errors (reset to 0 when recv is successful)
  double  linkBytes      =   0;   // Bytes over link since last call to getLinkBW() - estimated
  boolean verbose        = false; // Show verbose output
  boolean debug          = false; // Show internal debug
  boolean reOpen         = true;  // Automatically reopen connections
  boolean requireOpen    = false; // require remote to open connection to fix crosstalk issue for Bug 1548
  int     packetOverhead = 100;   // ???
  double  maxLinkRate    = 2e6;   // Max link rate for MFTP
  int     testLinkPkt    =   0;   // Used for LINKTEST
  byte[]  testLinkBuf    = new byte[RDP_PACKET_QUEUE_SIZE]; // Used for LINKTEST
  Thread  recvThread;             // PacketReader thread for processing incoming packets.
  Remote  wdef;                   // Default Remote class for write thread
  Remote  rlast          = null;  // TEMPORARY - Remote that sent last packet (used for speed with old KeyVector storage)
  int     maxRetry       = DEFAULT_MAX_RETRY;  // Remote Default: Max retries for RDP packets.
  int     maxWindow      = DEFAULT_MAX_WINDOW; // Remote Default: Max number of outstanding RDP packets.
  int     maxQueue       = DEFAULT_MAX_QUEUE;  // Remote Default: Max number of RDP packets on send queue.
  int     remoteIndex    =    0;  // Counter for remote connections.
  Midas   M;                      // Midas context to use (can be null)
  Object  ref;                    // The owner/ref used (usually the RMIF primitive).
  String  id             = "";    // The ID to use (same as used by the owner).
  MessageHandler owner;           // Owner (gets status messages)
  MessageHandler handler;         // Handler for messages received from remotes.
  String remoteFastID ="";        // holds the official ID for the remote saved in MR as REMOTE

  /** made private and slated for removal since NeXtMidas 4.1.0: Use {@link #getRemotes()}. */
  private  KeyVector   remotes    = new KeyVector(16);    // KeyVector of remotes
  /** Remotes are now stored in a HashTable for fast lookups of remotes when we get a packet.  The previous
    * KeyVector would iterate twice to find the stored Remote in some cases.<br>
    * Synchronized was added to all of the places that this is used so it is thread
    * safe to remove or add a remote. (contributed by Jeremy Nuanes)
    * @since 4.1.0
   */
  final Map<String, Remote> _fastRemotes = Collections.synchronizedMap(new HashMap<>(16));  //Make remote lookup as fast as possible
  //  /** @deprecated Since NeXtMidas 2.5.0: Use {@link #getRemotes()}. */
  //  @Deprecated public  KeyVector   remotes    = new KeyVector(16);    // KeyVector of remotes
  /** Since NeXtMidas 2.5.0: Use {@link #getProperties()}. */
  KeyVector   properties = new KeyVector(16);    // KeyVector of properties
  /** Since NeXtMidas 2.5.0: Use {@link #getChannels()}. */
  KeyVector   channels   = new KeyVector(16);    // KeyVector of channels

  /** Maximum number of errors when serving a property (pipe,result, etc.) to client before removing it
   *  to prevent flood of errors on server side. This count is per Property and gets reset after success.
   *  Used in {@link #serveProperties()}. Since 3.5.0.
   * */
  int maxErrorsToNotServeProperty = 10;

  /** Temporary, for testing with RmifJeremy2019 */
  @InternalUseOnly
  Rmif () {}

  /** Create without an owner or Midas. This constructor is used when RMIF is loaded as a
      stand-alone library.
      @param handler The Message Handler (for messages received from remotes).
  */
  public Rmif (MessageHandler handler) {
    this(null, null, handler);
  }

  /** Create an Rmif. This constructor is used by the <code>RMIF</code> primitive.
      @param M       Midas reference, created if null.
      @param owner   Used as the reference (and for status messages).
      @param handler The Message Handler (for messages received from remotes).
  */
  public Rmif (Midas M, MessageHandler owner, MessageHandler handler) {
    this(M, owner, handler, false);
  }

  /** Create an Rmif. This constructor is used by the <code>RMIF</code> primitive.
      @param M       Midas reference, created if null.
      @param owner   Used as the reference (and for status messages).
      @param handler The Message Handler (for messages received from remotes).
   *  @param stateBasedMsgHandling use state machine based message handling
   */
  public Rmif (Midas M, MessageHandler owner, MessageHandler handler, boolean stateBasedMsgHandling) {
    this.ref     = owner;
    this.M       = (M != null)? M : new Midas();
    this.owner   = owner;
    setMessageHandler(handler);

    if (owner instanceof IDable) id = ((IDable)owner).getID();

    // use different defaults when running with state machine logic
    if (debug) info("Using State Based Message Handling:"+stateBasedMsgHandling);
    this.stateBasedMsgProcessing = stateBasedMsgHandling;
    setModeBasedOptions();
  }

  private void setModeBasedOptions () {
    if (!stateBasedMsgProcessing) {
      this.reuseRemotes   = origmRemoteReuseEnabled;
      this.allOriginalMessages = origmAllOriginalMessages;
      this.supplimentalLogging = origmExtraLoggingEnabled;
      this.useLegacyRemoteNames = defaultUsesLegacyRemoteNames;
    }
    else {
      this.allOriginalMessages = statemAllOriginalMessages;
      this.supplimentalLogging = statemExtraLoggingEnabled;
    }
  }

  /** Create and set up the DatagramSocket, allocate buffers and start read/write threads.
      @param host   Socket host to bind on (if host == "" then use the wildcard address (0.0.0.0),
                                            an IP address chosen by the kernel).
      @param port   Socket port (port &lt; 0: use any local port).
      @param prange The port range to try.
      @since NeXtMidas 3.1.2
  */
  public void open (String host, int port, int prange) {
    String h = (host.trim().equals("")) ? "0.0.0.0" : host;

    while (dsock==null) {
      try {
        if (port < 0) port = 0;
        dsock = new DatagramSocket(new InetSocketAddress(h, port));
        dsock.setSoTimeout(50); // in milliseconds
        dsock.setSendBufferSize(Constants.B1M);
        dsock.setReceiveBufferSize(Constants.B1M);
        info("RMIF Connected/bound to Datagram socket port: "+getPort());
      }
      catch (Exception e) {
        if (allOriginalMessages) info("Port "+port+" not available");
        if (--prange<=0) throw new MidasException("No ports available "+port, e);
        port++;  // try next port number
      }
    }

    // allocate a packet buffer for input
    buffi  = new byte[HEADER_LEN + MAX_ADJUNCT_LEN + MAXPKTBUF];
    dpacki = new DatagramPacket(buffi,buffi.length);

    // allocate a packet buffer for output
    buffo  = new byte[HEADER_LEN + MAX_ADJUNCT_LEN + MAXPKTBUF];
    dpacko = new DatagramPacket(buffo,buffo.length);

    // create a default Remote class for write thread
    wdef = new Remote();
    wdef.id = "UNKNOWN";
    wdef.rmif = this;
    wdef.mh = owner;
    wdef.state = ADDR; // BUG 2250 fix for sending back MFTP response to clients
    if (wdef.mh == null) { warning("open() MessageHandler is null for wdef "+wdef); }

    // start the receiving thread
    packetReader = new PacketReader(this);
    recvThread = new MidasThread(M,packetReader,"RmifPacketReader_"+id); // NTN: name this thread
    recvThread.start();
  }

  /** Create and set up the DatagramSocket, allocate buffers and start read/write threads.
      @param port   Socket port (port &lt; 0: use any local port).
      @param prange The port range to try.
   */
  public void open (int port, int prange) {
    this.open ("", port, prange);
  }

  /** Close remotes, close sockets, disconnect.
   *  This version does a just a light close and not a full close*/
  public void close () {
    double time = Time.current();
    for (String key : _fastRemotes.keySet()) closeRemote(_fastRemotes.get(key));
    if (nchannel>0) {
      while (Time.current()-time<timeCloseOut) {
        boolean ok=true;
        for (int ic=1; ic<=nchannel; ic++) {
          if (getChannel(ic)!=null && getChannel(ic).state!=CLOSED) ok=false;
        }
        if (ok) break;
        Time.sleep(0.1);
      }
    }
    if (packetReader!=null) packetReader.stop();
    if (dsock!=null) dsock.close();
  }

  /** DO NOT call this method.  It is for testing only.
      Closes socket and reader thread so to simulate a dead RMIF.
      @since NeXtMidas 2.7.3 and updated to use fast remotes in 4.1.0
   */
  public void hardKillForTestingOnly () {
    warning("Rmif.hardKillForTestingOnly(): Stopping packet reader "+packetReader
        +", closing DatagramSocket "+dsock);
    // jnuanes/NTN - Close the reader and the socket first so we don't try to start a remote while we are exiting
    if (packetReader!=null) packetReader.stop();
    if (dsock!=null) dsock.close();
    synchronized(_fastRemotes)
    {
      for (Remote r : _fastRemotes.values()) {
        if ((r!=null) && (r.id!=null) && (M!=null)) {
          info("  Removing from registry "+r);
          M.registry.remove(r.id);
        }
      }
    } // end for loop
    // more cleanup
    _fastRemotes.clear();
    properties.clear();
    channels.clear();
  }

  /** Set the log file to use.
   *  @param logfile the log file to use */
  public void setLogFile (TextFile logfile) { this.logfile = logfile; }
  /** Get the log file in use.
   *  @return the log file in use */
  public TextFile getLogFile () { return logfile; }

  /** Enable/disable verbose output.
   *  @param verbose true enables verbose output */
  public void setVerbose (boolean verbose) { this.verbose = verbose; }
  /** Indicates if verbose output is enabled.
   *  @return true if verbose output is enabled */
  public boolean getVerbose () { return verbose; }

  /** Enable/disable internal debug.
   *  @param debug true to enable internal debug */
  public void setDebug (boolean debug) { this.debug = debug; }
  /** Indicates if internal debug is enabled.
   *  @return true if internal debug is enabled */
  public boolean getDebug () { return debug; }

  /** Enable/disable automatic reopen of connections.
   *  @param reOpen true to enable automatic reopen of connections */
  public void setReOpen (boolean reOpen) { this.reOpen = reOpen; }
  /** Indicates if automatic reopen of connections is enabled.
   *  @return true if automatic reopen of connections is enabled */
  public boolean getReOpen () { return reOpen; }

  /** Set to true to require unknown remote to first OPEN a connection to this RMIF otherwise,
      unsolicited packets will be ignored and a QUIET CLOSE packet may be sent back.
      This feature is here to resolve users's with crosstalk issues as described in BUG 1548.
      @param val set to true to prevent crosstalk issue, false (default) to allow packets from
                 unknown remote's to get forwarded to the upper level (i.e. the macro).
      @since NeXtMidas 2.8.1
   */
  public void setRequireOpen (boolean val) { requireOpen = val; }
  /** Get if requiring unknown remote to first OPEN a connection to this RMIF otherwise,
      unsolicited packets will be ignored and a QUIET CLOSE packet may be sent back.
      This feature is here to resolve users's with crosstalk issues as described in BUG 1548.
      @since NeXtMidas 2.8.1
      @return true if requiring unknown remote to first OPEN a connection to this RMIF
   */
  public boolean getRequireOpen () { return requireOpen; }

  /** Set the socket timeout in seconds.
   *  @param time socket timeout */
  public void setTimeOut (double time) { timeOut = time; }
  /** Get the socket timeout in seconds.
   *  @return socket timeout */
  public double getTimeOut () { return timeOut; }

  /** Set default time before declaring a RDP packet lost (seconds) for new Remote(s).
   *  @param time default time before declaring a RDP packet lost */
  public void setTimeLost (double time) { defaultTimeLost = time; }
  /** Get default time before declaring a RDP packet lost (seconds) for new Remote(s).
   *  @return default time before declaring a RDP packet lost */
  public double getTimeLost () { return defaultTimeLost; }

  //jnuanes - added this so all messages to slow sites will work
  /** Set default time before sending a retry (seconds) for new Remote(s).
   * @param time new default time before retry*/
  public void setTimeRetry (double time) { defaultTimeRetry = time; }
  /** Get default time before sending a retry (seconds) for new Remote(s).
   * @return default time before a retry*/
  public double getTimeRetry () { return defaultTimeRetry; }

  /** @deprecated Since NeXtMidas 2.5.0: This is not used.
   *  @param time not used */
  @Deprecated public void setTimeOutR (double time) {
    deprecate("Rmif: Since NeXtMidas 2.5.0 setTimeOutR(..) is is not used");
  }

  /** Set the time to wait for remote connections to close (in seconds).
   *  @param time the time to wait for remote connections to close */
  public void setTimeCloseOut (double time) { timeCloseOut = time; }
  /** Get the time to wait for remote connections to close (in seconds).
   *  @return the time to wait for remote connections to close */
  public double getTimeCloseOut () { return timeCloseOut; }

  /** Set the time before dropping a connection (in seconds). TBD
   *  @param time the time before dropping a connection
   *  @deprecated in NeXtMidas 4.1.0 - jnuanes - Not Used or Implemented */
  @Deprecated
  public void setTimeDrop (double time) { }

// JC4: Need to add this if we decide to support timeDrop
//  /** Get the time before dropping a connection (in seconds). */
//  public double getTimeDrop () { return timeDrop; }

  /** Set the time between pings (in seconds).
   *  @param time the time between pings */
  public void setTimePing (double time) { timePing = time; }

  /** Get the time between pings (in seconds).
   *  @return the time between pings */
  public double getTimePing () { return timePing; }

  /** Sets the default value for the max retries for RDP packets. New remote connections will
      be initialized with this value.
      @see Remote#setRetry(int)
      @param value the default value for the max retries for RDP packets
   */
  public void setRetry (int value) { maxRetry = value; }

  /** Gets the default value for the max retries for RDP packets. New remote connections will
      be initialized with this value.
      @see Remote#getRetry()
      @return the default value for the max retries for RDP packets
   */
  public int getRetry () { return maxRetry; }

  /** Keep tables keys case sensitive.
      @return true if table keys are case sensitive
      @since NeXtMidas 2.5.0
   */
  public boolean getCaseSensitive () {
    return caseSensitive;
  }

  /** Keep tables keys case sensitive. (You probably do not want this!!!)
      @param cs true if table keys are case sensitive
      @since NeXtMidas 2.5.0
   */
  public void setCaseSensitive (boolean cs) {
    // This method added for Jeremy's project that needs case-sensitive table keys
    // This method is NOT tested and all other users should avoid it.
    caseSensitive = cs;
    if (caseSensitive) {
      warning("Rmif: Setting table keys to be case sensitive.");
    } else {
      info("Rmif: Table keys are not case sensitive.");
    }
  }

  /** Sets the default value for the max window size used. Must be in the range
      1..{@link #MAX_WINDOW} default is 12. New remote connections will be
      initialized with this value.
      @see Remote#setWindow(int)
      @param value the default value for the max window size used
   */
  public void setWindow (int value) {
    if ((value < 1) || (value > MAX_WINDOW)) {
      throw new MidasException("Rmif: Can not set window for "+this+" to "+value+". Window "
                             + "size must be in the range 1.."+ MAX_WINDOW+".");
    }
    maxWindow = value;
  }

  /** Gets the default max window size used by new remote connections.
      @see Remote#getWindow()
      @return the default max window size
      @since NeXtMidas 2.5.0
   */
  public int getWindow () { return maxWindow; }

  /** Sets the default value for maximum number of packets on the RDP queue. Must be in the range
      0..N, default is 1024. New remote connections will be initialized with this value.
      @see Remote#setMaxQueue(int)
      @param value the default value for maximum number of packets on the RDP queue
      @since NeXtMidas 2.5.0
   */
  public void setMaxQueue (int value) {
    if (value < 0) {
      throw new MidasException("Rmif: Can not set maxQueue for "+this+" to "+value+". Max queue "
                             + "size must be in the range 0..N.");

    }
    maxQueue = value;
  }

  /** Gets the default maximum number of packets on the RDP queue used by new remote connections.
      @see Remote#getMaxQueue()
      @return the default maximum number of packets on the RDP queue used by new remote
      @since NeXtMidas 2.5.0
   */
  public int getMaxQueue () { return maxQueue; }

  /** return the KeyVector of remotes
   *  @return KeyVector of remotes
   *  @since 4.1.0 updated to use fast remotes */
  public KeyVector getRemotes () {
    //jnuanes - because of the use of _fastRemotes we needed to change this method
    KeyVector v = new KeyVector();
    synchronized(_fastRemotes) {
      for (String key : _fastRemotes.keySet()) {
        v.put(key, _fastRemotes.get(key));
      }
    }
    return v;
  }

  /** Return the remote with the given remoteID
   *  @param remoteID ID of the remote
   *  @return the remote
   *  @since NeXtMidas 4.1.0 and only for use with new fast Remotes storage
   */
  public Remote getRemote (String remoteID) {
    synchronized (_fastRemotes) {
      for (Remote r : _fastRemotes.values()) {
        if(r.getID().equals(remoteID)) {
          return r;
        }
      }
    }
    return null;
  }

  /** Return the remote at the given integer index from the set of remotes
   *  Note: The integer index is not very useful with the HashMap Remote
   *  storage introduced in NeXtMidas 4.1.0
   *  @param i integer index of the remote
   *  @return the remote
   */
  public Remote getRemote (int i) {
//    if (oldRemoteStorage) {
//      synchronized (remotes) { // NTN: remove this mutex when KeyVector.get() is synchronized
//        return (Remote)remotes.get(i-1);
//      }
//    }
    KeyVector kvRemotes = getRemotes ();
    return (Remote)kvRemotes.get(i-1);
  }

  /** return the KeyVector of properties
   *  @return the KeyVector of properties */
  public KeyVector getProperties () { return properties; }

  /** Return the property at the given integer index from the set of properties
   *  @param i integer index of the property
   *  @return the property
   */
  public Property getProperty (int i) { return (Property)properties.get(i-1); }

  /** Return the property by name
   *  @param name property name
   *  @return the property
   */
  public Property getProperty (String name) {
    int ip = indexProperty (name,-1);
    return getProperty(ip);
  }

  /** Gets the KeyVector of channels
   *  @return the KeyVector of channels */
  public KeyVector getChannels () { return channels; }

  /** Return the channel at the given integer index from the set of channels
   *  @param i channel index
   *  @return the channel at the given index */
  public Channel getChannel (int i) { return (Channel)channels.get(i-1); }

  /** Return the local port that the instance is connected to (returns -1 if not connected).
   *  @return the local port that the instance is connected to */
  public int getPort () { return (dsock == null)? -1: dsock.getLocalPort(); }

  /** Reset (clear time field and buffer) of receive queue for specified Remote for
      all packets in RDF receive queue from r.recvChk to newSeq.
      @param r Remote to reset it's RDP receive queue (cannot be null).
      @param newSeq new sequence to start from (-1 for everything)
      @since NeXtMidas 2.7.3
   */
  int resetRecvQueue (Remote r, int newSeq) {
    if (verbose) debug("resetRecvQueue for "+r+" newSeq="+newSeq+" recvChk="+r.recvChk+" recvQueue="+r.recvQueue);
    synchronized(r) { // mutext for updating RDP book keeping for specified remote
      //jnuanes - Don't output unnecessary messages
      if (allOriginalMessages && r.recvQueue>0) { warning("resetRecvQueue for "+r+" still have packets in queue: "+r.recvQueue); }
      //jnuanes - Use the length of the array it is safer
      for (int seq=0; seq!=r.recvBuf.length; seq++) {

        //jnuanes - Clear out everything but the current sequence or everything if given -1
        if (newSeq == -1 || seq != newSeq) {
          Packet pkt = r.recvBuf[seq];
          pkt.clear(0); // reset packet to mark it as available for use on recv queue
        }
      } // end for loop

      if (newSeq == -1) {
        newSeq = r.recvChk = r.recvQueue = r.recvReOrder = 0; // reset all to zero
      } else {
        r.recvChk = newSeq;
      }
    }
    return newSeq;
  }

  /** Reset send queue for specified Remote.
      Reset (clear time field and buffer) for all RDP Packets in send queue.
      Resets RDP send and check sequence to zero. Resets sendDrop to zero.
      Also clears any Packets in the sendQueue List.
      @since NeXtMidas 2.7.3
   */
  void resetSendQueue (Remote r) {
    int sizeSendQueue = r.sendQueue.size();
    if (verbose) debug("resetSendQueue for "+r+" sendSeq="+r.sendSeq+" sendChk="+r.sendChk+" sizeSendQueue="+sizeSendQueue);
    synchronized (r) { // mutex for updating RDP book keeping for specified Remote
      //jnuanes - Use the length of the array it is safer
      for (int seq=0; seq!=r.sendBuf.length; seq++) { // NTN 2009-10-06
        Packet pkt = r.sendBuf[seq];
        pkt.clear(0); // reset packet to mark it as available for use on send queue (buffer)
      }
      r.sendSeq  = 0;
      r.sendChk  = 0;
      r.sendDrop = 0;
      if (allOriginalMessages && (sizeSendQueue>0)) { //jnuanes - Don't output unnecessary messages
        warning("resetSendQueue for " + r+ " clearing "+sizeSendQueue+" packets in send queue");
      }
      r.sendQueue.clear();
    }
  }

  /** Read a packet from the socket and perform initial processing. Initial processing includes,
      handling of RDP protocol (reordering, receipts, filtering duplicates, etc) and LINKTEST
      functions.
   */
  boolean recvPacket () {
    try {
      dpacki.setLength(buffi.length);   // reset length
      dsock.receive(dpacki);            // blocks until datagram is received

      int  len  = dpacki.getLength();  // Total packet length
      byte seq  = buffi[SEQ];          // RDP sequence number (<0: not RDP)
      byte func = buffi[FUNC];         // RMIF function
      byte retry = buffi[TRY];         // RDP retry attempt
      byte info  = buffi[INFO];        // info field
      // byte adj  = buffi[ADJ];       // Length of adjunct header
      int  doff = HEADER_LEN;// + adj; // Data start offset
      int  dlen = len-doff;            // Data length
      int  port = dpacki.getPort();    // Port number
      InetAddress addr=dpacki.getAddress(); // Host address

      String remoteID;
      Remote r;
      // find the Remote object of packet address
      if (!useLegacyRemoteNames) {
        remoteID = createFastLookupKey(addr, port);
        r = _fastRemotes.get(remoteID);
      }
      else {
        r = indexRemote(port,addr);
        remoteID = "REMOTE"; // matches original op value passed into createClientRemote
      }

      boolean sendQuietClose = false;  // true to send a quiet close and return
      String host = addr.getHostAddress();
      if (r==null) r = createClientRemote(remoteID, host, port); // use fast id even in default mode

      if (func==PING && info==OPENED && (r.state==CLOSED || r.state==ADDR)) {
        // NTN BUG 1541: got a PING from an RMIF 1.1 remote (most likely a client) that is OPENED
        // but we are not ready (e.g. just restarted). Send a Quiet Close to remote to speed up
        // reopen (reconnect) time, since interested remote will reopen (NeXtMidas 2.7.3+)
        // or older versions of NeXtMidas it will timeout and then reopen connection
        sendQuietClose = true;
      }

      if (requireOpen) { // 2010-04-29 NTN BUG 2138 (allow ADDRessed remote to talk to us, unless
                         //            user specifically turned on flag to require remotes to initiate
                         //            open sequence to this RMIF instance.
        // NTN BUG 1548: If remote is NOT OPEN or NOT OPENED (then send quiet CLOSE via UDP) to fix crosstalk issue
        if (!sendQuietClose && r.state!=OPEN && r.state!=OPENED) {  // to fix BUG 1548 (Jef N.)
          // let the following packets through (when there isn't an open connection to remote)
          //   PING    - a remote client could be connecting
          //   OPEN    - a remote client is OPENing a connection
          //   CLOSE   - a remote is trying to close connection
          //   CLOSED  - a remote is responding to close request
          //   RECEIPT - must be for something we previously sent out (or a previous client sent out).
          //             this should not hurt anything
          if (func!=PING && func!=OPEN && func!=CLOSE && func!=CLOSED && func!=RECEIPT) {
            if (seq<0) {
              if (verbose||debug) {
                debug("Ignoring unsolicited UDP Packet seq="+seq+" retry="+retry+
                    " func="+func+"("+functionName(func)+") info="+info+"("+functionName(info)+") from "+r);
              }
              return false; // ignore other FUNCs that are non-RDP packets (most likely crosstalk from remote server)
              // also, since it could be DBUF (pipe data), there could be lots of them, hence we
              // are just going to drop these instead of sending back a Quiet Close
            }
            sendQuietClose = true; // send quiet close for RDP packets, so remote stop crosstalk
          }
        }
      } // end if (requireOpen)

      if (sendQuietClose) {
        // Send a close message to the remote so we don't have to wait for the time out
        buffi[FUNC] = CLOSE; // send a CLOSE response
        buffi[SEQ]  = -UDP;  // use UDP to reduce book keeping
        buffi[INFO]= -CLOSE; // RMIF 1.1 signal remote to do a "quite close"
        dpacki.setLength(HEADER_LEN);
        dsock.send(dpacki);
        if (verbose) debug("Sent Quiet CLOSE to unsolicited remote for Packet seq="+seq+
          " retry="+retry+" func="+func+"("+functionName(func)+") info="+info+"("+functionName(info)+") from "+r);
        return false;
      }

      if (debug) debug("recvPacket -> "+new Packet(buffi,0,len,false)+" from "+r);

      linkBytes += (len+packetOverhead);

      synchronized (r) { // mutex for updating RDP book keeping for specified Remote
        r.linkBytes += (len+packetOverhead);
        double currentTime = Time.current();
        if (func==RECEIPT) {              // handle receipts
          Packet pkt = r.sendBuf[seq];
          synchronized (pkt) { // make sure no other thread is changing packet as we might clear it
            double sendTime = pkt.time;
            double latency = currentTime-sendTime;
            if (sendTime<=0);               // previously receipted
            else if (latency>MAXLAT);       // outlyer (took too long)
            else if (retry>0);         // no specs on retry
            else if (r.latency==0) r.latency = latency;
            else r.latency = (r.latency*.9 + latency*.1);
            pkt.clear(0); // reset packet to mark it as receipted and make it available to use on send buffer
          } // end synchronized (pkt)

          // JC4: Add the next line to support RMIF when the primitive is paused. Jeff said he
          //      didn't think this should work, so I left it disabled.
          //checkPacketQueue(r);
          return false;
        }
        if (func==LINKTEST) {             // handle linktest
          //jnuanes - changed 128 to RDP_PACKET_QUEUE_SIZE I think this is what was wanted
          if (info>=0 && testLinkPkt<RDP_PACKET_QUEUE_SIZE) testLinkBuf[testLinkPkt++]=info;
          else if (info==-1) testLinkPkt=0;
          else if (info==-2) sendPacket(LINKTEST,-3,testLinkBuf,r, UDP);
          else if (info==-3) testLinkReport(r,dlen,buffi,doff);
          else if (info==-4) testLinkReport(r,testLinkPkt,testLinkBuf,0);
          else if (info<=-5) testLink(r,-(int)buffi[DATA]);
          return false;
        }
        Packet pkt = null;
        if (seq>=0) {              // Received RDP Packet
          if ((verbose||logfile!=null)) {   // handle logging
            int slen = (func >= DHDR && func <= DBUFD)? 0: dlen;
            String text="RECV func="+functionName(func)+
             " seq="+seq+" try="+retry+" info="+buffi[INFO]+
             //jnuanes - using r in the debug makes this better because we get to see everything in the Remote
             " data="+Convert.unpackS(buffi,doff,slen)+" from "+r+" at "+Time.tag();
            if (verbose) { info(text); }   // as of 2.8.0, this also writes to logfile
            else         { logfile.writeln(text); }
          }
          pkt = r.recvBuf[seq];    // get corresponding packet in RDP recv queue

          // calculate if seq is outside the expected window (recvChk+1 to recvChk+MAX_WINDOW)
          int nextExpectedSeq = (r.recvChk+1)&0x7f;
          int maxNextSeq      = (nextExpectedSeq+MAX_WINDOW)&0x7f;
          boolean outOfRange;
          if (nextExpectedSeq<maxNextSeq) {
            outOfRange = (seq<nextExpectedSeq) || (maxNextSeq<seq);
          } else { // maxNextSeq <= nextExpectedSeq
            outOfRange = (maxNextSeq<seq) && (seq<nextExpectedSeq);
          }
          int estimatedPacketsLost = seq - maxNextSeq;     // Number of packet that may be lost
          if (seq<maxNextSeq) estimatedPacketsLost += 128; // Add 128 to account for wrap around to 0

          if (pkt.time>0 && retry==0) {
            int overlapCount = seq - nextExpectedSeq;      // Number of overlapping seq (b/c of seq # wrap around)
            if (seq<nextExpectedSeq) overlapCount += RDP_PACKET_QUEUE_SIZE;
            if (overlapCount>MAX_OVERLAP) {
              if (logfile!=null) logfile.writeln("WARN: IGNORING new overlapping packet, old="+pkt.toStringShort());
              if (supplimentalLogging) warning("IGNORING new overlapping packet seq="+seq+" retry="+retry+" func="+func+"("+functionName(func)+
                ") info="+info+"("+functionName(info)+") from " + r.toString());
              return false; // ignore out of range packet since we still have existing, it should retry
            }
          }

          sendReceipt(func, len);

          if (supplimentalLogging && (verbose||logfile!=null) && seq>=0) {   // handle logging
            int slen = (func >= DHDR && func <= DBUFD)? 0: dlen;
            String text="RECEIPT func="+functionName(func)+
             " seq="+seq+" try="+retry+" info="+buffi[INFO]+
             //jnuanes - using r in the debug makes this better because we get to see everything in the Remote
             " data="+Convert.unpackS(buffi,doff,slen)+" from "+r+" at "+Time.tag();
            if (verbose) { info(text); }   // as of 2.8.0, this also writes to logfile
            else         { logfile.writeln(text); }
          }

          if (seq==nextExpectedSeq) {
                                // if next expecting sequence, don't bury it
          }
          else if (retry>0) {   // bury extra retries
            if (pkt.time>0) {   // already in receive buffer
              return false;
            } else if (pkt.time<0) {
              if (outOfRange && estimatedPacketsLost>MAX_OVERLAP) {
                return false;   // out of range packets greater than MAX_OVERLAP sequences away
              }

              double rtd; // estimated round trip delay
              if (r.latency==0) rtd=r.timeRetry;
              else  rtd=r.timeRetry+r.latency;
              double timePeriod = retry*rtd;            // this mirrors retry timing logic on RDP sending side
              double timeDiff   = currentTime+pkt.time; // add pkt.time (since is is negative)
              if (timeDiff<timePeriod) return false;    // already sent to macro, unnecessary retry (lost receipt?)
            }
          }

          // handle packets received out-of-order
          if (seq!=nextExpectedSeq && r.isOpened()) {  // Jeff: why do we need the r.isOpened?
            if (outOfRange) {  // out of range sequence, shift expecting window to (seq-MAX_WINDOW to seq)
              checkRecvQueue(r, estimatedPacketsLost); // now check the RDP receive queue (this will mark at least one lost packet)
              nextExpectedSeq = (r.recvChk+1)&0x7f;    // re-calculate next expected seq as checkRecvQueue() may have moved it
            }

            // if have existing packet in remote's receive queue at this slot, shift expected window
            for (int k=0; k<MAX_OVERLAP && pkt.time>0 && r.recvQueue>0; k++) { // try up to MAX_OVERLAP times
              estimatedPacketsLost = seq - nextExpectedSeq;         // update Number of packet that may be lost
              if (seq<nextExpectedSeq) estimatedPacketsLost += RDP_PACKET_QUEUE_SIZE; // Add RDP_PACKET_QUEUE_SIZE to account for wrap around to 0
              checkRecvQueue(r, estimatedPacketsLost);
              nextExpectedSeq = (r.recvChk+1)&0x7f; // re-calculate next expected seq as checkRecvQueue() may have moved it
            } // end for loop

            if (pkt.time>0) { // if we still have not sent packet up to macro, it will be overwritten
                if (logfile!=null) { logfile.writeln("WARN: OVERWRITTING EXISTING PACKET at seq="+seq+" old="+pkt.toStringShort()); }
            }

            if (seq!=nextExpectedSeq) { // check to see if it is still not the next expected seq b/c
                                        // call to checkRecvQueue() above may have moved expected seq
              pkt.initialize(buffi, 0, len, currentTime, true); // copy since we re-use the buffer
              r.recvQueue++;

              if (r.recvQueue>MAX_WINDOW) {
                String warnMsg = "WARN: RDP receive queue is FULL! recvQueue=" + r.recvQueue;
                if (logfile==null) { warning(warnMsg); }  // as of 2.8.0 this also writes to logfile
                else               { logfile.writeln(warnMsg); }
              }

              //sendLinkProblemReport(r,"PKTORDER "+r.recvQueue+" "+r.recvChk+" "+s,new Packet(buffi,0,len));
              return false;
            }
          }
          r.recvChk=seq;
        } // end if RDP packet

        procPacket(buffi,dlen,r);
        if (pkt!=null) {  // if RDP packet, then we just sent it to the macro
          pkt.clear(-currentTime);
        }
        if (r.recvQueue>0) checkRecvQueue(r, -1);
      } // end synchronized (r)

      if (sockrecverr>0) {
      // after successfully processing a received packet and checking receive queue, reset sockrecverr to zero.
        info("Total Packet recv errs: "+sockrecverr);
        sockrecverr=0;
      }
      return true;
    }
    catch (java.io.InterruptedIOException e) { }
    catch (Exception e) {
      if (packetReader.isAborted()) return false;
      if ("Interrupted system call".equals(e.getMessage())) return false;
      // All normal packet processing exceptions are done in procPacket(), the error handling here
      // provides an additional layer of stability for the PacketReader (receive) thread.

      if (sockrecverr==0) {
        // Only do the following ONCE until sockrecverr has been reset to account for cases where
        // there are a burst of errors that recvPacket was unable to handle (e.g. router corruption).
        //  (1) Display stack trace
        //  (2) Send "problem report" to local macro
        printStackTrace("Exception in recvPacket",e);
        sendLinkProblemReport(null,"Exception in recvPacket: "+e.getMessage(), null);
      }
      sockrecverr++;
    }
    return false;
  }

  /** Send a receipt
   *
   *  @param func  function
   *  @param len   length
   *  @throws IOException
   */
  void sendReceipt(final byte func, final int len) throws IOException {
    buffi[FUNC]=RECEIPT;     // send a receipt
    dpacki.setLength(8);
    dsock.send(dpacki);
    buffi[FUNC]=func;
    dpacki.setLength(len);
  }


  /** Check the receive queue for a given remote and update the RDP counters.
      @param r       Remote to check it's receive queue. This must not be null.
      @param numLost Estimated number of packets lost (normally set to -1), unless
                     caller is reporting that the next expected sequence is lost
                     (e.g. b/c it received a sequence number beyond the current window)
   */
  void checkRecvQueue (Remote r, int numLost) {
    int ochk=0;             // Number of missing packets (must be 0..127)
    synchronized (r) {      // mutex for updating RDP book keeping for specified Remote
      int seq  = r.recvChk; // seq number of last received RDP packet
      int oseq = (numLost>0)? (seq+1)&(0x7F) : -1; // Seq of first missing packet (-1=NoneMissing)
      double currentTime = Time.current();

      for (int i=0; i<=MAX_WINDOW && r.recvQueue>0; i++) { // NEW-DEC31
        seq = (++seq)&(0x7F);
        Packet pkt = r.recvBuf[seq];

        if (pkt.time<=0) { // if packet is missing (i.e. out of order)
          if (oseq<0) oseq=seq;
          if (++ochk>MAX_RDP_SEQ_NUM) {
            // NTN: Since NeXtMidas 2.5.3, we should no longer reach this block as long as queue size < MAX_WINDOW < MAX_RDP_SEQ_NUM
            sendLinkProblemReport(r,"PKTRQCHK "+r.recvQueue+" seq="+seq,null);
            r.recvQueue=0; return; // something went wrong - reset
          }
          continue; // find next received packet
        }

        if (oseq>=0 && oseq!=seq) {
          if (numLost>0) {
            // prevent break from timeLost check if told that there were lost packets.
            // this is common when there are a burst of messages received
          } else if (currentTime-pkt.time<r.timeLost) {
            // still within time frame where packet is not deemed totally lost, continue waiting for packet
            break;
          }

          int lostSeq = oseq;
          while (lostSeq!=seq && numLost>0) {    // clear out lost packets
            Packet lostpkt = r.recvBuf[lostSeq];
            lostpkt.clear(-currentTime);
            lostSeq = (++lostSeq)&(0x7F);
            numLost--;
          }

          sendLinkProblemReport(r,"PKTLOST Seq "+oseq+" to "+seq,null);
          oseq    = -1;  // reset oseq
          numLost = 0;
        } else { // since oseq==seq (we didn't really loose packet, reset oseq & numLost)
          oseq = -1;
          numLost = 0;
        }
        procPacket(pkt.getBufZeroOffset(), pkt.len-8, r); // JC4/NTN: Suggest making procPacket take in pkt or offset
        pkt.clear(-currentTime);
        r.recvChk=seq;
        r.recvReOrder++;
        r.recvQueue--;
        //sendLinkProblemReport(r,"PKTORDERed "+r.recvQueue+" "+seq,new Packet(buf,0,len));
      } // end loop

      if (oseq>=0 && numLost>0 && r.recvQueue>0) {
        // went through the above loop without marking any packets as
        // lost but were told that there were lost packets, so mark
        // them as lost by sending PKTLOST LINKERR up to macro
        if (r.recvQueue<numLost) numLost=r.recvQueue; // numLost cannot be greater than receive queue size
        int lostSeq = oseq;
        for (int j=0; j<numLost; j++) {        // clear out lost packets
          Packet lostpkt = r.recvBuf[lostSeq];
          lostpkt.clear(-currentTime);
          r.recvChk=lostSeq;
          r.recvQueue--;
          lostSeq = (++lostSeq)&(0x7F);
        }
        sendLinkProblemReport(r,"PKTLOST Seq "+oseq+" to "+lostSeq+" reported",null);
      }

      if (r.recvQueue<0) r.recvQueue=0; // Sanity check to make sure recv queue size is not negative
    } // end synchronized (r)
  }

  /** Tests the UDP connection between this system and the given remote.
      <pre>
        mode =  0   - Test sending and receiving (same as testLink(r,8); testLink(r,-8);
                      testLink(r,1024); testLink(r,-1024);)
        mode = -N   - Test receiving with a packet that is 2^N bytes long.
        mode =  N   - Test sending with a packet that is 2^N bytes long.
      </pre>
      @param r The remote to test.
      @param mode The test mode.
   */
  public void testLink (Remote r, int mode) {
    if (mode==0) {
      testLink(r,8); testLink(r,-8);
      testLink(r,1024); testLink(r,-1024);
    }
    else if (mode<0) {
      testLinkPkt=0;
      byte[] buf = new byte[1]; buf[0]=(byte)mode;
      int pktsize=1; for (; mode<0; mode++) pktsize*=2;
      info("Testing Link from "+r.id+"  PktSize="+pktsize);
      sendPacket(LINKTEST,-5,buf,r,UDP);
    }
    else if (mode>0) {
      int pktsize=1; for (; mode>0; mode--) pktsize*=2;
      byte[] buf = new byte[pktsize];
      info("Testing Link to "+r.id+"  PktSize="+pktsize);
      sendPacket(LINKTEST,-1,r,UDP);
      Time.sleep(0.5);
      for (int i=0; i<RDP_PACKET_QUEUE_SIZE; i++) sendPacket(LINKTEST,i,buf,r,UDP);
      Time.sleep(1.0);
      sendPacket(LINKTEST,-4,r,UDP);
    }
  }

  /** Since NeXtMidas 2.5.0: Internal use only; use {@link #testLink(Rmif.Remote,int)}.
   *  @param r   Remote
   *  @param len length
   *  @param buf buffer
   *  @param off offset
   */
  void testLinkReport (Remote r, int len, byte[] buf, int off) {
    if (off==0) info("Link Test Results from node: "+r.id);
    else info("Link Test Results to node: "+r.id);
    for (int i=0; i<len; ) {
      String text="  ";
      for (int j=0;j<10&&i<len;j++,i++) text += buf[i+off]+" ";
      info(text);
    }
    int ooo=0; for (int i=1; i<len; i++) if (buf[i+off]!=buf[i+off-1]+1) ooo++;
    info("Total "+len+"/"+RDP_PACKET_QUEUE_SIZE+" packets.  OutOfOrder="+ooo);
  }

  /** get temporary Remote for writing to an unOpened Remote
   *  @param ra RemoteAddress (contains port number and address)
   *  @return A reference to the Remote instance that is connected to the given port and address
   */
  public Remote getRemoteForAddress (RemoteAddress ra) {
    Remote r=indexRemote(ra.port,ra.addr);
    if (r==null) { r=wdef; r.addr=ra.addr; r.port=ra.port; }
    return r;
  }

  /** Send an RMIF message packet with the given fields through the specified
      remote. A new packet is constructed using the parameter values and is
      sent to the remote listener through the Remote. If the flag parameter is
      set to RDP then the packet will be sent using the reliable mechanism.
      @param func   The function value to send.
      @param info   The info value to send.
      @param buffer The data to send.
      @param boff   The byte offset in buffer where data starts.
      @param bytes  The number of bytes to send.
      @param r      The remote containing the connection parameters to send to.
      @param flags  Controls how the data is to be sent.
      @param adjval Adjunct header value
   */
  public synchronized void sendPacket (byte func, int info, byte[] buffer, int boff, int bytes,
      Remote r, int flags, double adjval) {
    sendPacket(func, info, buffer, boff, bytes, r, flags, adjval, true);
  }

  // may make this public for rmif primitive can use
  // @param copy If RDP packet, should a copy of the buffer be made into the Packet?
  synchronized void sendPacket (byte func, int info, byte[] buffer, int boff, int bytes,
                                        Remote r, int flags, double adjval, boolean copy) {
    if (bytes>MAXPKTBUF) { // More meaningful error message when packet size > MAXPKTBUF
      throw new MidasException("Rmif: Can not send data packet (len="+bytes+") > "+MAXPKTBUF+" bytes.");
    }
    // NTN Bug 1633: Closed remotes should only be allow send PING, PONG, or OPEN packets over the wire
    if (remoteClosedShouldNotSendPacket(r,func)) return; // check differs if STATEM or original
    byte[] buf = dpacko.getData();
    buf[FUNC] = func;
    buf[FLAG] = 0;
    buf[INFO] = (byte)info;
    buf[REP ] = DataTypes.EEEI;
    buf[SEQ ] = -UDP;
    buf[TRY ] = 0;
    buf[RPT ] = 0;
    buf[ADJ ] = 0;
    if ((flags&ADJ_TIME)!=0) {
      buf[FLAG] = ADJ_TIME;
      Convert.packD(buf,8,adjval);
      buf[ADJ] = 8;
    }
    if ((flags&ADJ_OFFSET)!=0) {
      buf[FLAG] = ADJ_OFFSET;
      Convert.packD(buf,8,adjval);
      buf[ADJ] = 8;
    }
    int doff = HEADER_LEN + buf[ADJ];
    if (bytes>0) System.arraycopy(buffer,boff, buf,doff, bytes);
    int len = doff+bytes;
    if (debug) {
      String isRDP = ((flags&0xF)==RDP)? "RDP" : "   ";
      debug("sendPacket "+isRDP+" -> "+new Packet(buf,0,len,false)+"  to  "+r);
    }
    if ((flags&0xF)==RDP) {
      if (sendDelayedRdpPackets(r))  {
        if (r.sendQueue.size() < maxQueue) {
          r.sendQueue.addLast(new Packet(buf,0,len));
        } else {
          String msg = "Rmif: Can not send RDP packet at this time. RDP send queue is full (maxQueue="+maxQueue+").";
          if (supplimentalLogging) msg += " For: "+r;
          throw new MidasException(msg);
        }
      } else {
        r.linkBytes += sendPacketRdp(buf,len,r,copy); // <-- Sets RDP fields and calls sendPacketRaw(..)
      }
    } else {
      r.linkBytes += sendPacketRaw(buf,0,len,r.addr,r.port);
    }
  }

  /** Is remote closed and should not sent packet
   *  The original check for sendPacket
   *
   *  @param r     Remote
   *  @param func  function
   *  @return if remote is closed and should not send the packet
   */
  boolean remoteClosedShouldNotSendPacket (Remote r, byte func) {
    boolean badState = r.state==CLOSED && !(func==OPEN || func==PING || func==PONG);
    if (badState && allOriginalMessages) warning("Remote is CLOSED not sending packet func="+functionName(func)+" to "+r);
    return badState;
  }

  /** Tags an RDP packet.
      @param buf The RMIF packet buffer.
      @param len The length of the buffer.
      @param r   The remote to send to.
      @param copy Should a copy of the buffer be made into the Packet?
      @return Number of bytes send (0 if packet is delayed until later).
   */
  int sendPacketRdp (byte[] buf, int len, Remote r, boolean copy) {
    int seq = (r.sendSeq++) & 0x7f;
    buf[SEQ] = (byte)seq;
    Packet pkt = r.sendBuf[seq];
    if (pkt.time!=0) {
      // JC4 & NTN: Might want to add a check to verify that (pkt.time==0), if it fails,
      // something is seriously wrong (e.g. invalid WINDOW size, or error during
      // handling of RDP receipts)
      String msg = "sendPacketRdp: RDP send buffer/book keeping may be corrupt!";
      if (supplimentalLogging) msg += " For: " + r;
      warning(msg);
    }
    pkt.initialize(buf, 0, len, Time.current(), copy);
    return sendPacketRaw(buf,0,len,r.addr,r.port);
  }

  /** Sends any delayed RDP packets.
      @param r The remote to send packets to.
      @return false if the send queue is clear and more packets can be sent, true if the RDP
              send window is full (none can be sent yet). (This return value matches the return
              of <code>r.isWindowFull()</code>.)
   */
  synchronized boolean sendDelayedRdpPackets (Remote r) {
    while (!r.sendQueue.isEmpty() && !r.isWindowFull()) {
      Packet pack = r.sendQueue.removeFirst();
      sendPacketRdp(pack.buffer, pack.buffer.length, r, false); // already did copy to put on queue, can skip now
    }
    return r.isWindowFull();
  }

  /** Send an RMIF message packet with the given fields through the specified remote. If
      <code>bytes==null</code>, this is the same as
      <code>sendPacket(func,info,null,0,0,r,flags,0.0)</code>; otherwise it matches
      <code>sendPacket(func,info,bytes,0,bytes.length,r,flags,0.0)</code>.
      @param func  The function value to send.
      @param info  The info value to send.
      @param bytes The data to send.
      @param r     The remote containing the connection parameters to send to.
      @param flags Controls how the data is to be sent.
   */
  public void sendPacket (byte func, int info, byte[] bytes, Remote r, int flags) {
    if (bytes==null) sendPacket(func,info,null,0,0,r,flags,0.0);
    else sendPacket(func,info,bytes,0,bytes.length,r,flags,0.0);
  }

  /** Send an RMIF message packet with the given fields through the specified remote. This is the
      same as <code>sendPacket(func,info,null,0,0,r,flags,0.0)</code>
      @param func  The function value to send.
      @param info  The info value to send.
      @param r     The remote containing the connection parameters to send to.
      @param flags Controls how the data is to be sent.
   */
  public void sendPacket (byte func, int info, Remote r, int flags) {
    sendPacket(func,info,null,0,0,r,flags,0.0);
  }

  /** Send raw byte data to a destination. The data is the len bytes from the
      buf parameter starting at the given offset.  The destination is
      determined by the remote parameter.

      @param buf The buffer containing the data to send.
      @param off The offset into the buffer of the data start.
      @param len The number of bytes to send.
      @param addr The remote address to send the data through.
      @param port The remote port to send the data through.
      @return total bytes
      @since 4.1.0 safety fixes contributed by jnuanes
   */
  public synchronized int sendPacketRaw (byte[] buf, int off, int len, InetAddress addr, int port) {
    //jnuanes - the socket can be closed when we shutdown a rmif primitive so we
    //want to send nothing.  If something is sent to the closed socket it will
    //throw an exception and the data buffer will not get garbage collected.
    if (dsock != null && dsock.isClosed()) return 0; // safety fix
    if (buf!=buffo) System.arraycopy (buf,off, buffo,0, len);
    dpacko.setLength(len);
    dpacko.setPort(port);
    dpacko.setAddress(addr);
    try {
      dsock.send(dpacko);
      if ((verbose||logfile!=null) && buffo[SEQ]>=0) {
        int slen=len-8;
        if (buffo[FUNC] >= DHDR && buffo[FUNC] <= DBUFD) slen=0;
        String text= "SEND func="+functionName(buffo[FUNC])+
        " seq="+buffo[SEQ]+" try="+buffo[TRY]+" info="+buffo[INFO]+
        " data="+new String(buffo,DATA,slen)+" to "+addr+":"+port+" at "+Time.tag();
        if (verbose) { info(text); }  // as of 2.8.0, this also writes to logfile
        else         { logfile.writeln(text); }
      }
      if (socksenderr>1) info("Total Packet send errs: "+socksenderr);
      socksenderr=0;
    } catch (Exception e) {
      if (socksenderr==0) throw new MidasException("Packet send: "+e+" Addr:"+addr+" "+port, e);
      socksenderr++;
    }
    int totalBytes = len+packetOverhead;
    linkBytes += totalBytes;
    return totalBytes;
  }

  /** check the remote link for RDP book-keeping
   *  @return status 0 if unsuccessful
   *  @since 4.1.0 updated to use fast remotes
   */
  public int checkRemotes () {
    int stat=0;
    //jnuanes - Made this a local variable that we pass to the methods that need it
    double time = Time.current();
    synchronized(_fastRemotes)
    {
      //jnuanes - We may be deleting a remote in unOpenRemote while we are iterating
      //This safely can iterate a hashtable this way
      Object[] keys = _fastRemotes.keySet().toArray();
      for (Object key : keys) {
        Remote r = _fastRemotes.get(key);
        stat += checkPacketQueue(r, time);
        if (r.recvQueue>0) { checkRecvQueue(r, -1); }
        boolean openedRemoteTimedout = r.isOpened() && (time-r.timePong > timeOut);
        if (openedRemoteTimedout) { unOpenRemote(r, time); } // NTN BUG 1462: Need to also check remote timeout here
                                                             // otherwise timeout could be increased by almost timePing
        if (time-r.timeLast > timePing) {
          if (openedRemoteTimedout); //jnuanes - do nothing we already called unOpenRemote(r);
          //jnuanes - This should probably be rewritten so we don't have an the above if
          else if (r.isClient); // servers don't ping, only clients do
          // jnuanes & NTN: BUG 1541 & 1548: reopen not quite right/cross talk begin of changes
          // else if (r.isOpened() || r.isOpening()) sendPacket(PING,0,r,UDP);    // RMIF 1.0: info field always zero in PING message
          else if (r.isOpened() || r.isOpening()) sendPacket(PING,r.state,r,UDP); // RMIF 1.1: info field has remote's state (OPEN) in PING message
          // jnuanes & NTN: BUG 1541 & 1548: end of changes
          r.timeLast = time;
        }
      }
    }
    return stat;
  }

  /** check the remote link for RDP book-keeping on the send queue
   *  @since 4.1.0 jnuanes - Passing in time to the function so we are not using a member time
   */
  int checkPacketQueue (Remote r, double time) {
    int i,seq,retry,stat=0;
    double t,rtd;
    for (i=r.sendChk; i<r.sendSeq; i++) {
      seq = i&0x7f;
      Packet pkt = r.sendBuf[seq];
      synchronized (pkt) { // make sure no other thread is changing packet
        t = pkt.time;
        if (t==0) {       // already receipted
          if (i==r.sendChk) r.sendChk++;
          continue;
        }
        retry = pkt.getTry();
        // estimate round trip delay
        if (r.latency==0) rtd=1; else rtd=r.timeRetry+r.latency;
        if ((time-t)<(retry+1)*rtd) continue;
        if (t<0 && (i-r.sendChk) >= r.maxWindow) continue;
        if (t>0 && ++retry>r.maxRetry) {
          if (i==r.sendChk) r.sendChk++;
          r.sendDrop++;
          sendLinkProblemReport(r, "PKTDROP Retry="+retry+" Seq="+seq, pkt.copy()); // use copy() since we clear out buffer
          pkt.clear(0);
          continue;
        }
        if (retry>0) r.retried++;
        if (t<0) pkt.time = Time.current();
        pkt.setTry((byte)retry);
        r.linkBytes += sendPacketRaw(pkt.buffer, pkt.off, pkt.len, r.addr, r.port);
        stat++;
      } // end synchronized (pkt)
    }
    sendDelayedRdpPackets(r);
    return stat;
  }

  /** Internal message handling -- usually passes message to the appropriate message handler. */
  int handle (String name, int info, Object data, Object from) {
    Message msg = new Message(name,info,data,handler,from);
    return (handler==null? Commandable.NOOP : handler.processMessage(msg));
  }

  /** Report a link problem to the appropriate (local) message handler. */
  void sendLinkProblemReport (Remote r, String text, Packet pack) {
    Table t    = new Table();
    String rid = (r==null)? "<UNKNOWN>" : r.id;
    if (r!=null) {
      if (supplimentalLogging) {
        text += " " + r.toString();
      }
      else {
        text += " ("+functionName(r.state)+")";
      }
    }
    else {
      text = "null Remote";
    }
    t.put("TEXT",text);
    t.put("REMOTE",rid);
    t.put("PACKET",pack);
    handle("LINKERR",0,t,owner);
  }

  /** return total link bandwidth since last poll in bytes/sec
   *  @return total link bandwidth since last poll in bytes/sec */
  public double getLinkBW() {
    double time = Time.current();
    double rate = linkBytes / (time-linkTime);
    linkTime = time;
    linkBytes = 0;
    return rate;
  }

  /** return average link latency in milliseconds
   *  @return link latency in milliseconds
   *  @since 4.1.0 updated to use fast remotes */
  public int getLinkLatency() {
    int lat, glat=0;
    synchronized(_fastRemotes)
    {
      for (Remote r : _fastRemotes.values())
      {
        lat = (int)(r.latency*1000);
        if (lat>glat) glat=lat;
      }
    }
    return glat;
  }

  /** Process a packet. This is called by {@link #recvPacket()} or {@link #checkRecvQueue(Remote)}
      to process a standard RMIF packet. */
  void procPacket (byte[] buf, int len, Remote r) {
    Table   tbl;
    Channel chan;
    String  msgname;
    String  string;
    double  time=0;
    byte    info = buf[INFO], chanNum = info;
    byte    func = buf[FUNC];
    byte    seq  = buf[SEQ];

    try {
      // get time
      if ((buf[FLAG]&ADJ_TIME)!=0) time = Convert.unpackD(buf,8);

      // adjust for adjunct header
      int doff = buf[ADJ];
      len-=doff; doff+=8;

      double currentTime = Time.current();
      // finish processing based on FUNC field
      switch (func) {
        case DBUF:  // Same as DBUFC - file data access
        case DBUFC:
          chan = getChannel(chanNum);
          if (chan==null || chan.df==null || r==null) {
            warning("Got unSolicited DBUF "+chanNum+" "+r.addr+":"+r.port);
          }
          else if (seq<0 && chan.df.io.avail() < len) {
            warning("Pipe full remote "+r+" - dumping unreliable data");
          }
          else if (func==DBUFC) {
            if (time>0) chan.df.setTimeAt(time);
            int uclen = uncompress(buf,doff,len,uncbuf);
            if (uclen>0) chan.df.write (uncbuf,0,uclen);
          }
          else {
            if (time>0) chan.df.setTimeAt(time);
            chan.df.write(buf,doff,len);
          }
          break;

        case DHDR:
          // JC4: The following section is likely to have issues if given an extended type 3000/5000
          //      file which will have more that 512 bytes in the main header. It is also likely to
          //      have issues with Type 6000 files (column definitions in extended header) and Type
          //      3000/5000/6000 files with long column names (names in extended header). I do not
          //      have a test case for this right now, nor is anyone looking to use it, so we can
          //      just leave this as-is until we need to change it.
          chan = getChannel(chanNum);
          if (chan==null) break;                       // illegal sequence
          DataFile df = chan.df;
          if (df!=null && df.isOpen) {
            if (!reOpen) break;
            df.flags |= BaseFile.REOPEN;
            df.close();
          }
          byte[] db = new byte[512];
          System.arraycopy(buf,doff, db,0, 512);
          df = new DataFile();
          df.init (ref,chan.name,db,0);
          if (!revertSafetyFixes && (df.getTypeCodeClass() != 1))
          {
            // shenders Petrel 3/2/2012 - In order to more reliably process type 2000 data with
            // frame sizes bigger than one RMIF packet, increase the default output pipe size
            // to make room for at least one frame of data
            if ( df.getBPE() * 4 > df.defPipeSize ) {
              info("RMIF: Increase pipe size for: " + chan.name + " to: " + df.getBPE() * 4
                              + " from: " + df.defPipeSize);
              df.defPipeSize = Math.min( (int) 2e6, (int) df.getBPE() * 4 );
            }
          }
          df.open (BaseFile.OUTPUT);
          chan.df = df;
          break;

        case SET: // Same as ACK
        case RET: // Same as ACK
        case GET: // Same as ACK
        case ACK:
          tbl = new Table();
          if (caseSensitive) tbl.setFlags(tbl.getFlagsInt()|Table.CASESENSITIVE);
          tbl.fromBytes(buf,doff,len+doff);
          msgname = "BAD";
          if (tbl.containsKey("MSGNAME")) {
            msgname = tbl.getString("MSGNAME");
            tbl.remove("MSGNAME");
          }
          else if (func==SET) msgname = "SET";
          else if (func==RET) msgname = "RET";
          else if (func==GET) msgname = "GET";
          else if (func==ACK) msgname = "ACK";
          handle(msgname,chanNum,tbl,r);
          break;

        case SEND:
          break;

        case RECV:
          break;

        case MFTP:
          tbl = new Table();
          if (caseSensitive) tbl.setFlags(tbl.getFlagsInt()|Table.CASESENSITIVE);
          tbl.fromBytes(buf,doff,len+doff);
          if (verbose) info("Got MFTP from "+r+" "+tbl);
          processMftpMessage(tbl,r);
          break;

        case PING:         // server function
          if (verbose) info("Got a PING message from "+r+" info="+info);
          if (r!=null && r.isClient) r.timePong = currentTime;
          // NTN note: X-Midas RMIF currently requires info (chanNum) field to be echoed back
          // (since it is used as index to remote) in the PONG response message
          int protocol = (info!=OPENED)? UDP: RDP ; // NTN BUG 1541: send pong message unreliably if remote's state is NOT OPENED
          sendPacket(PONG,chanNum,r,protocol);      //               so PONG message does not get stuck in RDP queue
          break;

        case PONG:         // client function
          if (verbose) info("Got PONG from "+r+" info="+info);
          if (r!=null && !r.isClient) r.timePong = currentTime;
          if (r.isOpening()) openRemote(r);
          break;

        case OPEN:          // server function
          if (verbose) info("Got OPEN from "+r+" chan="+chanNum);
          if (chanNum<=0) {     // open remote
            r.rep      = buf[REP];
            r.state    = OPENED;
            r.isClient = true;
            r.recvChk  = seq;
            // r.timePong = 0;  // ??? 2.7.2 and prior
            r.timePong = currentTime;  // NTN BUG 2017: need to use current time (otherwise, OPENED connection
                                          // on server will NOT timeout to an unOpened state if client is kill
                                          // before it sends the next PING packet.
            sendPacket(OPENED,0,r,RDP);
            handle("OPEN",chanNum,r,owner);
          }
          else if (r.isOpened()) serveChannel(r,chanNum,new String(buf,doff,len));
          else warning("Cannot open channel on closed remote");
          break;

        case OPENED:        // client function
          if (r==null) warning("Received unsolicited OPENED packet");
          else if (chanNum==0) {
            if (r.isOpened()) { if (verbose) info("Channel already opened"); break; }
            r.state=OPENED;
            r.rep=buf[REP];
            for (int ic=1; ic<=nchannel; ic++) {
              chan = getChannel(ic);
              if (chan==null) { if (verbose) info("channel is null for "+ic+",r="+r); }
              else if ((chan.flags&MULTI)!=0) openChannel (ic,r);
              else if (chan.r!=r) warning("No channel open for "+ic+". On different remote "+r);
              else if (chan.state==OPENED) { if (verbose) info("Channel already open"+r); }
              else if (chan.state==OPEN) openChannel(ic,r);
            }
            handle("OPENED",chanNum,r,owner);
          } else {
            // need to handle a channel open here
            if (verbose) info("Already opened channel n="+chanNum);
          }
          break;

        case CLOSE:         // server function
          if (verbose) info("Got CLOSE from "+r+" info/chanNum="+info);
          //jnuanes - always let quiet close through
          if (handleUnopenedConnectionOrRemoteIsSever(r,chanNum,time));
          //jnuanes - Always clean the send and receive queue
          else if(chanNum <= 0) {
            for (int ip=1; ip<=nproperty; ip++) delMember(r,-1,ip);
            if (chanNum == -CLOSE) {}  // RMIF 1.1: QUIET CLOSE requested from remote client (do not respond with CLOSED)
            else { sendPacket(CLOSED,0,r,RDP); }
            r.state=CLOSED;
            resetSendQueue(r); // BUG 1541, 1548 & 1631 - reset send queue for remote
            handle("CLOSE",chanNum,r,owner);
          }
          else { // remote is client, close the specified channel
            for (int ip=1; ip<=nproperty; ip++) delMember(r,chanNum,ip);
            sendPacket(CLOSED,chanNum,r,RDP);
          }
          break;

        case CLOSED:        // client function
          if (r==null) warning("Received unsolicited CLOSED packet");
          else if (chanNum==0) { r.state=CLOSED; handle("CLOSED",chanNum,r,owner); }
          else if (chanNum>0) closedChannel(chanNum,r);
          break;

        case MODIFY:
          if (r==null || !r.isOpened()) warning ("Cannot modify an unOpened remote");
          else {
            tbl = new Table();
            if (caseSensitive) tbl.setFlags(tbl.getFlagsInt()|Table.CASESENSITIVE);
            tbl.fromBytes(buf,doff,len+doff);
            procModify (tbl,r);
          }
          break;

        case GETPROP:
          string = Convert.l2s(nproperty);
          if (chanNum>0 && chanNum<=nproperty) string = getProperty(chanNum).name;
          sendPacket(RETPROP,chanNum,string.getBytes(),r,RDP);
          break;

        case RETPROP:
          string =  new String(buf,doff,len);
          if (verbose) info ("KeyName["+chanNum+"]="+string);
          if (chanNum==0) {  // info field=0 gets number of keys, signals query of each
            if (verbose) info("Getting individual keys");
            int nkeys = Integer.parseInt(string);
            for (int i=1; i<=nkeys; i++) sendPacket(GETPROP,i,r,RDP);
          }
          else handle("PROPERTY",chanNum,string,r);
          break;

        default:
          break;
      }

      // NTN BUG 1462: always update timePong when we process any packets from an OPENED remote.
      // This prevents remote from timing out if data packets are getting through, but PING/PONG packets are not.
      if (r!=null && r.state==OPENED ) { r.timePong = currentTime; }

    } catch (Exception e) {
      // Error processing packet...
      // (1) Display stack trace
      // (2) Send "problem report" to local macro
      // (3) Send "problem report" to remote client (TBD)
      printStackTrace("Exception in procPacket from remote "+r,e);
      sendLinkProblemReport(r,"Exception in procPacket: "+e.getMessage(),
                            new Packet(buf,0,len) );
      //sendPacket(<ERROR>,chanNum,r,RDP); // TBD
    }
  }

  /** Handle the CLOSE logic for the cases that the connection is unopened or
   *  the Remote is server. The default logic (not state machine) was pealed off
   *  to make the recvPacket method more readable
   *
   *  @param r        Remote
   *  @param chanNum  channel number
   *  @param time     time
   *  @return         true if this method handled the situation
   */
  boolean handleUnopenedConnectionOrRemoteIsSever (Remote r, byte chanNum, double time) {
    boolean caseHandled = false;
    if (r==null || !r.isOpened()) { // un-OPENED connection, don't need to do anything
      if (chanNum != -CLOSE) warning("Cannot close unOpened remote: "+r); // only warn if NOT quiet CLOSE
      caseHandled = true;
    }
    else if (!r.isClient) { // remote is server
      caseHandled = true;
      // remote server giving a close to a client
      if (chanNum==0 && r.isOpened()) unOpenRemote(r, time);
      else if (chanNum == -CLOSE) { // RMIF 1.1: QUIET CLOSE request from remote server
        // NTN BUG 1541 & 1548: reopen not quiet right/cross talk
        if (verbose) debug("  Got QUIET CLOSE request from "+r);
        boolean reopen = (r.isOpened() || r.isOpening());
        unOpenRemote(r, time);
        // NTN BUG 1541: send out PING to reopen connection with remote server
        if (reopen) sendPacket(PING,r.state,r,UDP);
      }
    }
    return caseHandled;
  }

  String formatName (String name) {
    if (name.startsWith("Files/")) {  // HServer syntax
      name = name.substring(6);
      int i = name.indexOf("/");
      if (i>0) name = name.substring(i+1) + "{AUX=" + name.substring(0,i) + "}";
    }
    return name;
  }

  void processMftpMessage (Table t, Remote r) {
    int todo=0;
    FilePropertyMember fpm=null;
    String func = t.getS("FUNC");
    String name = t.getS("NAME");
    //System.out.println("Recv Mftp "+t+" from "+r);
    // As of JDK17 an extra REMOTE (CLOSED) packet with an empty table is received - NXM-3785
    if (t.isEmpty() || (func==null) || (name==null)) return;
    int info = 0;
    if (func.equals("EXIST")) {
      String s = (M.io.find(formatName(name)))? "Y":"N";
      t.put("EXISTS",s);
      sendPacket(MFTP,info,t.toBytes(),r,UDP);
      return;
    }
    int ip = indexProperty(name,FILE);
    if (ip<=0 && func.equals("OPEN")) {
      addProperty("F:"+name);
      ip = indexProperty(name,FILE);
    }
    Property p = getProperty(ip);
    if (p==null) {
      t.put("LENGTH",-1);
    }
    else if (func.equals("OPEN")) {
      if (!p.bf.isOpen) p.bf.open();
      t.put("LENGTH",p.bf.getSize());
      fpm = new FilePropertyMember();
      fpm.addr = r.addr;
      fpm.port = r.port;
      fpm.pktlen = t.getL("PKTLEN",1024);
      fpm.rate = t.getD("RATE",maxLinkRate);
      fpm.todo = 0;
      p.pms[++p.members] = fpm;
      r.state = OPEN;
    }
    else if (func.equals("READ")) {
      int im = findFilePropertyMember (p,r);
      fpm = (FilePropertyMember)p.pms[im];
      if (fpm.todo>0) { fpm.todo=0; Time.sleep(0.001); } // stop previous transfer
      fpm.offset = t.getD("OFFSET");
      fpm.pktlen = t.getL("PKTLEN",fpm.pktlen);
      fpm.rate   = t.getD("RATE",fpm.rate);
      fpm.done   = 0;
      // cant set fpm.todo to queue handler thread until after response
      todo       = t.getL("LENGTH");
    }
    else if (func.equals("WRITE")) {

    }
    else if (func.equals("CLOSE")) {
      int im = findFilePropertyMember (p,r);
      p.members--;
      for (int m=im; m<=p.members; m++) {
        p.member[m] = p.member[m+1];
        p.channel[m] = p.channel[m+1];
        p.pms[m] = p.pms[m+1];
      }
      if (p.members==0) {
        p.bf.close();
        delProperty(ip);
      }
      r.state = CLOSE;
    }
    sendPacket(MFTP,info,t.toBytes(),r,UDP);
    if (todo>0 && fpm!=null) fpm.todo = todo;
  }

  int findFilePropertyMember (Property p, Remote r) {
    for (int im=1; im<=p.members; im++) {
      FilePropertyMember fpm = (FilePropertyMember)p.pms[im];
      if (fpm.port==r.port && fpm.addr.equals(r.addr)) return im;
    }
    return 0;
  }

  void serveChannel (Remote r, int ic, String name) {
    if (name.startsWith("PIPE:")) {
      String pname = name.substring(5);
      int ip = indexProperty(pname,PIPE);
      if (ip>0) {
        DataFile df = (DataFile)getProperty(ip).bf;
        if (df.isOpen) sendPacket(DHDR,ic,df.hb,r,RDP);
        addMember(r,ic,ip);
      } else warning("Pipe not found: "+pname);
    }
    else if (name.equals("Q:SET")) addMember(r,ic,1);
    else if (name.equals("Q:GET")) addMember(r,ic,2);
    else if (name.equals("Q:RET")) addMember(r,ic,3);
    else if (name.equals("Q:ACK")) addMember(r,ic,4);
    else {
    }
  }

  int uncompress (byte[] buf, int offset, int len, byte[] dbuf) {
    int i=0,j,k,n,m; byte func,funcl=0,b=0,b2=0;
    if (lutu==null) initTables();
    for (n=offset,len+=offset; n<len;) {
      func = buf[n++];
      m = buf[n++]; if (m<0) m *= -128;
      switch (func) {
      case DC_RAW:
        System.arraycopy (buf,n, dbuf,i, m);
        n+=m; i+=m;
        break;
      case DC_PACK:
        for (j=i+m; i<j;) {
          b = buf[n++];
          k = i+8; if (k>j) k=j;
          for (;i<k;) {
            b2=buf[n++];
            dbuf[i++]=(byte)(b-lutm[b2&0xF]);
            dbuf[i++]=(byte)(b-lutm[(b2>>4)&0xF]);
          }
        }
        i=j;
        break;
      case DC_BAND:
        int nband=buf[n++], nbh=nband/2, nb=0;
        for (j=i+m; i<j; nb--) {
          if (nb==0) { dbuf[i++]=b=buf[n++]; b2=buf[n++]; nb=nband; }
          else if (nb>=nbh) dbuf[i++]=b2;
          else dbuf[i++]=b;
        }
        i=j;
        break;
      case DC_CONST:
        b = buf[n++];
        for (j=i+m; i<j;) dbuf[i++] = b;
        break;
      default:
        Shell.warning("Aborted uncompress due to unhandled function: "+func+" "+funcl);
        return 0;
      }
      funcl = func;
    }
    return i;
  }

  /** Initializes compression tables. */
  void initTables() {
    int i,j;
    lutl = new byte[256];
    lutu = new byte[256];
    for (i=j=0; i<256; i++) {
      if (j<15 && lutm[j+1]-i < i-lutm[j]) j++;
      lutl[i]=(byte)j; lutu[i]=(byte)(j<<4);
    }
/*
    for (i=j=0; j<256; ) { lutl[j++]=lutm[i]; i = (++i)%16; }
    for (i=j=0; j<256; ) { lutu[j++]=lutm[i]; if (j%16==0) i++; }
*/
  }

  int compress (PipePropertyMember prop, byte[] tmp, int len, byte[] buf) {
    byte bmax,bmin,b;
    int i,j,k,kk,n,i1,i2,ip,ipk;
    int nband,nbh,imax,lenchk=0;
    if (lutu==null) initTables();
    i1 = Math.max(0,Math.min(len,prop.trim1));
    i2 = Math.max(1,Math.min(len,prop.trim2));
    if (i2==1) i2=len;
    nband = ((i2-i1)+(prop.plotwidth/2))/prop.plotwidth;
    // jnuanes - using while loops is a better way to right the following for loops
    // for (;i1<len-1 && tmp[i1]==tmp[i1+1]; i1++);  if (i1<5) i1=0;
    // for (;i2>i1+2 && tmp[i2-2]==tmp[i2-1]; i2--); if (len-i2<5) i2=len;
    while(i1<len-1 && tmp[i1]==tmp[i1+1]) i1++;  if (i1<5) i1=0;
    while(i2>i1+2 && tmp[i2-2]==tmp[i2-1]) i2--; if (len-i2<5) i2=len;
    i=n=0;
    /* left edge constant */
    for (; i<i1; i+=k) {
      k=kk=i1-i; if (k>127) { kk = -k/128; k = -kk*128; }
      buf[n++] = DC_CONST;
      buf[n++] = (byte)kk;
      buf[n++] = tmp[i1];
      lenchk += k;
    }
    /* center data */
    for (;i<i2;) {
      k=kk=i2-i; if (k>127) { kk = -k/128; k = -kk*128; }
      if (prop.comp == 1) {
        buf[n++] = DC_RAW;
        buf[n++] = (byte)kk;
        System.arraycopy (tmp,i, buf,n, k);
        n+=k; i+=k;
      }
      else if (nband>=3) {
        buf[n++] = DC_BAND;
        buf[n++] = (byte)kk;
        buf[n++] = (byte)nband; nbh = nband/2;
        for (ipk=i+k; i<ipk; ) {
          j=i+nband; if (j>ipk) j=ipk;
          ip=imax=i; bmax=bmin=tmp[i++];
          for (;i<j;) { b=tmp[i++];
            if (b>bmax) { bmax=b; imax=i-1; }
            else if (b<bmin) bmin=b;
          }
          if (imax-ip>=nbh) { buf[n++]=bmin; buf[n++]=bmax; }
          else              { buf[n++]=bmax; buf[n++]=bmin; }
        }
        i = ipk;
      }
      else {
        buf[n++] = DC_PACK;
        buf[n++] = (byte)kk;
        for (ipk=i+k; i<ipk; ) {
          ip=i; j=i+8; if (j>ipk) j=ipk;
          bmax=bmin=tmp[i++];
          for (;i<j;) {
            b=tmp[i++];
            if (b>bmax) bmax=b;
            else if (b<bmin) bmin=b;
          }
          buf[n++] = bmax;
          for (i=ip; i<j;) {
            b = lutl[bmax-tmp[i++]];
            if (i<j) b |= lutu[bmax-tmp[i++]];
            buf[n++] = b;
          }
        }
        i = ipk;
      }
      lenchk += k;
    }
    /* right edge constant */
    for (; i<len; i+=k) {
      k=kk=len-i; if (k>127) { kk = -k/128; k = -kk*128; }
      buf[n++] = DC_CONST;
      buf[n++] = (byte)kk;
      buf[n++] = tmp[i2];
      lenchk += k;
    }
    if (lenchk==len) return n;
    warning("Compression error "+lenchk+" of "+len);
    return 0;
  }

  /** Adds a property. The following syntax is allowed for the name:
      <pre>
        _pipe               Data pipe (same name on client/server)
        _local=_remote      Data pipe (different name on client/server)
       {_local=_remote}     Data pipe (alternate syntax)
        Q:&lt;name&gt;            Message Queue
        F:&lt;name&gt;            File (MFTP)
        &lt;name&gt;              Result
      </pre>
      @param name property name
      @see #delProperty(int)
   */
  public void addProperty (String name) {
    int ip = ++nproperty;
    if (verbose) info("Opening property "+ip+": "+name);
    Property p = new Property();
    if (name.charAt(0)=='{') name = name.substring(1,name.length()-1);
    int ie = name.indexOf('=');
    if (ie>=0) { p.name=name.substring(0,ie); p.rname=name.substring(ie+1); }
    else { p.name=p.rname=name; }
    p.name = name;
    if (name.charAt(0) == '_') {
      p.type = PIPE;
      if (ref instanceof MidasReference) {
        p.bf = new DataFile((MidasReference) ref,name);
      } else {
        p.bf = new DataFile(Convert.ref2Midas(ref),name);
      }
      p.bf.setFlags(BaseFile.ALWAYSREOPEN);
      if (p.bf.find(-1)) p.bf.open();
    }
    else if (name.startsWith("Q:")) {
      p.type = MESSAGE;
      p.name = name.substring(2);
    }
    else if (name.startsWith("F:")) {
      p.type = FILE;
      p.name = name.substring(2);
      if (ref instanceof MidasReference){
        p.bf = new BaseFile((MidasReference) ref,formatName(p.name));
      } else {
        p.bf = new BaseFile(Convert.ref2Midas(ref),formatName(p.name));
      }
    }
    else {
      p.type = RESULT;
    }
    properties.put(ip-1,p.name,p);
  }

  /** Deletes a property.
      @param ip Property index in the KeyVector (see {@link #getProperties()}).
      @see #addProperty(String)
   */
  public void delProperty (int ip) {
    properties.remove(ip-1);
    nproperty = properties.size();
  }

  int indexProperty (String name, int type) {
    for (int ip=1; ip<=nproperty; ip++) {
      Property p = getProperty(ip);
      if (!name.equals(p.name)) continue;
      if (type<0 || type==p.type) return ip;
    }
    return 0;
  }

  int indexMember (Remote r, int ip) {
    Property p = getProperty(ip);
    for (int n=1; n<=p.members; n++) {
      if (p.member[n] == r) return n;
    }
    return 0;
  }

  void addMember (Remote r, int ic, int ip) {
    int n;
    Property p = getProperty(ip);
    for (n=1; n<=p.members; n++) {
      if (p.member[n] == r) return;
    }
    n = p.members+1;
    if (n>=MAXMEMBER) throw new MidasException("Too many members on "+p);
    p.member[n] = r;
    p.channel[n] = (byte)ic;
    if (p.type==PIPE) p.pms[n] = new PipePropertyMember();
    if (p.type==FILE) p.pms[n] = new FilePropertyMember();
    p.members=n;
  }

  void delMember (Remote r, int ic, int ip) {
    Property p = getProperty(ip);
    for (int n=1; n<=p.members; n++) {
      if (p.member[n] == r && (p.channel[n] == ic || ic<0)) {
        p.members--;
        for (int m=n; m<=p.members; m++) {
          p.member[m] = p.member[m+1];
          p.channel[m] = p.channel[m+1];
          p.pms[m] = p.pms[m+1];
        }
        return;
      }
    }
  }

  /** Convenience method used by procPacket(..) to create a client Remote */
  Remote createClientRemote (String id, String host, int port) {
    Table tbl = new Table();
    tbl.put("HOST", host);
    tbl.put("PORT", port);
    tbl.put("ISCLIENT", true);
    //jnuanes - not sure this does what you want it to do.  A Table does not maintain order
    //  unless you uses one of the ordered versions, but you don't specify this so setting ID
    //  first or last makes not difference. Read your comment below.
    tbl.put("ID",   id);  // 2009-11-24 NTN: moved setting ID to last, so that other properties are
                          // available for warning/debug messages in Remote.setId(..)
    return addRemote(tbl);
  }

  /** Adds a new remote.
      @param targs Table of arguments used to initialize a new remote connection.
      @return the Remote
      @see #openRemote(Table)
      @since 4.1.0 updated to use fast remotes
   */
  public Remote addRemote (Table targs) {
    if (debug) debug("addRemote targs="+targs);

    Remote r = new Remote(this);
    rlast = null; // only used when using Old KeyVector Remote storage
    try {
      KeyObject.setKeys(r,targs);
      r.addr = InetAddress.getByName(r.host);
      Remote rold = indexRemote(r.port, r.addr);
      if (rold!=null) {
        // Reset the ID if it is not equal to the old one
        if (r.id != null && !r.id.equals(rold.id) ) {
          info("Overriding old remote ID "+rold.id+" with "+r.id);
          rold.setID(r.id);
        }
        r = rold; // use reference to existing Remote so that RDP resends will be ignored.
      }
      if ((r.id!=null) && (M != null)) M.registry.put(r.id,r);
      else r.id = this.id;
      r.rmif = this;
      r.mh = owner;
      if (verbose) { info("Remote address: "+ r.addr+" on port "+r.port + " -> "+r); }
      if (r.isClosed()) { r.state = ADDR; } // 2010-04-29 NTN: added this back to fix BUG 2138
                                            // to allow talking to remotes that have been ADDRessed
      else if (verbose) { info("ReOpening unclosed remote "+r); }

      if (r.mh == null) { warning("addRemote() MessageHandler is null for "+r); }
      String rname = r.id;
      if (!useLegacyRemoteNames)  rname = createFastLookupKey(r.addr, r.port);
      else if (rname.equals("REMOTE")) rname = rname + "_" + (++remoteIndex);
      _fastRemotes.put(rname, r);
    } catch (Exception e) {
      info("Problem="+e.getMessage()+"   with addRemote="+targs);
      r = null;
    }
    return r;
  }

  /** Return a reference to the Remote that is talking to the remote address
      and port.  If there is no Remote currently registered that is talking to
      that address/port combination, the method will return null.
      @param port The remote port number
      @param addr The remote address
      @return A reference to the Remote instance that is connected to the
      given port and address.
      @since 4.1.0 updated to use fast remotes
  */
  Remote indexRemote (int port, InetAddress addr) {
    if (!useLegacyRemoteNames) {
      //jnuanes - changed to lookup remotes faster
      return _fastRemotes.get(createFastLookupKey(addr, port));
    }
    else {
      return legacyIndexRemote(port,addr);
    }
  }

  Remote legacyIndexRemote (int port, InetAddress addr) {
    synchronized (_fastRemotes) {
      for (Remote r : _fastRemotes.values()) {
        if (port==r.port && addr.equals(r.addr)) {
          return r;
        }
      }
    }
    return null;
  }

  /** Adds a new remote and opens it. This calls <code>addRemote(args)</code> and then opens the
      the connection.
      @param args Table of arguments used to initialize a new remote connection.
      @return new remote
      @see #addRemote(Table)
   */
  public Remote openRemote (Table args) {
    if (debug||verbose) debug("openRemote args="+args+" (public method)");
    Remote r = addRemote(args);
    if (r!=null) {
      double currentTime = Time.current();
      r.timeLast = currentTime;
      if (!r.isOpening()) r.timePong = currentTime;
      // jnuanes & NTN: BUG 1633 - Set Remote's state to OPEN before sending
      // out PING packet letting the server know we want to open. Otherwise, on
      // fast networks the PONG can come back and we will be in the CLOSED state.
      r.state = OPEN;
      // jnuanes & NTN: BUG 1541 & 1548: reopen not quite right/cross talk
      // sendPacket(PING, 0, r, UDP); // RMIF 1.0: info field always zero in PING message
      sendPacket(PING, OPEN, r, UDP); // RMIF 1.1: info field has remote's state (OPEN) in PING message
    } else {
      warning("Failed openRemote for "+args);
    }
    return r;
  }

  /** Open a remote connection. Called by {@link #procPacket(byte[], int, Remote)}
      to open or reopen a connection following a PONG message from a remote.
   */
  void openRemote (Remote r) {
    if (debug|verbose) info("Opening remote "+r);
    if (r!=null) {
      // jnuanes & NTN BUG 1631: do some clean up before sending OPEN response
      // jnuanes - the test needs to be fixed to address the problems,  we don't
      // want an old remote sending out garbage to a new client
      if (!reuseRemotes) resetSendQueue(r); // NTN: this causes problems with regression (test_rmif_remote_reuse)
      resetRecvQueue(r, -1);
      sendPacket(OPEN, 0, r, RDP);
    }
    else warning("Bad remote on openRemote");
  }

  void unOpenRemote (Remote r, double time) {
    if (debug) debug("unOpenRemote "+r+" time="+time+" timePong="+r.timePong+" timeLast(Ping)="+r.timeLast);
    if (r.isOpening()) return;
    //jnuanes - use function isClient()
    if (r.isClient() && r.timePong==0) return;   // not really started yet
    r.state = OPEN;
    for (int ic=1; ic<=nchannel; ic++) {
      Channel c = getChannel(ic);
      if (c==null);
      else if (c.r==r) c.state=OPEN;
    }
  }

  /** Converts the given object to a remote.
      @param obj  The object to convert to a remote.
      @return The remote instance or null if unable to convert this object to a remote instance.
   */
  public Remote getRemote (Object obj) {
    Remote r = null;

         if (obj instanceof Remote       ) { r = (Remote)obj; }
    else if (obj instanceof Table        ) { r = getRemote((Table)obj); }
    else if (obj instanceof RemoteAddress) { r = getRemoteForAddress((RemoteAddress)obj); }

    return r;
  }

  /** Get a remote based on the ID in a table
   *  @param targs the ID
   *  @return the remote
   */
  public Remote getRemote (Table targs) {
    if (targs==null) return null;
    //jnuanes - renamed to tid so it doesn't hide the id member
    String tid = targs.getS("ID");
    if (tid==null) return null;
    return getRemote(tid);
  }

  /** Closes the given remote and removes it from the set of remotes. This is the same as
      <code>removeRemote(getRemote(targs))</code>.
      @param targs The remote to remove.
      @see #removeRemote(Rmif.Remote)
   */
  public void removeRemote (Table targs) {
    Remote r = getRemote(targs);
    removeRemote(r);
  }

  /** Closes the given remote and removes it from the set of remotes. This calls
      {@link #closeRemote(Rmif.Remote)} to close the remote if it is still open.
      @param r The remote to remove.
      @see #closeRemote(Rmif.Remote)
      @since 4.1.0 updated to use fast remotes
   */
  public void removeRemote (Remote r) {
    if (r==null) return;
    //Always remove the remote from the list first so no one tries to use it
    //while we are cleaning up
    String lookupKey = (!useLegacyRemoteNames) ? createFastLookupKey(r.addr, r.port) : r.id;
    _fastRemotes.remove(lookupKey);
    if (r.isOpened())  closeRemote(r); // Close an open connection
    if (r.isOpening()) closeRemote(r); // Close a connection that is being opened
    if ((r.id!=null) && (M!=null)) {
      M.registry.remove(r.id);
    }
  }

  /** Close the given remote, removing it from the context registry in the process. All open
      channels and properties associated with this remote are also closed. If the remote is null
      then the method has no effect. <br>
      <br>
      Upon returning from this method the given remote will be closed along with all of its channels
      and properties. The state of the remote will have been set to {@link #CLOSED} but it will NOT
      have been removed from the set of remotes (this allows it to be reopened later). Use
      {@link #removeRemote(Rmif.Remote)} to remove a remote from the set of remotes.
      @param r The remote to close.
      @see #removeRemote(Rmif.Remote)
   */
  public void closeRemote (Remote r) {
    if (debug) debug("closeRemote r="+r);

    if (r==null) return;
    if ((r.id!=null) && (M!=null)) M.registry.remove(r.id);
    if (verbose) info("Closing remote "+r);
    for (int ic=1; ic<=nchannel; ic++) { // close channels
      Channel c = getChannel(ic);
      if (c==null);
      else if (c.r!=r);
      else if (c.state==CLOSED);
      else closeChannel(ic);
    }
    for (int ip=1; ip<=nproperty; ip++) delMember (r,-1,ip);
    //jnuanes - Only send a close if the remote is for a client->server connection
    //          A server should never tell a client to CLOSE because a server is
    //          always up and if it goes down the client will PING timeout and try
    //          reopening.
    if (r.isOpened() && (revertSafetyFixes || !r.isClient())) sendPacket(CLOSE, 0, r, RDP);
    if (revertSafetyFixes) r.state = CLOSED;
    resetRecvQueue(r, -1);
    resetSendQueue(r);
    //jnuanes - Don't set the state until we are CLOSED setting the state to
    //CLOSED is kind of silly because we are supposed to wait for the CLOSED
    //message in the NM version of the protocol.  Another interesting thing
    //is that we should set the state to CLOSE before we send the CLOSE packet.
    //so we move to a closing state.
    if (!revertSafetyFixes) r.state = CLOSED;
  }

  /** Adds a channel. Same as <code>addChannel(r,ic,name,0)</code>.
      @param r    Remote connection.
      @param ic   Channel number.
      @param name Channel name.
   */
  public void addChannel (Remote r, int ic, String name) {
    addChannel(r,ic,name,0);
  }

  /** Adds a channel to a Remote.
      @param r     Remote connection.
      @param ic    Channel number (use ic=-1 to automatically assign next available number).
      @param name  Channel name (e.g. "SET", "GET", "RET", "ACK".
      @param flags Channel flags (see {@link Rmif.Channel}).
   */
  public void addChannel (Remote r, int ic, String name, int flags) {
    if (ic<=0) ic = nchannel+1;
    if (verbose) info("Adding channel "+ic+": "+name+" on "+r);
    Channel c = getChannel(ic);
    if (c==null) c = new Channel();
    if (name.charAt(0)=='{') name = name.substring(1,name.length()-1);
    int ie = name.indexOf('=');
    if (ie>=0) { c.name=name.substring(0,ie); c.rname=name.substring(ie+1); }
    else { c.name=c.rname=name; }
    c.r = r;
    c.type = RESULT;
    c.state = OPEN;
    c.flags = flags;
    if (name.equals("SET") || name.equals("GET") ||
        name.equals("RET") || name.equals("ACK")) c.type = KEY;
    if (name.startsWith("Q:")) c.type = MESSAGE;
    if (name.charAt(0) == '_') c.type = PIPE;
    channels.put(ic-1,c.name,c);
    nchannel = Math.max(nchannel,ic);
    if (r!=null && r.isOpened()) openChannel(ic,r);
  }

  /** Opens a channel.
      @param ic Channel number.
      @param r  Remote connection.
   */
  void openChannel (int ic, Remote r) {
    if (verbose) info("Opening channel "+ic+" on "+r);
    Channel c = getChannel(ic);
    if (c==null) return;
    if (c.state == OPENED && (c.flags&MULTI)==0) return;
    if (r==null) r=c.r; // default remote
    if (c.type == PIPE) {
      String rname = "PIPE:"+c.rname;
      sendPacket(OPEN, ic, rname.getBytes(), r, RDP);
    } else if (c.type == RESULT) {
      sendPacket(OPEN, ic, c.rname.getBytes(), r, RDP);
    } else if (c.type == KEY) {
      sendPacket(OPEN, ic, c.rname.getBytes(), r, RDP);
    } else if (c.type == MESSAGE) {
      sendPacket(OPEN, ic, c.rname.getBytes(), r, RDP);
    }
    c.state = OPENED;
  }

  /** Closes a channel.
      @param ic Channel number.
   */
  public void closeChannel (int ic) {
    if (verbose) info("Closing channel "+ic);
    Channel c = getChannel(ic);
    if (c==null) return;
    c.state = CLOSE;
    if (c.type == PIPE) {
      String name = "PIPE:"+c.name;
      sendPacket(CLOSE, ic, name.getBytes(), c.r, RDP);
    } else {
      sendPacket(CLOSE, ic, c.name.getBytes(), c.r, RDP);
    }
  }

  /** Closes a channel.
      @param name  Channel name.
   */
  public void closeChannel (String name) {
    if (verbose) info("Closing channel "+name);
    for (int ic=1; ic<=nchannel; ic++) {
      Channel c = getChannel(ic);
      if (c==null);
      else if (!c.name.equals(name));
      else if (c.state==CLOSED);
      else closeChannel(ic);
    }
  }

  void closedChannel (int ic, Remote r) {
    Channel c = getChannel(ic);
    if (c.state==CLOSE && c.r==r) c.state=CLOSED;
  }

  /** Converts from function ID to name (see {@link #functionList}).
   *  @param func function ID
   *  @return function name
   */
  public static String functionName (byte func) {
    return RmifInterface.functionName(func);
  }

  /** Converts from protocol ID to name.
   *  @param protocol protocol ID
   *  @return protocol name
   */
  public static String protocolName (int protocol) {
    return Parser.get(protocolList,protocol,-1);
  }

  /** Converts from packet sequence number to protocol name (see {@link Rmif} for details).
      @param seq packet sequence number
      @return protocol name
      @since NeXtMidas 2.5.0
   */
  public static String protocolNameForSeq (int seq) {
    return (seq >= 0)? "RDP": protocolName(-seq); // See comments about "seq" for details.
  }

  /** Converts property type ID to string (see {@link #typeList}).
   *  @param type property type ID
   *  @return property name
   */
  public static String propTypeName (int type) {
    return Parser.get(typeList,type);
  }

  /** brief description of command
   *  @return brief description containing id, host name, and port */
  public String getTag() {
    String hostname = dsock.getLocalAddress().getHostName();
    return id+"-Local-"+hostname+"-"+getPort();
  }

  /** serve all properties loop
   *  @return status - 0 for error */
  public int serveProperties() {
    int stat = 0;
    for (int ip=1; ip<=nproperty; ip++) {
      Property p = getProperty(ip);
      try {
        // jnuanes - if the property is null move onto the next.  Seems like
        // this would never happen
        if (p == null) continue;
        else if (p.type == PIPE)   stat += servePipeProperty(p);
        else if (p.type == RESULT) stat += serveResultProperty(p);
        else if (p.type == FILE)   stat += serveFileProperty(p);
        if (stat > 0) {     // if successfully served property
          p.errorCount = 0; // since 3.5.0: reset error count (BUG 2756)
        }
      } catch (Exception ex) { // since 3.3.3 for BUG 2756
        Table t = new Table();
        t.put("PROPERTY",  p);
        t.put("EXCEPTION", ex);
        // since 3.5.0: increment error count and remove it after specified error count (BUG 2756)
        p.errorCount++;
        if (p.errorCount >= maxErrorsToNotServeProperty) {
          delProperty(ip);
          if (p.bf != null) {
            p.bf.close();
          }
          t.put("COMMENT", "No longer serving property [" + p + "] after " + p.errorCount + " errors.");
        }
        handle("ERROR_SERVING_PROPERTY", 0, t, owner);
      }
    }
    return stat;
  }

  /** Serves a packet. This is used to broadcast a packet to all subscribers of a given property
      type. This iterates over all of the listeners for the property and sends the packet to each
      of them with {@link #sendPacket(byte,int,byte[],int,int,Rmif.Remote,int,double)}.
      @param func  The property type ({@link #ACK}, {@link #RET}, {@link #SET}, or {@link #GET}).
      @param info  The packet info  (passed to <code>sendPacket(..)</code>).
      @param bytes The packet bytes (passed to <code>sendPacket(..)</code>).
      @param flags The packet flags (passed to <code>sendPacket(..)</code>).
   */
  public synchronized void servePacket (byte func, int info, byte[] bytes, int flags) {
    Property p = null;
         if (func==ACK) p = getProperty(4);
    else if (func==RET) p = getProperty(3);
    else if (func==SET) p = getProperty(1);
    else if (func==GET) p = getProperty(2);

    if (p==null) warning("Illegal packet to server "+func);
    else for (int im=1; im<=p.members; im++) {
      Remote r = p.member[im];
      if (!r.isOpened()) continue;
      sendPacket(func,info,bytes,0,bytes.length,r,flags,0.0);
    }
  }

  void procModify (Table t, Remote r) {
    if (verbose) info("Processing Modify on "+r);
    String name = t.getS("PROPERTY");
    int ip = indexProperty(name,-1);
    if (ip<=0) return;
    Property p = getProperty(ip);
    if (p.type == PIPE) {
      int im = indexMember(r,ip);
      if (im<=0) return;
      PipePropertyMember ppm = (PipePropertyMember)p.pms[im];
      if (t.containsKey("DEC")) ppm.dec = t.getB("DEC");
      if (t.containsKey("COMP")) ppm.comp = t.getB("COMP");
      if (t.containsKey("TRIM1")) ppm.trim1 = Math.max(0,t.getL("TRIM1")-1);
      if (t.containsKey("TRIM2")) ppm.trim2 = Math.max(0,t.getL("TRIM2"));
      if (t.containsKey("PLOTWIDTH")) ppm.plotwidth = Math.max(0,t.getL("PLOTWIDTH"));
    }
  }

  /** @deprecated Since NeXtMidas 2.5.0: Internal use only; use {@link #serveProperties()}.
   *  @param p Pipe Property
   *  @return status
   */
  @Deprecated public int servePipeProperty (Property p) {
    int nr,xfer,bytes=0,chan,stat=0;
    byte rep=0, buf[]=null;
    DataFile df=(DataFile)p.bf;
    double time=0;
    if (!df.isOpen) {
      if (!df.find(-1)) return stat;
      if (!df.open(BaseFile.NOABORT)) return stat;
      reOpenPipeProperty(p);
    }
    nr = (int)df.avail();
    if (nr==CoreIO.CLOSED) {
      df.close();
      return stat;
    }
    if (nr==CoreIO.REOPEN) {
      p.bf = df.reOpen();
      reOpenPipeProperty(p);
    }
    if (nr<=0) return stat;
    for (int im=1; im<=p.members; im++) {
      Remote r = p.member[im];
      if (!r.isOpened()) continue;
      chan = p.channel[im];
      PipePropertyMember ppm = (PipePropertyMember)p.pms[im];
      if (ppm.dec>1) {
        if (++ppm.count < ppm.dec) continue;
      }
      if (buf==null) {          // get new data
        if (df.getTimeLineHandler()!=null) time = df.getTimeAt();
        if (df.typeClass==2 && df.ape==1) xfer=(int)(MAXPKTBUF/df.dbpe);
        else if (nr==1 || df.typeClass==2) xfer=1;
        else xfer = (int)(1024/df.dbpe);
        xfer = Math.min(nr,Math.max(1,xfer));
        bytes = (int)(xfer*df.dbpe);
        buf = new byte[bytes];
        df.read(buf,0,bytes);
        rep = Shell.rep;
      }
      if (ppm.comp!=0 && df.dataType==DataTypes.BYTE) {
        int clen = compress(ppm,buf,bytes,cbuf);
        int flg=UDP; if (time>0) flg |= ADJ_TIME;
        sendPacket(DBUFC,chan,cbuf,0,clen,r,flg,time);
      }
      else if (df.dataType==DataTypes.BYTE || r.rep==rep) {
        int flg=UDP; if (time>0) flg |= ADJ_TIME;
        sendPacket(DBUF,chan,buf,0,bytes,r,flg,time);
      }
      else {
      }
      stat++;
    }
    if (buf==null) df.skip(nr); // never read the data, skip it
    return stat;
  }

  void reOpenPipeProperty (Property p) {
    for (int im=1; im<=p.members; im++) {
      Remote r = p.member[im];
      if (!r.isOpened()) continue;
      int chan = p.channel[im];
      DataFile df = (DataFile)p.bf;
      sendPacket(DHDR,chan,df.hb,r,RDP);
    }
  }

  /** @deprecated Since NeXtMidas 2.5.0: Internal use only; use {@link #serveProperties()}.
   *  @param p Results Property
   *  @return 0 */
  @Deprecated public int serveResultProperty (Property p) {
    if (p.members>0) return 0;
    return 0;
  }

  /** @deprecated Since NeXtMidas 2.5.0: Internal use only; use {@link #serveProperties()}.
   *  @param p File Property
   *  @return status */
  @Deprecated public int serveFileProperty (Property p) {
    int stat=0;
    for (int im=1; im<=p.members; im++) {
      FilePropertyMember fpm = (FilePropertyMember)p.pms[im];
      if (fpm.todo<=0) continue;
      if (fpm.done==0) {
        p.bf.seek(fpm.offset); // read the data
        if (fpm.dbuf==null || fpm.dbuf.length<fpm.todo) fpm.dbuf = new byte[fpm.todo];
        p.bf.read(fpm.dbuf,0,fpm.todo);
        fpm.time = Time.current();
      }
      else if ((double)fpm.done/(Time.current()-fpm.time) > fpm.rate) continue;
      int doff = fpm.done;
      int dlen = Math.min(fpm.todo,fpm.pktlen);
      wdef.addr = fpm.addr; wdef.port = fpm.port;
      fpm.done+=dlen; fpm.todo-=dlen;
      sendPacket(DBUF,0, fpm.dbuf,doff,dlen, wdef,UDP|ADJ_OFFSET, fpm.offset+doff);
      stat+=1;
    }
    return stat;
  }

  /** Writes an error message out.
   *  @since 4.1.0 jnuanes contributed to support state machine logic */
  void error (String str) {
    str = Time.tag() + " ["+id+"] "+str; // tag all messages with time and RMIF ID
    if (logfile!=null) { logfile.writeln("ERROR:  "+str); }
    if (M == null)     { System.out.println("ERROR:  "+str); }
    else               { M.error(str); }
  }

  /** Writes an info message out. */
  void info (String str) {
    str = Time.tag() + " ["+id+"] "+str; // tag all messages with time and RMIF ID
    if (logfile!=null) { logfile.writeln("INFO:  "+str); }
    if (M == null)     { System.out.println("INFO:  "+str); }
    else               { M.info(str); }
  }

  /** Writes a warning message out. */
  void warning (String str) {
    str = Time.tag() + " ["+id+"] "+str; // tag all messages with time and RMIF ID
    if (logfile!=null) { logfile.writeln("WARN:  "+str); }
    if (M == null)     { System.out.println("WARN:  "+str); }
    else               { M.warning(str); }
  }

  /** Writes a warning message out. */
  void printStackTrace (String str, Throwable t) {
    if (M == null) { t.printStackTrace(); }
    else           { M.printStackTrace(str,t); }
  }

  /** Writes a deprecate message out. */
  void deprecate (String str) {
    if (M == null) { System.out.println("DEPRECATE:"+str); }
    else           { M.deprecate(str); }
  }

  /** Writes a debug message out. */
  void debug (String str) {
    // JC4: May change this in the future...
    str = "DEBUG: " + Time.tag() + " ["+id+"] "+str; // tag all messages with time and RMIF ID
    if (logfile!=null) { logfile.writeln(str); }
    if (M == null)     { System.out.println(str);  }
    else               { M.type(str); }
  }

  /** Set the message handler for messages received from remotes.
      @param mh The Message Handler (for messages received from remotes).
      @return previous message handler
      @since NeXtMidas 3.3.0
   */
  public MessageHandler setMessageHandler (MessageHandler mh) {
    MessageHandler prev = this.handler;
    this.handler = mh;
    if (handler==null) warning("Rmif: message handler (for messages received from remotes) is null!");
    return prev;
  }

  /** Get the current message handler for messages received from remotes.
      @return current message handler (null for none)
      @since NeXtMidas 3.3.0
   */
  public MessageHandler getMessageHandler () {
    return this.handler;
  }

  /** Get maximum number of errors when serving a property (pipe,result, etc.) to client before removing it
   *  to prevent flood of errors on server side. This count is per Property and gets reset after success.
   *  @return maximum number of errors when serving a property
   *  @since NeXtMidas 3.5.0
   */
  public int getMaxErrorsToNotServeProperty () {
    return maxErrorsToNotServeProperty;
  }

  /** Set maximum number of errors when serving a property (pipe,result, etc.) to client before removing it
   *  to prevent flood of errors on server side. This count is per Property and gets reset after success.
   *  Used in {@link #serveProperties()}.
   *  @param newValue maximum number of errors when serving a property
   *  @since NeXtMidas 3.5.0
   */
  public void setMaxErrorsToNotServeProperty (int newValue) {
    this.maxErrorsToNotServeProperty = newValue;
  }

  ////  Additional code that supports fast remote lookup - Jeremy's contribution ////

  /** Create a key we can use to lookup remotes quickly in our _fastRemotes Hashtable
   *  replaces dots in the addr with _ as a . can not be in a NeXtMidas Table key.
   *  @param addr   internet address
   *  @param dport  port
   *  @return fast lookup key
   *  @since 4.1.0 jnuanes contributed to support state machine logic
   */
  String createFastLookupKey(InetAddress addr, int dport)
  {
    String host = addr.getHostAddress().replace(".","_"); //Can't store a . in a table key;
    return "REMOTE_"+host+"_"+dport;
  }

  /** Create remote name
   *  replaces dots in the addr with _ as a . can not be in a NeXtMidas Table key.
   *  @param host   full host address
   *  @param dport  port
   *  @return remote name
   */
  @InternalUseOnly
  public static String getRemoteName (String host, int dport) {
    int start = host.indexOf("/");
    String hostAddr = host.substring(start+1).replace(".","_");
    return "REMOTE_"+hostAddr+"_"+dport;
  }

  ////  Additional code that supports allows op in and out of Jeremy contributed options ////

  /** Set to false to use new address port based remote names.
   *  WHEN using default non-state machine message handling.
   *  @param defaultUsesLegacyRemoteNames true for REMOTE_# style names
   *                              false for address/port style
   */
  public static void setDefaultUsesLegacyRemoteNames (boolean defaultUsesLegacyRemoteNames) {
    Rmif.defaultUsesLegacyRemoteNames = defaultUsesLegacyRemoteNames;
  }

  /**  Set if remote reuse is enabled when running with original message handling
   *   @param remoteReuseEnabled true to enable
   */
  public static void setOrigmRemoteReuse (boolean remoteReuseEnabled) {
    Rmif.origmRemoteReuseEnabled = remoteReuseEnabled;
  }

  /** Set if all original messages are enabled when running with original message handling
   *  @param allOriginalMessages true to enable all original messages
   */
  public static void setOrigmOriginalMessages (boolean allOriginalMessages) {
    Rmif.origmAllOriginalMessages = allOriginalMessages;
  }

  /** Set if extra logging is enabled when running with original message handling
   *  @param extraLoggingEnabled true to enable extra logging
   */
  public static void setOrigmExtraLogging (boolean extraLoggingEnabled) {
    Rmif.origmExtraLoggingEnabled = extraLoggingEnabled;
  }

  /** Set if all original messages are enabled when running with state machine message handling
   *  @param allOriginalMessages true to enable
   */
  public static void setStatemOriginalMessages (boolean allOriginalMessages) {
    Rmif.statemAllOriginalMessages = allOriginalMessages;
  }

  /** Set if extra logging is enabled when running with state machine message handling
   *  @param extraLoggingEnabled true to enable extra logging
   */
  public static void setStatemExtraLogging (boolean extraLoggingEnabled) {
    Rmif.statemExtraLoggingEnabled = extraLoggingEnabled;
  }

  ///  Temporary code that allows debug or emergency reversal to old Remote KeyVector storage ///

  /** Emergency/debug setter to allow revertion to old KeyVector Remote Storage
   *  @param useOldKeyVectorStorage use old KeyVectore Remote Storage
   */
  @InternalUseOnly
  public static void setOldRemoteStorage (boolean useOldKeyVectorStorage) {
    Rmif.oldRemoteStorage = useOldKeyVectorStorage;
  }

  /** check the remote link for RDP book-keeping
   *  @return status 0 if unsuccessful
   */
  @LimitedFutureSupport("remove as soon as sure do not need old remote KeyVector storage")
  private int checkRemotesKeyVector () {
    int stat=0;
    double time = Time.current();
    for (int ir=1; ir<=remotes.size(); ir++) {
      Remote r = getRemote(ir);
      if (r==null) continue;
      stat += checkPacketQueue(r,time);
      if (r.recvQueue>0) { checkRecvQueue(r, -1); }
      boolean openedRemoteTimedout = r.isOpened() && (time-r.timePong > timeOut);
      if (openedRemoteTimedout) { unOpenRemote(r, time); } // NTN BUG 1462: Need to also check remote timeout here
                                                     // otherwise timeout could be increased by almost timePing
      if (time-r.timeLast > timePing) {
        if (openedRemoteTimedout) { unOpenRemote(r, time); }
        else if (r.isClient) {} // servers don't ping, only clients do
        // jnuanes & NTN: BUG 1541 & 1548: reopen not quite right/cross talk begin of changes
        // else if (r.isOpened() || r.isOpening()) { sendPacket(PING,0,r,UDP); }    // RMIF 1.0: info field always zero in PING message
        else if (r.isOpened() || r.isOpening()) { sendPacket(PING,r.state,r,UDP); } // RMIF 1.1: info field has remote's state (OPEN) in PING message
        // jnuanes & NTN: BUG 1541 & 1548: end of changes
        r.timeLast = time;
      }
    }
    return stat;
  }

  /** return average link latency in milliseconds
   *  @return link latency in milliseconds */
  @LimitedFutureSupport("remove as soon as sure do not need old remote KeyVector storage")
  private int getLinkLatencyKeyVector() {
    int lat, glat=0;
    for (int ir=0; ir<remotes.size(); ir++) {
      Remote r = (Remote)remotes.get(ir);
      lat = (int)(r.latency*1000);
      if (lat>glat) glat=lat;
    }
    return glat;
  }

  @LimitedFutureSupport("remove as soon as sure do not need old remote KeyVector storage")
  private Remote indexRemoteKeyVector (int port, InetAddress addr) {
    Remote last = rlast; // this is an atomic set (no synchronization needed)
    if ((last!=null) && (last.port==port) && last.addr.equals(addr)) return last;

    synchronized (remotes) {
      for (int ir=0; ir<remotes.size(); ir++) {
        Remote r = (Remote)remotes.get(ir);
        if (port==r.port && addr.equals(r.addr)) {
          rlast = r;
          return r;
        }
      }
    }
    return null;
  }

  @LimitedFutureSupport("remove as soon as sure do not need old remote KeyVector storage")
  private void removeRemoteKeyVector (Remote r) {
    if (r==null) return;
    if (r.isOpened())  closeRemote(r); // Close an open connection
    if (r.isOpening()) closeRemote(r); // Close a connection that is being opened
    if ((r.id!=null) && (M!=null)) M.registry.remove(r.id);
    remotes.remove (r.id);
    rlast = null;
  }

  /** used by hardKillForTestingOnly */
  @LimitedFutureSupport("remove as soon as sure do not need old remote KeyVector storage")
  private void clearRemotesKeyVector () {
    for (int ir=0; ir<remotes.size(); ir++) {
      Remote r = (Remote) remotes.get(ir);
      if ((r!=null) && (r.id!=null) && (M!=null)) {
        info("  Removing from registry "+r);
        M.registry.remove(r.id);
      }
    } // end for loop
    remotes.clear();
  }

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

  /** Runnable class that manages the calling of the recvPacket method on a
      single Rmif instance. The run method calls the recvPacket method on the
      Rmif instance until the stop method is invoked. When the stop method is
      invoked the reader will finish with its current invocation of recvPacket
      and then return, finishing the thread.
  */
  class PacketReader implements Runnable {
    Rmif    rmif;          // The Rmif instance that this instance is servicing.
    boolean abort = false; // Controls the execution of the run method.

    /** Initialize a new instance that services the given Rmif. */
    public PacketReader (Rmif rmif) { this.rmif = rmif; }

    /** Invoked by Thread.start to invoke the rmif.recvPacket method until {@link #stop()} is called. */
    @Override
    public void run() { while (!abort) rmif.recvPacket(); }

    /** Cause the run method to cease executing and return. */
    public void stop() { abort = true; }

    /** Return the current state of the abort property. Will be false until {@link #stop()} is called. */
    public boolean isAborted () { return abort; }
  } // end class PacketReader

  /** A remote address. */
  public class RemoteAddress implements IDable,MessageHandler {
    int port;
    InetAddress addr;
    MessageHandler mh;
    /** Rmif.RemoteAddress Constructor
     *  @param r local reference to remote process
     */
    public RemoteAddress (Remote r) {
      addr = r.addr;
      port = r.port;
      mh   = r.mh;
    }
    @Override
    public String getID() { return "UNKNOWN"; }
    @Override
    public int processMessage (Message msg) { return mh.processMessage(msg); }
    @Override
    public String toString() { return "R: RMIF-RemoteAddr "+addr+":"+port; }
  } // end class RemoteAddress

  /** local reference to remote process */
  public class Remote implements IDable,MessageHandler {
    /** ID for remote connection */
    String  id;
    /** Remote host */
    public  String  host;
    /** Remote port */
    int     port;
    /** Remote host address */
    InetAddress addr;
    /** Time of last PING sent (clients only) */
    double  timeLast  = 0.0;
    /** Time when last PONG was sent/received (MJS) */
    double  timePong  = 0.0;
    /** Time before declaring a RDP packet lost (seconds) */
    double  timeLost  = 5;
    /** Time to retry (seconds), used to estimate round-trip delay */
    double  timeRetry = 0.2;
    /** Link latency estimate (seconds) */
    double  latency   = 0.0;
    /** Data representation ('I'=IEEE, 'E'=EEEI) */
    byte    rep;
    /** Does this Remote represent a client? */
    boolean isClient  = false;
    /** Connection state (see Rmif.functionList) */
    byte    state     = CLOSED;
    /** Retry count */
    int     retried   = 0;
    /** Max retries for RDP packets */
    int     maxRetry  = DEFAULT_MAX_RETRY;
    /** Max number of outstanding RDP packets */
    int     maxWindow = DEFAULT_MAX_WINDOW;
    /** Max number of RDP packets on send queue */
    int     maxQueue  = DEFAULT_MAX_QUEUE;
    /** Bytes over link since last call to Remote.getLinkBW() */
    double  linkBytes = 0;
    /** Time of last call to Remote.getLinkBW() (MJS) */
    double  linkTime  = 0;
    //jnuanes - It doesn't seem like a good idea to have a remote referencing its parent
    //The only reason I put this back in is because it is public.
    /** Current RMIF Resource */
    public  Rmif    rmif;
    /** Current Message Handler (usually an RMIF command) */
    public  MessageHandler mh;
    /** Slaved remote command */
    public  Remote  next;

    /* send reliable data protocol (RDP) handlers (book-keeping) */
    /** Current send sequence number for RDP packets                                      */
    int sendSeq = 0;
    /** Oldest active RDP packet sent sequence number that has not been receipted         */
    int sendChk = 0;
    /** Total number of dropped RDP packets (sent to remote, but not receipted by remote) */
    int sendDrop = 0;
    /** sequence number of last received RDP packet (0-127)                               */
    int recvChk = 0;
    /** Current number of RDP packets in receive queue                                    */
    int recvQueue = 0;
    /** Total number of RDP packets received out of order                                 */
    int recvReOrder = 0;

    /** RDP send buffer (index=RDP sequence) */
    Packet[]   sendBuf   = new Packet[RDP_PACKET_QUEUE_SIZE];
    /** RDP receive buffer (index=RDP sequence) */
    Packet[]   recvBuf   = new Packet[RDP_PACKET_QUEUE_SIZE];
    /** Delayed packets needing to be sent */
    LinkedList<Packet> sendQueue = new LinkedList<>();

    /** Generic constructor. */
    public Remote () {
      // Create uninitialized Packets for send and receive buffers that we will
      // reuse over and over again... this is an optimization that allows us to
      // avoid unnecessary object creation.
      for (int i=0; i<RDP_PACKET_QUEUE_SIZE; i++) {
        sendBuf[i] = new Packet();
        recvBuf[i] = new Packet();
      }
    }

    /** Generic constructor that sets default values using Rmif instance.
     *  @param rmif Rmif instance */
    public Remote (Rmif rmif) {
      this();
      //jnuanes - this looks like it was left out I still think having references to your parent is bad
      this.rmif = rmif; // set it separately, but I agree this is where it should be set JTM
      this.maxRetry  = rmif.maxRetry;
      this.maxWindow = rmif.maxWindow;
      this.maxQueue  = rmif.maxQueue;
      this.timeLost  = rmif.defaultTimeLost;
      //jnuanes - added this so all messages to slow sites will work
      this.timeRetry = rmif.defaultTimeRetry; // does not seem to hurt pre-existing users to set something else
    }

    /** local ID for remote command
     *  @return local id
     */
    @Override
    public String getID() { return id; }

    /** @deprecated Since NeXtMidas 2.5.0: Use {@link #isOpening()} or {@link #isOpened()}.
        This method is functionally identical to {@link #isOpening()} even though its name
        implied that it was the same as {@link #isOpened()}.
        @return true if this Remote is open
     */
    @Deprecated public boolean isOpen () {
      deprecate("Remote: Since NeXtMidas 2.5.0, use isOpening() or isOpened() rather than isOpen().");
      return isOpening();
    }

    /** Return true if this Remote is open (client side and server side), false otherwise.
        This returns true when: (1) the client's socket is open, and (2) the client is registered
        with the server (i.e. in the server's list of remotes).
        @return true if this Remote is open
        @since NeXtMidas 2.5.0
     */
    public boolean isOpened () {
      return (state == OPENED);
    }

    /** Return true if this Remote is being opened (client side open, waiting for server).
        This returns true during the brief period between an OPEN message being sent to the
        server and an OPENED message being sent back. <i>In most cases, you will want to use
        the {@link #isOpened()} method rather than this one.</i>
        @return true if this Remote is being opened
        @since NeXtMidas 2.5.0
     */
    public boolean isOpening () {
      return (state == OPEN);
    }

    /** Return true if this Remote is closed, false otherwise.
     *  @return true if this Remote is closed */
    public boolean isClosed () {
      return (state == CLOSED);
    }

    /** set Host:Port syntax parser
     *  @param hostport String of Host:Port */
    public void setHP (String hostport) {
      int i = hostport.indexOf(':');                    // <-- JC4: This is NOT compatible with IPv6
      host = hostport.substring(0,i).toLowerCase();
      port = Integer.parseInt( hostport.substring(i+1) );
    }

    /** Sets the host address of the connection. If the host name is given with a '/' in it, only
        the part before the '/' is used. Note that host names are always converted to lower case.
        @param host the host address of the connection
     */
    public void setHost (String host) {
      int i = host.indexOf('/');
      if (i>0) this.host = host.substring(0,i).toLowerCase();
      else this.host = host.toLowerCase();
    }

    /** Sets the port number used by this connection.
     *  @param port the port number used by this connection */
    public void setPort (int port) { this.port = port; }

    /** Set if this Remote is a client instance, otherwise it is a server instance.
        @param isClient true if this remote is a client instance
        @since NeXtMidas 2.7.3
     */
    public void setIsClient (boolean isClient) { this.isClient = isClient; }
    /** Get if this Remote is a client instance, otherwise it is a server instance.
        @return true if this remote is a client instance
        @since NeXtMidas 2.7.3
      */
    public boolean isClient() { return this.isClient; }

    /** Sets the ID of this remote and updates its entry in the registry (if applicable).
     *  @param newID the ID of this remote */
    public void setID (String newID) {
      String oldID = id;
      id = newID;
      if (!newID.equals(oldID) && (M!=null)) {
        //jnuanes - make the M.registry final so we are all synchronized on the same instance
        //synchronizing on non final variables can cause problems if the reference is moved
        synchronized (M.registry) {
          if (M.registry.get(oldID) == this) { // Remove old registry entry
            M.registry.remove(oldID);
          }
          Object prevRemote = M.registry.get(newID);
          if (prevRemote != null && prevRemote!=this) {
            warning("Overriding existing registry entry for "+newID+" "+prevRemote+" with "+this.toString());
          }
          M.registry.put(newID, this);         // Put new registry entry
        }
      }
    }

    /** Sets the max window size used for this remote. Must be in the range
        1..{@link Rmif#MAX_WINDOW}, default is 12.
        @param value the max window size used for this remote
     */
    public void setWindow (int value) {
      if ((value < 1) || (value > MAX_WINDOW)) {
        throw new MidasException("Rmif: Can not set window for "+this+" to "+value+". Window "
                               + "size must be in the range 1.."+ MAX_WINDOW+".");
      }
      maxWindow = value;
    }

    /** Sets the default value for maximum number of packets on the RDP queue. Must be in the range
        0..N, default is 1024.
        @param value the default value for maximum number of packets on the RDP queue
        @since NeXtMidas 2.5.0
     */
    public void setMaxQueue (int value) {
      if (value < 0) {
        throw new MidasException("Rmif: Can not set maxQueue for "+this+" to "+value+". Max queue "
                               + "size must be in the range 0..N.");
      }
      maxQueue = value;
    }

    /** Gets the host address of the connection.
     *  @return host address */
    public String getHost() {
      return (addr != null)? addr.toString(): null;
    }

    /** Gets the port number used by this connection.
     *  @return port number */
    public int getPort() { return port; }

    /** Gets the current link status as an int. See {@link Rmif#functionList} for a list of valid
        states. Use {@link #getStatus()} to get the status as a string (for macro use).
        @return the current link status as an int
        @since NeXtMidas 2.5.0
     */
    public int getState() {
      return state;
    }

    /** Gets the current link status as a string. See {@link Rmif#functionList} for a list of valid
        states. Use {@link #getState()} to get the status as an int (for library use).
        @return the current link status as a string
     */
    public String getStatus() { return functionName(state); }

    /** Check to see of the connection is alive (checks for a timeout), returns "ON" or "OFF".
     *  @return "ON" if connection is alive, otherwise "OFF" */
    public String getAliveStatus () {
      return (Time.current()-timePong<timeOut)? "ON" : "OFF";
    }

    /** Gets the max window size used. This is the maximum number of outstanding RDP packets
        allowed at any given time. See RMIF explain file for a discussion of how the window
        is used.
        @return he max window size used
     */
    public int getWindow () { return maxWindow; }

    /** Gets the default value for maximum number of packets on the RDP queue. This is the maximum
        number of RDP packets waiting to be sent (due to the window being full). See RMIF explain
        file for a discussion of how the queue is used.
        @return the default value for maximum number of packets on the RDP queue
        @since NeXtMidas 2.5.0
     */
    public int getMaxQueue () { return maxQueue; }

    /** Gets the max number of retries for this Remote.
     *  @return  the max number of retries */
    public int getRetry() { return maxRetry; }
    /** Sets the max number of retries for this Remote.
     *  @param value the max number of retries */
    public void setRetry (int value) { maxRetry = value; }

    /** Get retry count
     *  @return the retry count
     */
    public int getRetried() { return retried; }

    /** Gets an estimate of link latency (in seconds).
     *  @return an estimate of link latency */
    public double getLatency() { return latency; }

    /** Gets the current send sequence number for RDP packets.
     *  @return the current send sequence number for RDP packets */
    public int getSendSeq() { return sendSeq; }

    /** Gets the number of queued RDP messages waiting to be sent.
     *  @return the number of queued RDP messages waiting to be sent */
    public int getSendQueue() { return sendQueue.size(); }

    /** Gets the count of dropped RDP packets (sent to remote, but not received by remote).
     *  @return the count of dropped RDP packets */
    public int getDropped() { return sendDrop; }

    /** Gets the current recv sequence number for RDP packets.
        Jeff: looks like recvChk is the last received sequence number
              and recvSeq is just incremented and not used.
        @return last received sequence number
      */
    public int getRecvSeq () { return recvChk; /* return recvSeq; */ }

    /** Gets the number of RDP messages on the recvQueue. When a RDP packet is received out-of-order
        it is added to the recvQueue pending receipt of preceding packets. This ensures that packets
        are delivered to the message handler in their intended order.
        @return recvQueue number of RDP messages on the recvQueue
     */
    public int getRecvQueue () { return recvQueue; }

//    /** @deprecated Since NeXtMidas 2.5.0: Not used, always 0. */ // removed in 3.9.1
//    @Deprecated public int getLost() {
//      deprecate("Remote: Since NeXtMidas 2.5.0, getLost() is not used and always returns 0.");
//      return 0;
//    }

    /** Gets the total number of RDP packets received out-of-order for this Remote.
     *  @return total number of RDP packets received out-of-order for this Remote */
    public int getReOrdered() { return recvReOrder; }

    /** Set time before declaring a RDP packet lost (seconds) for this Remote.
     *  @param time time before declaring a RDP packet lost */
    public void setTimeLost (double time) { timeLost = time; }
    /** Get time before declaring a RDP packet lost (seconds) for this Remote.
     *  @return time before declaring a RDP packet lost */
    public double getTimeLost () { return timeLost; }

    /** Gets time to retry (seconds) for this Remote.
     *  @return time to retry */
    public double getTimeRetry () { return timeRetry; }
    /** Sets time to retry (seconds) for this Remote.
     *  @param value time to retry */
    public void setTimeRetry (double value) { timeRetry = value; }

    /** Indicates if the send window is full for this Remote.
     *  @return true if the send window is full for this Remote */
    boolean isWindowFull () {
      return (sendSeq-sendChk) >= maxWindow;
    }

    /** Gets the estimated link bandwidth for this Remote.
     *  @return the estimated link bandwidth */
    public double getLinkBW () {
      double time = Time.current();
      double rate = linkBytes / (time-linkTime);
      linkTime = time;
      linkBytes = 0;
      return rate;
    }

    /** process message on remote command */
    @Override
    public int processMessage (Message msg) {
      return (mh!=null)? mh.processMessage(msg) : Commandable.NOOP;
    }

    /** set slaved remote command
     *  @param value slaved remote command */
    public void setNext (String value) {
      if (M != null)  next = (Remote)M.registry.get(value);
      if (next==this) next=null; // don't allow recursion
    }

    /** brief description of command */
    @Override
    public String toString() {
      if (host==null) host = getHost();
      String tmp = "R: RMIF-Remote "+host+":"+port+" "+id+" ("+functionName(state)+") isClient="+isClient;
      if (next!=null) tmp += " ->> "+next.id;
      return tmp;
    }

    /** brief description of command
     *  @return brief description including id, host and port */
    public String getTag() {
      return id+"-Remote-"+host+"-"+port;
    }
  } // end class Remote

  /** Packet to serve to clients (for sending or receiving). */
  public class Packet {
    /* package private variables that should only be accessed within Rmif.java */
    /** The rmif packet */
    byte[] buffer;
    /** Packet start offset */
    int off;
    /** Packet length (RMIF header + Data) */
    int len;
    /** Send/receive time for packet (0=NotUsed, aka dirty packet) */
    double time;

    /** Creates a new uninitialized Packet.
        @since NeXtMidas 2.5.1
     */
    Packet() { // package-private only used in Rmif.java
      // uses default values for buffer=null, off=0, len=0, time=0
    }

    /** Creates a new Packet object with the time set to zero.
        This makes a **COPY** of the buffer.
     *  @param buf The packet buffer
     *  @param off The offset into the buffer for the start of the packet.
     *  @param len The total length of the RMIF packet in bytes (RMIF header + Data).
     */
    public Packet (byte[] buf, int off, int len) {
      // this(buf, off, len, 0, true);
      initialize(buf, off, len, 0, true);
    }

    /** Creates a new Packet object with the time set to zero.
        @param buf  The packet buffer.
        @param off  The offset into the buffer for the start of the packet.
        @param len  The total length of the RMIF packet in bytes (RMIF header + Data).
        @param copy Should a copy of the buffer be made?
        deprecate this constructor?
     */
    public Packet (byte[] buf, int off, int len, boolean copy) {
      initialize(buf, off, len, 0, copy);
    }

    /** Creates a new Packet object.
        @param buf  The packet buffer.
        @param off  The offset into the buffer for the start of the packet.
        @param len  The total length of the RMIF packet in bytes (RMIF header + Data).
        @param copy Should a copy of the buffer be made?
        @since NeXtMidas 2.5.1
     */
    Packet (byte[] buf, int off, int len, double time, boolean copy) {  // package-private only used in Rmif.java
      initialize(buf, off, len, time, copy);
    }

    /** Copy factory to do a shallow copy of this Packet, i.e. it will use
        the same reference to the source packet's byte[] buffer.
        @since NeXtMidas 2.5.1
     */
     Packet copy () {
       return new Packet(buffer, off, len, time, false);
     }

    /** Clear this packet and mark it as dirty so that it can be re-used.
        @param time time to reset packet to (normally zero for sent RDP packets
                    and -time for received packets sent up to the macro.)
        @since NeXtMidas 2.5.1
      */
    synchronized void clear (double time) { // package-private only used in Rmif.java
      this.buffer = null;
      this.time   = time;
    }

    /** Initialize current Packet object.
        @param buf  The packet buffer.
        @param off  The offset into the buffer for the start of the packet.
        @param len  The total length of the RMIF packet in bytes (RMIF header + Data).
        @param copy Should a copy of the buffer be made?
        @since NeXtMidas 2.5.1
    */
    synchronized void initialize (byte[] buf, int off, int len, double time, boolean copy) {  // package-private only used in Rmif.java
      if (buf == null) {
        throw new MidasException("Rmif: Can not create Packet, invalid packet buffer, buf=null.");
      }
      if ((len < 8) || (buf.length < off+len) || (off < 0)) {
        throw new MidasException("Rmif: Can not create Packet, invalid buffer length/offset, "
                                +"buf.length="+buf.length+", off="+off+", len="+len+". "
                                +"Minimum length is 8.");
      }
      if (copy) {
        this.buffer = new byte[len];
        this.off    = 0;
        this.len    = len;
        this.time   = time;
        System.arraycopy(buf,off, buffer,0, len);
      } else {
        this.buffer = buf;
        this.off    = off;
        this.len    = len;
        this.time   = time;
      }
    }

    /** Get internal data in a buffer that starts at offset=0. Makes a copy if
        the buffer does not already start at 0.

        JC4/NTN: We would like to get rid of this once procPacket is modified to
        take in an offset.
        @return internal data in a buffer that starts at offset=0
        @since NeXtMidas 2.5.1
     */
    byte[] getBufZeroOffset() {  // package-private only used in Rmif.java
      if (off == 0) return buffer;  // Extremely common!
      byte[] buf = new byte[len];
      System.arraycopy(buffer, off, buf, 0, len);
      return buf;
    }

    // THESE ARE IN THE ORDER THEY APPEAR IN THE PACKET
    /** RMIF Packet Field: func (see {@link #getFunction()}).
     *  @return function */
    public byte getFunc() { return buffer[off+FUNC]; }
    /** RMIF Packet Field: flag
     *  @return flag */
    public byte getFlag() { return buffer[off+FLAG]; }
    /** RMIF Packet Field: info
     *  @return info */
    public byte getInfo() { return buffer[off+INFO]; }
    /** RMIF Packet Field: rep (data representation: IEEE, EEEI, etc).
     *  @return data representation */
    public byte getRep()  { return buffer[off+REP];  }
    /** RMIF Packet Field: seq (&gt;=0: RDP sequence number, &lt;0 Protocol ID).
     *  @return &gt;=0: RDP sequence number, &lt;0 Protocol ID */
    public byte getSeq()  { return buffer[off+SEQ];  }
    /** RMIF Packet Field: try (RDP packet try number, see {@link #getProtocol()})
     *  @return RDP packet try number */
    public byte getTry()  { return buffer[off+TRY];  }
    /** RMIF Packet Field: ptr (not used, always 0)
     *  @return 0 */
    public byte getRpt()  { return buffer[off+RPT];  }
    /** RMIF Packet Field: adj (size of adjunct header)
     *  @return size of adjunct header */
    public byte getAdj()  { return buffer[off+ADJ];  }

    // THESE ARE IN THE ORDER THEY APPEAR IN THE PACKET (ALL SETS ARE PACKAGE-PRIVATE)
    /** RMIF Packet Field: func (see {@link #getFunction()}).
     *  @param val function */
    void setFunc (byte val) { buffer[off+FUNC] = val; }
    /** RMIF Packet Field: flag
     *  @param val flag */
    void setFlag (byte val) { buffer[off+FLAG] = val; }
    /** RMIF Packet Field: info
     *  @param val info */
    void setInfo (byte val) { buffer[off+INFO] = val; }
    /** RMIF Packet Field: rep (data representation: IEEE, EEEI, etc). */
    void setRep (byte val)  { buffer[off+REP] = val;  }
    /** RMIF Packet Field: seq (&gt;=0: RDP sequence number, &lt;0 Protocol ID).
     *  @param val &gt;=0: RDP sequence number, &lt;0 Protocol ID */
    void setSeq (byte val)  { buffer[off+SEQ] = val;  }
    /** RMIF Packet Field: try (RDP packet try number, see {@link #getProtocol()})
     *  @param val RDP packet try number */
    void setTry (byte val)  { buffer[off+TRY] = val;  }
    /** RMIF Packet Field: ptr (not used, always 0)
     *  @param val ptr */
    void setRpt (byte val)  { buffer[off+RPT] = val;  }
    /** RMIF Packet Field: adj (size of adjunct header)
     *  @param val size of adjunct header */
    void setAdj (byte val)  { buffer[off+ADJ] = val;  }

    /** The name of the RMIF function (see {@link #getFunc()}).
     *  @return the name of the RMIF function */
    public String getFunction() { return functionName(getFunc()); }

    /** The name of the protocol used (see {@link #getSeq()}).
     *  @return the name of the protocol used */
    public String getProtocol() { return protocolNameForSeq(getSeq()); }

    /** Gets the packet data as a String.
     *  @return the packet data as a String */
    public String getData() {
      int doff=off+DATA+buffer[getAdj()];
      int dlen=len-doff;
      return Convert.unpackS(buffer,doff,dlen);
    }

    /** Gets a description of the RMIF packet useful for debugging. This just includes the header
        information of the RMIF packet.
        @return String representation of member data
     */
    @Override
    public String toString () {
      if (buffer == null) {
        return "RMIF-Packet: <NULL>";
      } else {
        return "RMIF-Packet: func="+getFunc()+" ("+getFunction()+") flag="+getFlag()+" info="+getInfo()
             + " rep="+(char)getRep()+" seq="+getSeq()+" ("+getProtocol()+") try="+getTry()+" rpt="
             + getRpt()+ " adj="+getAdj()+" time="+Time.toString(time);
      }
    }

    /** Short form of toString that only prints out 12 characters in data and
     *  time using just HMS. This is only used for testing
     *  @return Short form of toString that only prints out 12 characters
     */
    public String toStringShort () {
      if (buffer == null) {
        return "Packet: buf=null time="+Time.toString(time,Time.HMS);
      } else {
        int length = Math.min(8, len-8); // len-8 to account for skipping the header
        return "Packet: func="+getFunction()+" seq="+getSeq()+" try="+getTry()
          + " time="+Time.toString(time,Time.HMS) + " buf=" + Convert.unpackS(buffer,off+8,length);
      }
    }
  } // end class Packet

  /** property to serve to clients */
  public class Property {
    /** Rmif local Property name */
    public String name;
    /** Rmif remote Property name */
    public String rname;
    int type,members;
    BaseFile bf;
    Remote[] member = new Remote[MAXMEMBER];
    Object[] pms = new Object[MAXMEMBER];
    byte[] channel = new byte[MAXMEMBER];
    int errorCount = 0; // since 3.5.0 for BUG 2756

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

    /** Gets a description of the RMIF property useful for debugging.
     *  @return String representation of member data */
    @Override
    public String toString() {
      return "RMIF-Server Property: Name="+name+" Type="+propTypeName(type)+" Members="+members;
    }
  } // end class Property

  /** Pipe Property Structure */
  public class PipePropertyMember {
    byte dec,count;
    byte comp,flag;
    int plotwidth=512;
    int trim1,trim2;

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

    /** Gets a description of the pipe property member useful for debugging.
     *  @return String representation of member data */
    @Override
    public String toString() {
      return "PipePropertyMember: dec="+dec+" comp="+comp
            +" pw="+plotwidth+" trim1="+trim1+" trim2="+trim2;
    }
  } // end class PipePropertyMember

  /** File Property Structure */
  public class FilePropertyMember {
    int todo,done,pktlen,port;
    double offset,time,rate;
    InetAddress addr;
    byte[] dbuf;

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

    /** Gets a description of the file property member useful for debugging.
     *  @return String representation of member data */
    @Override
    public String toString() {
      return "FilePropertyMember: todo="+todo+" done="+done
            +" offset="+offset+" time="+time+" rate="+rate+" pktlen="+pktlen;
    }
  } // end class FilePropertyMember

  /** channel from client to served property on Remote */
  public class Channel {
    /** local  property name for this Channel */
    public String name;
    /** remote property name for this Channel */
    public String rname;
    /** RMIF Remote where this property is located for this Channel */
    public Remote r;
    /** Type of property served from Remote for this Channel. See {@link Rmif#typeList} for possible values.
        Actual value will be: {@link Rmif#PIPE}, {@link Rmif#RESULT}, {@link Rmif#KEY}, etc.
     */
    int type;
    /** flags for this channel. See {@link Rmif#MULTI}. */
    int flags;
    /** DataFile (when type is PIPE or FILE) for this Channel */
    DataFile df;
    /** connection state of this Channel */
    byte state= CLOSED;

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

    /** Gets a description of the file property member useful for debugging.
     *  @return String representation of member data */
    @Override
    public String toString() {
      return "RMIF-Client Channel: Name="+name
            +" Type="+propTypeName(type)+" State="+functionName(state);
    }

    //jnuanes - added equals
    @Override
    public boolean equals(Object obj)
    {
      boolean equal = false;
      if(obj instanceof Channel)
      {
        Channel c = (Channel)obj;
        equal = ((name == null && c.name == null) ||
            (name != null && name.equals(c.name)) ||
            (c.name != null && c.name.equals(name))) &&
            c.r == r &&
            ((rname == null && c.rname == null) ||
            (rname != null && rname.equals(c.rname)) ||
            (c.rname != null && c.rname.equals(rname))) &&
            type == c.type &&
            flags == c.flags;
      }
      return equal;
    }

    //jnuanes - added since we have a equals method
    @Override
    public int hashCode()
    {
      return super.hashCode();
    }
  } // end class Channel
}
