package nxm.sys.prim;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Convert;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Message;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.net.HDisplay;
import nxm.sys.net.HMessage;
import nxm.sys.net.HQuery;
import nxm.sys.net.HServer;
import nxm.sys.net.Rmif;
import nxm.sys.net.Rmif.Remote;
import nxm.sys.net.Rmif.RemoteAddress;
import nxm.sys.net.RmifSM;

/**
  Remote Midas InterFace - handles remote processors using an RMIish protocol.

  @author Jeff Schoen
  @see nxm.sys.net.Rmif
*/
public class rmif extends Primitive implements Chainable {

  protected Rmif rmif;
  protected HServer http;
  protected TextFile logfile;
  private boolean stateBasedMsgHandlingIsDefault; // new in 4.1.0 for NXM-3781
  private boolean killForTestingOnly = false; // This is meant to be set for TESTING ONLY to simulate
                                              // an RMIF instance that has been killed abruptly

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

  @Override
  public int open () {
    String tmp,item;

    // get server port to bind on
    int port = MA.getL("PORT");
    int prange = MA.getL("/PRANGE",1);
    boolean reOpen = MA.getState("/REOPEN",true);

    // since 3.1.2: get host to bind on (e.g. localhost, 10.10.10.88)
    String host = MA.getS("/HOST", ""); // blank will default to wildcard IP address (0.0.0.0)
    if (verbose && !"".equals(host)) M.info("HOST SWITCH: " + host);

    // since 3.1.2: get device to bind on (e.g. eth0, eth1, eth1.101). takes precedence over /HOST=
    String dev = MA.getS("/DEVICE", "");
    if (!"".equals(dev)) {
      if (verbose) M.info("DEVICE SWITCH: " + dev);
      try {
        NetworkInterface nintf = NetworkInterface.getByName(dev);
        if (nintf != null) {
          Enumeration<InetAddress> e = nintf.getInetAddresses();
          while (e.hasMoreElements()) {
            InetAddress ia = e.nextElement();
            if (ia instanceof Inet4Address) {
              host = ia.getHostAddress();
              break;
            }
          }
        } else {
          M.warning("Network device [" + dev + "] could not be resolved.");
        }
      } catch (SocketException se) {
        M.printStackTrace("Unable to get network device to bind on for /DEVICE="+dev, se);
      }
    }

    stateBasedMsgHandlingIsDefault = MA.getState("/STATEM", false);

    // open Rmif port
    openRmifConnection(getMessageHandler(),host, port,prange);

    // check for log file
    if (MA.find("/LOG")) openLogFile(MA.getTextFile("/LOG"));

    // get port parameters
    initRmifConnection(reOpen, MA.getTable("/PPT"));

    // create properties array
    rmif.addProperty ("Q:SET");
    rmif.addProperty ("Q:GET");
    rmif.addProperty ("Q:RET");
    rmif.addProperty ("Q:ACK");
    tmp = MA.getS("PROPS");
    while (tmp.length() > 0) {
      int i=tmp.indexOf('|'); if (i<0) { item=tmp; tmp=""; }
      else { item=tmp.substring(0,i); tmp=tmp.substring(i+1); }
      rmif.addProperty (item);
    }

    // loop through command line list of remotes
    for (int ir=1; ir*2<MA.nargs; ir++) {
      tmp = MA.getS(ir*2+1);
      Remote r = rmif.openRemote (Convert.o2t(tmp));
      tmp = MA.getS(ir*2+2);
      while (tmp.length() > 0) {
        int i=tmp.indexOf('|'); if (i<0) { item=tmp; tmp=""; }
        else { item=tmp.substring(0,i); tmp=tmp.substring(i+1); }
        rmif.addChannel (r,-1,item);
      }
    }

    // start Http Server
    if (MA.getState("/HTTP")) {
      openHttpServer("RMIF Web Server", MA.getCS("/HOMEPAGE"), MA.getS("/AUXLIST"));
    }

    return NORMAL;
  }

  @Override
  public int process () {
    int stat = 0;

    // check the remotes
    stat += rmif.checkRemotes();

    // serve the properties
    stat += rmif.serveProperties();

    if (stat==0) return NOOP;  // pause
    else return NORMAL;
  }

