package nxm.sys.net;

import java.net.InetAddress;

import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;

/** Rmif with State Machine logic, so packets are handled in order.<br>
 *
 *  <pre>
 *  This extended version of Rmif has the following user contributed upgrades:
 *  1) State machine logic and extra checks to make sure packets are processed in order
 *  2) Faster remote storage by using a synchronized HashMap
 *  3) Remote naming based on address and port, for faster lookup.
 *  4) Remote reuse is not allowed 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.
 *  5) Various safety fixes on open and close
 *  6) Adjustments to logging to provide more critical details and remove some
 *     less relevant information
 *  </pre>
 *
 *  @author Jeremy Nuanes / Jeanette McCall / Jeff Schoen / J. Czechowski IV / Neon Ngo
 *  @since  State Machine logic broken out into child class in NeXtMidas 4.1.3
*/
public class RmifSM extends 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 RmifSM (MessageHandler handler) {
    this(null, null, handler);
  }

  /** Create an Rmif that keeps track of state.
      @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 RmifSM (Midas M, MessageHandler owner, MessageHandler handler) {
    super(M, owner, handler, true);
    logStateMachineInconsistencies();
  }

  /** Close remotes, close sockets, disconnect.
   *  This version does a full and not just a light close */
  public void close () {
    //jnuanes - Close the reader and the sock so we don't try to start a remote while we are exiting
    //THIS MUST BE DONE FIRST SO WE DON'T OPEN REMOTES WHILE WE ARE CLOSING
    if (packetReader!=null){try{packetReader.stop();}catch(Exception e){}}
    if (dsock!=null && !dsock.isClosed()){try{dsock.close();}catch(Exception e){}}
    closeRemote(wdef);
    double time = Time.current();
    //jnuanes - remove all remotes we are shutting down
    synchronized(_fastRemotes)
    {
      //Safe remove from a Hashtable while iterating
      Object[] keys = _fastRemotes.keySet().toArray();
      for (int i = 0; i != keys.length; ++i)
      {
        removeRemote(_fastRemotes.get(keys[i]));
      }
    }
    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);
      }
    }
    channels.clear(); // jnuanes - we are closing down get rid of pointers to channels
  }

  /** 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.<br>
      This user contributed version keeps track of state and flags when the communication
      is in a bad state.
   */
  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
      boolean sendQuietClose = false;  // true to send a quiet close and return

      String fastRemoteID = createFastLookupKey(addr, port);
      // find the Remote object of packet address
      Remote r = _fastRemotes.get(fastRemoteID);
      //jnuanes - If we are trying to OPEN create a Remote
      if(func == PING && info == OPEN)
      {
        //Close old remote so we start with a fresh state
        removeRemoteQuiet(r);
        String host = addr.getHostAddress();
        r = createClientRemote(fastRemoteID, host, port);
      }
      boolean badState = true;
      //jnuanes - these are all cases where we would let a message pass. if we are not in
      //a good state the message is rejected.
      //If the remote is a client
      if (r!=null)
      {
        synchronized(r)
        {
          //A client is trying to close us.  Don't care what state we are in.
          //I have seen a NM server spew ACK messages even though he is not OPENED
          if(func == RECEIPT ||
              func == CLOSE)
          {
            //If we are recieving a reciept for something we must have sent something out
            //Or a previous client sent something.  Lets not be rude and respond.  This should
            //not hurt anything.
            badState = false;
          }
          else if(r.isOpened())
          {
            badState = false;
          }
          //A client is trying to connect to our server
          else if(r.isClient())
          {
            if(r.isOpening() && func == OPEN)
            {
              badState = false;
            }
            //We need to get a ping message in order to open
            else if(func == PING && info == OPEN)
            {
              badState = false;
            }
            //Client is trying to open a connection to the server.
            else if((r.getState() == ADDR || r.getState() == CLOSED) && func == OPEN)
            {
              badState = false;
            }
            else if(r.isClosed() && func == CLOSE)
            {
              //Discard these messages without a warning
              //If we send out a whole bunch of ACK messages
              //And the client goes down he will send us a
              //quiet close for each ACK or RET
              return false;
            }
          }
          else
          {
            if(r.isOpening() && (func == PONG || func == OPENED))
            {
              badState = false;
            }
            else
            {
              sendQuietClose = true;
            }
          }
        }
      }
      else
      {
        //The remote is null and we are getting ACKs and RETs
        //So we know it is a server sending us stuff we don't want
        if(func == ACK || func == RET)
        {
          sendQuietClose = true;
        }
      }
      //The remote is in a bad state
      if(badState)
      {
        String message = null;
        if(sendQuietClose)
        {
          //Send a close message to the server 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(8);
          dsock.send(dpacki);
          message = "Sent CLOSE to unsolicited server for Packet";
        }
        else
        {
          message = "Rejecting unsolicited Packet";
        }
        info(message+" seq="+seq+" retry="+retry+" func="+func+"("+functionName(func)+
            ") info="+info+"("+functionName(info)+") from " +
            (r != null?r.toString():(addr.getHostAddress()+ ":" + port)));
        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 maxExpectedSeq  = (nextExpectedSeq+r.getWindow())&0x7f;
          int minExpectedSeq  = (nextExpectedSeq-r.getWindow())&0x7f;
          boolean outOfRange = seqOutOfRange(seq, nextExpectedSeq, maxExpectedSeq);

          int estimatedPacketsLost = seq - maxExpectedSeq;     // Number of packet that may be lost
//jnuanes - changed 128 to RDP_PACKET_QUEUE_SIZE I think this is what was wanted
          if (seq<maxExpectedSeq) estimatedPacketsLost += RDP_PACKET_QUEUE_SIZE; // Add RDP_PACKET_QUEUE_SIZE to account for wrap around to 0

          //jnuanes - reset the queues on OPEN and OPENED
          //Don't do the following if we are opening a channel
          if ((func==OPEN && info == 0) || (r.isOpening() && func == OPENED)) { // server function
            nextExpectedSeq = seq; // set this packet as next expected seq for processing
          }
          else if (pkt.time>0 && retry==0) {
            int overlapCount = seq - nextExpectedSeq;      // Number of overlapping seq (b/c of seq # wrap around)
//jnuanes - changed 128 to RDP_PACKET_QUEUE_SIZE I think this is what was wanted
            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());
              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
            }
          }
          else if(outOfRange) //jnuanes - handle things that are out of range
          {
            //You are out of the range and you are a retry send a reciept back if it
            //could have been in the range of packets we have processed IE if we are
            //on packet 1 and this is a retry of 127 the sender never received the
            //receipt message.  The reason we need to do this is because we don't
            //keep good books on what we have processed.
            boolean outOfProcessedRange = seqOutOfRange(seq, minExpectedSeq, nextExpectedSeq);
            if(outOfProcessedRange)
            {
              warning("Out of range seq="+seq+" nextExpectedSeq="+nextExpectedSeq+
                " minExpectedSeq="+minExpectedSeq+" maxExpectedSeq="+maxExpectedSeq+
                " retry="+retry+" func="+func+"("+functionName(func)+
                ") info="+info+"("+functionName(info)+") from " + r.toString());
            }
            else
            {
              sendReceipt(func, len);
              if ((verbose||logfile!=null)) {   // handle logging
                int slen = (func >= DHDR && func <= DBUFD)? 0: dlen;
                String text="Out of range seq="+seq+" nextExpectedSeq="+nextExpectedSeq+
                " minExpectedSeq="+minExpectedSeq+" maxExpectedSeq="+maxExpectedSeq+" 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); }
              }
            }
            return false;  //Reject all out of range sequences
          }
          sendReceipt(func, len);
          if ((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
//jnuanes - changed 128 to RDP_PACKET_QUEUE_SIZE I think this is what was wanted
              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;
  }

  /** Is remote closed and should not sent packet
   *  The Jeremy / STATEM check for sendPacket<br>
   *  Jeremy, having (r.state!=OPENED) rather than !r.isOpened()
   *  keeps the number of method calls the same
   *
   *  @param r     Remote
   *  @param func  function
   *  @return if remote is closed and should not send the packet
   */
  @Override
 boolean remoteClosedShouldNotSendPacket (Remote r, byte func) {
   return (r.state!=OPENED) && func!=OPEN && func!=OPENED && func!=PING && func!=PONG;
 }

  @Override
  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 (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);
          //jnuanes - check the state when we process
          if (!checkClientRemoteState(r, func)) break;
          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);
          //jnuanes - check the state when we process
          if (!checkServerRemoteState(r, func)) break;
          r.timePong = currentTime;
          if (r.isOpening()) openRemote(r);
          break;

        case OPEN:          // server function
          if (verbose) info("Got OPEN from "+r+" chan="+chanNum);
          //jnuanes - check the state when we process
          if (!checkClientRemoteState(r, func)) break;
          if (chanNum<=0) {     // open remote
            r.rep      = buf[REP];
            r.state    = OPENED;
            //jnuanes - We should not set this in here it should be done on the PING OPEN
//            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
          //jnuanes - check the state when we process
          if (!checkServerRemoteState(r, func)) break;
          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 (chanNum != -CLOSE && !checkClientRemoteState(r, func)) break;
          //jnuanes - Always clean the send and recieve queue
          if(chanNum <= 0)
          {
            //jnuanes - Close down the remote quietly we don't want a new remote
            //to get rogue CLOSED messages.
            removeRemoteQuiet(r);
          }
          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
          //jnuanes - check the state when we process
          if (!checkServerRemoteState(r, func)) break;
          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
    }
  }

  @Override
  public Remote addRemote (Table targs) {
    if (debug) debug("addRemote targs="+targs);

    Remote r = new Remote(this);
    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); }
      _fastRemotes.put(createFastLookupKey(r.addr, r.port), r);
    } catch (Exception e) {
      info("Problem="+e.getMessage()+"   with addRemote="+targs);
      r = null;
    }
    return r;
  }

  @Override
  Remote indexRemote (int port, InetAddress addr) {
    //jnuanes - changed to lookup remotes faster
    return _fastRemotes.get(createFastLookupKey(addr, port));
  }

  @Override
  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
      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");
  }

  //jnuanes - pass the time we are using to the unOpenRemote
  @Override
  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
    //jnuanes - Always log that a remote has timed out and remove it quietly
    info("Closing timed out client " + r + " time: " + time + " r.timePong " + r.timePong + " deltatime: " + (time-r.timePong) + " timeOut: " + timeOut);
    removeRemoteQuiet(r);
  }

  @Override
  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
    _fastRemotes.remove(createFastLookupKey(r.addr, r.port));
    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);
  }

  @Override
  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() && !r.isClient()) sendPacket(CLOSE, 0, r, RDP);
    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.
    r.state = CLOSED;
  }

  //////////////// RmifSM only methods /////////////////////////////

  /** Check for sequence being out of range
   *
   *  @param seq             sequence number received
   *  @param minExpectedSeq  minimum expected sequence number
   *  @param maxExpectedSeq  maximum expected sequence num
   *  @return  true if out of range
   *  @since 4.1.0 jnuanes - contributed along with state machine logic
   */
  boolean seqOutOfRange(final byte seq, final int minExpectedSeq, final int maxExpectedSeq) {
    boolean outOfProcessedRange = false;
    if (minExpectedSeq < maxExpectedSeq) {
      outOfProcessedRange = (seq<minExpectedSeq) || (maxExpectedSeq<seq);
    }
    else { // nextExpectedSeq <= minExpectedSeq
      outOfProcessedRange = (maxExpectedSeq<seq) && (seq<minExpectedSeq);
    }
    return outOfProcessedRange;
  }

  /** Added so we can check to see if we are the correct Remote. This method
   *  should be expanded on to check the actual state of the remote and make
   *  sure we can process the message.
   *
   *  @param r    remote
   *  @param func function
   *  @return true if okay
   *  @since 4.1.0 jnuanes contributed for state machine logic
   */
  boolean checkClientRemoteState(Remote r, byte func) {
    if (r==null) { error("Received func="+func+"("+functionName(func)+") from null Remote server"); return false; }
    if (!r.isClient()) { error("Recieved func="+func+"("+functionName(func)+") from a server. "+r); return false; }
    return true;
  }

  /** Added so we can check to see if we are the correct Remote. This method
   *  should be expanded on to check the actual state of the remote and make
   *  sure we can process the message.
   *
   *  @param r    remote
   *  @param func function
   *  @return true if okay
   *  @since 4.1.0 jnuanes contributed for state machine logic
   */
  boolean checkServerRemoteState(Remote r, byte func) {
    if (r==null) { error("Received func="+func+"("+functionName(func)+") from null Remote client"); return false; }
    if (r.isClient()) { error("Recieved func="+func+"("+functionName(func)+") from a client. "+r); return false; }
    return true;
  }

  /** Removes the remote from the registry and closes all chanels without sending
   *  information to the client we don't want the client to close his remote we
   *  just want to close/remove our side of things.
   *
   *  @param r remote
   *  @since 4.1.0 jnuanes contributed to support state machine logic
   */
  void removeRemoteQuiet (Remote r) {
    if (r==null) return;
    // jnuanes - By setting the state to CLOSED the closeRemote will perform all
    // of its clean up without sending a packet accross the wire.
    synchronized (r)
    {
      // Always remove the remote from the list first so no one tries to use it
      // while we are cleaning up
      _fastRemotes.remove(createFastLookupKey(r.addr, r.port));
      if ((r.id!=null) && (M!=null)) M.registry.remove(r.id);
      closeClientChannels(r);
      for (int ip=1; ip<=nproperty; ip++) delMember (r,-1,ip);
      resetRecvQueue(r, -1);
      resetSendQueue(r);
      r.state = CLOSED;
    }
  }

  /** Closes all channels the server has for the given remote
   *  @param r remote
   *  @since 4.1.0 jnuanes contributed to support state machine logic
   */
  void closeClientChannels (Remote r) {
    for (int ic=1; ic<=nchannel; ic++) {
      Channel c = getChannel(ic);
      if (c==null) continue;
      else if (c.r!=r);
      else if (c.state==CLOSED);
      else {
        c.state = CLOSED;
        c.r = null;
      }
    }
  }

  private void logStateMachineInconsistencies() {
    StringBuilder logOfBadState = new StringBuilder();
    if (allOriginalMessages != false) logOfBadState.append("Excessive Logging ON : ");
    if (supplimentalLogging != true) logOfBadState.append("Supplemental Logging OFF : ");
    if (logOfBadState.length() == 0) return;

    // LOG Rmif/STATEM with bad settings
    String badStateMessage = "RMIF/STATM settings are not those recommended for operational usage : " +
       logOfBadState.toString() + " Please consider adjusting the settings mentioned.";
    info(badStateMessage);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // 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 extends Rmif.PacketReader {

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

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

  } // end class PacketReader
}