  @Override
  public int close () {
    if (http!=null) http.stopThread();
    if (rmif!=null ) {
      if (killForTestingOnly) {
        rmif.hardKillForTestingOnly(); // for TESTING only
      }
      else rmif.close();
    }
    if (logfile!=null) {
      logfile.writeln("RMIF log stopped at "+Time.tag());
      logfile.close();
    }
    return NORMAL;
  }

  /** Opens the RMIF connection. This is only called from open(), it is separate from open() to
      allow subclasses to override.
      @param handler The message handler
      @param port    The port to use (-1 for a local port).
      @param prange  The port range to use.
   */
  protected void openRmifConnection (MessageHandler handler, int port, int prange) {
    this.openRmifConnection(handler, "0.0.0.0", port, prange);
  }

  /** Opens the RMIF connection. This is only called from open(), it is separate from open() to
      allow subclasses to override.
      @param handler The message handler
      @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    The port to use (-1 for a local port).
      @param prange  The port range to use.
      @since NeXtMidas 3.1.2
   */
  protected void openRmifConnection (MessageHandler handler, String host, int port, int prange) {
    rmif = (stateBasedMsgHandlingIsDefault) ? new RmifSM(M,this,handler) : new Rmif(M,this,handler);
    rmif.open(host,port,prange);
  }

  /** TEMPORARY - Switch which return statement is commented out if you
   *  have added in nxm.sys.net.RmifJeremy2019 and expect this to work!
   *  @return a very pure RmifJeremy2019 with all methods overriden
   */
  private Rmif useJeremy2019Version() {
    M.info("TEMPORARY OPTION IS ENABLED: Using pure Jeremy 2019 Rmif code");
//  return new nxm.sys.net.RmifJeremy2019(M,this,handler);
    return null;
  }

  /** Opens the log file. This is only called from open(), it is separate from open() to
      allow subclasses to override.
      @param log The log file to use.
   */
  protected void openLogFile (TextFile log) {
    logfile = log;
    // Deprecated since 2.5.0... need to use NOABORT with TextFile.open(), see TextFile.open(int)
    if (logfile.open(TextFile.OUTPUT|TextFile.NOABORT|TextFile.FLUSH)) { // since 3.3.0 auto flush to disk
      logfile.writeln("RMIF log started at "+ Time.tag());
      rmif.setLogFile(logfile);
    } else {
      M.warning("Could not open log at URL="+logfile.getURL());
    }
  }

  /** Initialize the RMIF connection. This is only called from open(), it is separate from open() to
      allow subclasses to override.
      @param reOpen Reopen same named pipes on client side when reconnecting.
      @param ppt    The Port Parameter Table for tuning connections.
   */
  protected void initRmifConnection (boolean reOpen, Table ppt) {
    rmif.setVerbose(verbose);
    rmif.setReOpen(reOpen);
    if (ppt!=null) KeyObject.setKeys(rmif,ppt);
  }

  /** Open a HTTP server. This is only called from open(), it is separate from open() to
      allow subclasses to override.
      @param title    The title of the server (to be displayed to users).
      @param homepage The homepage to show.
      @param auxlist  The AUX list to show.
   */
  protected void openHttpServer (String title, String homepage, String auxlist) {
    if (auxlist == null) auxlist = "";
    http = HServer.launch(this, title, rmif.getPort(), homepage, auxlist);
    http.addSource( new HDisplay("Display",M) ); // added in 4.1.0 for NXM-3815
    http.addSource( new HMessage("Message",M) );
  }
  /** Get partners
   *  @return partners */
  public int getPartners() {
    return MA.getL("/PARTNERS",0);
  }
  /** Get server
   *  @return server */
  public HServer getServer() {
    return http;
  }

  @Override
  public synchronized int processMessage (Message msg) {
    if (!thisIsMe()) { MQ.put(msg); return 0; }

    Remote r     = null;
    int flags    = Rmif.RDP; // default send packet flags is to use RDP protocol
    Object quals = msg.getQuals();

    if (quals instanceof Table) {
      // BUG 1991 to allow user to specify desired protocol when sending packets
      // to remote. Currently only UDP or RDP (default) are supported.
      String protocol = ((Table)quals).getS("PROTOCOL");
      flags = Parser.find(Rmif.protocolList, protocol, Rmif.RDP, -1); // -1 for zero-based
      // if (verbose) M.info("using protocol="+protocol+" flags="+flags); // DEBUG
    }
    if (msg.to instanceof Remote)
      r=(Remote)msg.to;
    else if (msg.to instanceof RemoteAddress)
      r=rmif.getRemoteForAddress((RemoteAddress)msg.to);

    if (verbose) M.info("Processing msg: "+msg.name);

    if (msg.name.startsWith("ADDR")) {
      r = rmif.addRemote (Convert.o2t(msg.data));
    }
    else if (msg.name.equals("OPEN")) {
      r = rmif.openRemote (Convert.o2t(msg.data));
      if (http!=null) http.addSource( new HQuery(r.getTag(),r,null,64) );
    }
    else if (msg.name.startsWith("ADDC")) {
      int cflags = 0; // channel flags
      if (msg.name.indexOf("/M")>0) cflags = Rmif.MULTI;
      rmif.addChannel (r, msg.info, msg.data.toString(), cflags);
    }
    else if (msg.name.startsWith("DELC")) {
      if (msg.info>0) rmif.closeChannel (msg.info);
      else rmif.closeChannel (msg.data.toString());
    }
    else if (msg.name.equals("RADDR")) {
      if (msg.data!=null) r = rmif.getRemote(Convert.o2t(msg.data));
      rmif.removeRemote(r);
      if (http!=null) http.removeSource (r.getTag());
    }
    else if (msg.name.startsWith("CLOSE")) {
      rmif.closeRemote(r);
      if (msg.name.indexOf("/R")>0) rmif.removeRemote(r);
      if (http!=null) http.removeSource( r.getTag() );
    }
    else if (msg.name.equals("SET")) {
      for (; r!=null; r=r.next)
      rmif.sendPacket (Rmif.SET,msg.info,Convert.o2ba(msg.data),r, flags);
    }
    else if (msg.name.equals("GET")) {
      for (; r!=null; r=r.next)
      rmif.sendPacket (Rmif.GET,msg.info,Convert.o2ba(msg.data),r, flags);
    }
    else if (msg.name.equals("RET")) {
      if (r==null) rmif.servePacket(Rmif.RET,msg.info,Convert.o2ba(msg.data), flags);
      else rmif.sendPacket (Rmif.RET,msg.info,Convert.o2ba(msg.data),r, flags);
    }
    else if (msg.name.equals("ACK")) {
      if (r==null) rmif.servePacket(Rmif.ACK,msg.info,Convert.o2ba(msg.data), flags);
      else rmif.sendPacket (Rmif.ACK,msg.info,Convert.o2ba(msg.data),r, flags);
    }
    else if (msg.name.equals("KEYS") || msg.name.equals("PROPS") ) {
      rmif.sendPacket (Rmif.GETPROP,msg.info,r, flags);
    }
    else if (msg.name.equals("PING")) {
      rmif.sendPacket (Rmif.PING,0,r,flags);
    }
    else if (msg.name.equals("MODIFY")) {
      for (; r!=null; r=r.next)
      rmif.sendPacket (Rmif.MODIFY,msg.info,Convert.o2ba(msg.data),r, flags);
    }
    else if (msg.name.equals("TESTLINK")) {
      rmif.testLink (r,msg.info);
    }
    else if (msg.name.equals("EXIT")) {
      return FINISH;
    }
    else if (msg.name.equals("KILL_FOR_TESTING_ONLY")) {
      // This message should only be used for testing!  Not meant as part of the API!
      M.warning("RMIF: '"+getID()+"' killed by "+msg.from);
      killForTestingOnly = true;
      return FINISH;
     }
    return NORMAL;
  }

  /** Get remotes
   *  @return the KeyVector of remotes */
  public KeyVector getRemotes() { return rmif.getRemotes(); }

  /** Get properties
   *  @return the KeyVector of properties */
  public KeyVector getProps() { return rmif.getProperties(); }

  /** Get channels
   *  @return the KeyVector of channels */
  public KeyVector getChans() { return rmif.getChannels(); }

  /** Get next link
   *  @return next link */
  public Object getNextLink() { return rmif; }
  /** Get previous link
   *  @return previous link */
  public Object getPrevLink() { return null; }
  /** Get Rmif
   *  @return rmif */
  public Rmif getRmif() { return rmif; }

  /** @since NeXtMidas 3.3.0 */
  @Override
  public synchronized void setMessageHandler (MessageHandler mh) {
    super.setMessageHandler(mh);
    if (rmif != null) rmif.setMessageHandler(mh);
  }
}
