package nxm.xbc.lib;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;

import nxm.sys.inc.Constants;
import nxm.sys.inc.Keyable;
import nxm.sys.lib.Command;
import nxm.sys.lib.Convert;
import nxm.sys.lib.FileUtil;
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.Time;
import nxm.sys.libxm.XmMsgHandler;
import nxm.sys.libxm.XmMsgHandler.XmMessage;

/** Base class for X-Midas Host support commands (coded in Fortran). <br>
    <br>
    Host commands are primitives that were written for X-Midas that are being re-hosted under the
    NeXtMidas shell. These commands should run as coded with a few exceptions. See the "XBC" section
    in the "NeXtMidas User's Guide" for more details. <br>
    <br>
    This base class is used to load and execute the Host code. To build the Fortran code, use the
    standard <code>BLD</code> command.

    @author  Jeff Schoen / J. Czechowski IV
*/
public class HPrimitive extends Command implements Constants,Keyable {
  static boolean useNative = false;

  /** List of states for the host primitive.
      <pre>
        [Run Primitive]       hostState = HOST_START    --+
              |                                           |
              v                                           |
          M$CHECKIN           hostState = HOST_CHECKIN    | state = OPEN
              |               hostState = HOST_OPEN       |
              v                                           |
           M$SYNC             hostState = HOST_SYNC     --+
              |               hostState = HOST_PROCESS    | state = PROCESS
              v                                           |
          M$RETURN            hostState = HOST_RETURN   --+
              |               hostState = HOST_CLOSE      | state = CLOSE
              v                                           |
        [Primitive Done]      hostState = HOST_DONE     --+
      </pre>
      M$CHECKIN is MANDATORY (This is where Mc and Mu structures are mapped) !
      Primitive will stay in OPEN until it sees, HOST_SYNC (call to M$SYNC) or
      a transition to any state below HOST_PROCESS (skip M$SYNC call).  We will hold
      the host thread in M$SYNC (if called) until the NeXtMidas primitive transition
      to process.  Similarly, for M$RETURN we will hold the thread until NeXtMidas transitions
      to CLOSE.  If M$SYNC and/or M$RETURN are missing, we transition to HOST_DONE when the
      native thread (the FORTRAN) completes.
   */
  static final String hostStateList = "Start,CheckIn,Open,Sync,Process,Return,Close,Done";

  static final int HOST_START   =  1;  // Host primitive state: primitive started
  static final int HOST_CHECKIN =  2;  // Host primitive state: in     M$CHECKIN (set from native)
  static final int HOST_OPEN    =  3;  // Host primitive state: after  M$CHECKIN
  static final int HOST_SYNC    =  4;  // Host primitive state: in     M$SYNC    (set from native)
  static final int HOST_PROCESS =  5;  // Host primitive state: after  M$SYNC
  static final int HOST_RETURN  =  6;  // Host primitive state: in     M$RETURN  (set from native)
  static final int HOST_CLOSE   =  7;  // Host primitive state: after  M$RETURN
  static final int HOST_DONE    =  8;  // Host primitive state: primitive done

  static final int MAX_STRINGS  = 16;  // Max number of

  static int debug = 0;
  /** Set the Midas debug flag
   *  @param val The new Midas debug flag
   */
  public static void setDebug (int val) { debug = val;  }
  /** Get the Midas debug flag
   *  @return The current Midas debug flag
   */
  public static int getDebug  ()        { return debug; }

  /** path to library */
  public String libpath;
  /** subroutine */
  public String subroutine;
          long          handle;         // Used by native code
          long          context;        // Used by native code
  private LinkedHashMap<String,Long> strings = new LinkedHashMap<>(); // The string cache.
  private XmMsgHandler  xmMsgHandler;   // Conversion between NM and XM message formats.
          int           hostState;      // State of host primitive (see hostStateList)
  private NmJackets     nmJackets;
  private File          tempFile;       // The temp file used for loading a copy of the host primitive (Bug 959)

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

  private static boolean initLib(HPrimitive prim) {
    libHostInit(new NmJackets(null)); // Throws UnsatisfiedLinkError if lib not loaded
    return true;
  }

  private static native void libHostInit (Object xm);

  /** Initialize the command context. This has nothing to do with the X-Midas m$checkin. */
  public int checkin (Midas M) {
    super.checkin(M);
    if (!useNative) {
      loadLibrary("xbc","lib","libHPrimitive");
      loadLibrary("xbc","lib","HPrimitive");
      useNative = initLib(this);
    }
    String root  = name.toLowerCase() + Shell.getLibrarySuffix();
    String ext   = Shell.getLibraryExt();
    String extNoDot = ext.substring(1); // strip dot from extension
    String fname = M.io.getName(option, "host", root, extNoDot);

    try {
      tempFile = File.createTempFile(root+"_", ext);
      tempFile.deleteOnExit();

      FileUtil.copyBaseFile(M, fname, tempFile);
      fname = tempFile.toString();
    } catch (IOException e) {
      throw new MidasException("Unable to load "+root+ext, e);
    }

    nmJackets    = new NmJackets(this);
    libpath      = fname;
    // ORIG 2.7.1 subroutine   = "mh$"+name.toLowerCase();
    subroutine   = "mainroutine_"; // NTN: to support C++ primitives
    state        = init();
    xmMsgHandler = XmMsgHandler.getHandlerFor(this.option);
    return state;
  }

  public void checkout () {
    free();
    File file = tempFile; // temp reference to address random NPE (Bug 2000)
    if (file != null) {
      if (!file.delete() && file.exists()) { // remove temp copy of host primitive's shared library
        M.warning("Unable to delete temp copy of host primitive's shared library: "+file);
      }
      file = tempFile = null;
    }
    super.checkout();     // <-- call this last (Bug 2664)
  }

  /** Loads a native library, throws MidasException on error. */
  private void loadLibrary (String opt, String area, String name) {
    try {
      String path = Shell.getOptPath(opt,area);
      if (!Shell.loadLibraryUnsafe(path,name)) {
        throw new MidasException("Unable to initialize XBC, use of native code is disabled.");
      }
    } catch(SecurityException e) {
      throw new MidasException("Unable to initialize XBC due to security exception.", e);
    } catch(UnsatisfiedLinkError e) {
      throw new MidasException("Unable to initialize XBC due to linking exception. It is likely "
                             + "that there is a method missing in XBC or that the shell was not "
                             + "properly exited after running 'make all xbc'.", e);
    }
  }

  // Starts the X-Midas primitive in it's own thread and waits for m$sync.
  // If it goes straight to m$return, we finish.
  public int open() {
    String threadName   = "Native Code for "+name.toUpperCase();
    Thread nativeThread = new MidasThread(M, new RunNativeCode(), threadName);

    setHostState(HOST_START);
    nativeThread.start();                                   // Start the native code.
    while (hostState < HOST_SYNC) { Time.sleep(M.pause); }  // Wait for M$SYNC to be called.
    return (hostState > HOST_PROCESS)? FINISH : NORMAL;     // If host skips processing, return FINISH
  }

  public int process() {
    // We must sleep here to wait for the X-Midas primitive while it is processing. Returning
    // SLEEP tells Command to sleep for 0.1 sec before calling this method again.
    return (hostState > HOST_PROCESS)? FINISH : SLEEP;
  }

  public int close() {
    setInterrupt(1);                                       // Sets Mc.break in native code.
    while (hostState < HOST_DONE) { Time.sleep(M.pause); } // Wait for native code to finish.
    nmJackets.closeNmJackets();
    return NORMAL;
  }

  /** Gets the current state of the host primitive.
      @return the current state of the host primitive.
   */
  public String getHostState () {
    return Parser.get(hostStateList, hostState);
  }

  /** Sets the host state. DO NOT MAKE THIS PUBLIC!!! */
  private void setHostState (int newState) {
    if (newState <= hostState) {
      M.warning("HPrimitive: Unexpected state transition from "+getHostState()+" to "
               +Parser.get(hostStateList, newState));
    }
    hostState = newState;
//System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>> name = "+name+"  setHostState("+getHostState()+")");
  }

  /** Only used by native code. */
  void setHostStateJNI (int newState) {
    setHostState(newState);

    if (hostState == HOST_CHECKIN) {
      while (state <= INIT) Time.sleep(M.pause);
      setHostState(HOST_OPEN);
    }

    if (hostState == HOST_SYNC) {
      while (state <= OPEN) Time.sleep(M.pause);
      setHostState(HOST_PROCESS);
    }

    if (hostState == HOST_RETURN) {
      while (state <= PROCESS) Time.sleep(M.pause);
      setHostState(HOST_CLOSE);
    }
  }

  public final int processMessages(int maxmsg) {
    // JC4: Need to disable automatic message processing by the run() method since X-Midas
    //      primitives expect to manually poll the message queue each time they are ready
    //      to process a message.
    return NOOP;
  }


  /** Searches for a string in the cache. This method takes in the characters for a string from the
      native code (up to 16 characters long) with 0 values for all extra fields.
      @return The pointer to the cached string or 0 if a new one needs to be allocated.
   */
  long getStringJNI(byte c0, byte c1, byte c2, byte c3, byte c4, byte c5, byte c6, byte c7,
                    byte c8, byte c9, byte cA, byte cB, byte cC, byte cD, byte cE, byte cF) {
    long pointer = 0L;

    try {
      StringBuffer str = new StringBuffer();

      if (c0 != 0) { str.append((char)c0); }      if (c1 != 0) { str.append((char)c1); }
      if (c2 != 0) { str.append((char)c2); }      if (c3 != 0) { str.append((char)c3); }
      if (c4 != 0) { str.append((char)c4); }      if (c5 != 0) { str.append((char)c5); }
      if (c6 != 0) { str.append((char)c6); }      if (c7 != 0) { str.append((char)c7); }
      if (c8 != 0) { str.append((char)c8); }      if (c9 != 0) { str.append((char)c9); }
      if (cA != 0) { str.append((char)cA); }      if (cB != 0) { str.append((char)cB); }
      if (cD != 0) { str.append((char)cC); }      if (cD != 0) { str.append((char)cD); }
      if (cE != 0) { str.append((char)cE); }      if (cF != 0) { str.append((char)cF); }

      Long val = strings.remove(str.toString());

      //System.out.println("getStringJNI("+str+") = "+val);

      if (val != null) {
        pointer = val.longValue();
      }

    } catch(Throwable t) {
      M.printStackTrace("HPrimitive: Error in getStringJNI", t);
      pointer = 0L;
    }

    return pointer;
  }


  /** Puts a string in the cache. If passed a non-string the pointer will be returned unmodified.
      If passed a string and the cache is not full the string will be added to the cache and 0 will
      be returned. If the cache is full the string will be added to the cache and a pointer to the
      oldest element in the cache (which is now being dropped from the cache) will be returned.
      @param obj     The object.
      @param pointer The C pointer to the object.
      @return The pointer to the object to delete.
   */
  long putStringJNI(Object obj, long pointer) {
    //System.out.println("putStringJNI('"+obj+"', "+pointer+")");
    try {
      if ((obj instanceof String) && (((String)obj).length() <= 16)) {
        String str = (String) obj;
        strings.put(str, Long.valueOf(pointer));

        if (strings.size() > MAX_STRINGS) {
          Iterator<Long> iter = strings.values().iterator();

          pointer = iter.next().longValue();
          iter.remove();
        } else {
          pointer = 0L;
        }
      }
    } catch(Throwable t) {
      M.printStackTrace("HPrimitive: Error in putStringJNI", t);
      pointer = 0L;
    }

    return pointer;
  }

  /** Called from JNI code to print out a stack trace. */
  void printStackTraceJNI (Throwable t) {
    M.printStackTrace("HPrimitive: Error detected while running native code for "+name, t);
  }

  /** init() loads HOST Primitive shared object (library) and subroutine to be invoke in exec() */
  private native int init();
  /** free() unloads HOST Primitive shared object (library) */
  private native int free();
  /** exec() is executed inside the Primitive's thread */
  private native int exec(NmJackets nmjackets);

  private native int findControl (String key);
  private native int getControlType (int cid);
  private native int getControlName (int cid, byte[] array);
  private native int getControlParameters (int cid, byte[] array);
  private native double getControl (int cid);
  private native void setControl (int cid, double value);

  public boolean isPipable() {
    return true;
  }
  private String[] keys = null;
  public String[] getKeys() {
    if (keys!=null) return keys;
    byte[] array = new byte[24];
    int nc = getControlName(-1,array);
    keys = new String[nc];
    for (int n=0; n<nc; n++) {
      int ln = getControlName(n,array);
      keys[n] = new String(array,0,ln);
    }
    return keys;
  }

  public Object getKey (String key) {
    int cid=0;
    if (key.equals("ID")) return id;
    else if (key.endsWith("LIST")) {
      key = key.substring(0,key.length()-4);
      cid = findControl(key);
      if (cid<0) return null;
      int type = getControlType(cid);
      if (type!=1) return null;
      byte[] bitems = new byte[256];
      int litems = getControlParameters(cid,bitems);
      return new String(bitems,0,litems);
    }
    else {
      if (key.startsWith("W_")) {
        cid = key.charAt(2)-'1';
        if (key.length()==4) cid = cid*10 + key.charAt(3)-'1';
      }
      else cid = findControl(key);
      if (cid<0) return null;
      int type = getControlType(cid);
      Object value = null;
      if (type==1) {
        byte[] bitems = new byte[256];
        int nitem = (int)getControl(cid);
        int litems = getControlParameters(cid,bitems);
        String items = new String(bitems,0,litems);
        value = Parser.get(items,nitem);
      }
      // Updated in 3.9.0 to use valueOf instead of new instance
      else if (type==2) value = Integer.valueOf((int)getControl(cid));
      else if (type==3) value = Float.valueOf((float)getControl(cid));
      else if (type==4) value = Double.valueOf(getControl(cid));
      return value;
    }
  }

  public Object setKey (String key, Object value) {
    int cid=0;
    if (key.equals("ID")) return id;
    else {
      if (key.startsWith("W_")) {
        cid = key.charAt(2)-'1';
        if (key.length()==4) cid = cid*10 + key.charAt(3)-'1';
      }
      else cid = findControl(key);
      if (cid<0) return null;
      int type = getControlType(cid);
      if (type==1) {
        byte[] bitems = new byte[256];
        int litems = getControlParameters(cid,bitems);
        String items = new String(bitems,0,litems);
        String item = value.toString();
        int nitem = Parser.find(items,item,0);
        setControl(cid,(double)nitem);
      }
      else setControl(cid,Convert.o2d(value));
      return value;
    }
  }

  /** Sets the interrupt (break) indicator which maps to <code>MC.BREAK</code> in FORTRAN.
      @param value The new value for the interrupt indicator where zero indicates
                   that there is no interrupt.
   */
  native void setInterrupt (int value);

  /** Gets the stack trace from the native code. */
  static native String[] getStackTrace (long st);

  /** Gets an X-Midas message from the queue. This should only be used from the native code.
      @param headerPointer Native pointer to the header.
      @param dataPointer   Native pointer to the data.
      @param maxdata       Maximum length of the data to copy.
      @param wait          Number of seconds to wait (&lt;0 wait forever).
      @param mask          X-Midas mask value.
      @return 0 if no message was found, 1 if a message was found.
   */
  int getXmMsg(long headerPointer, long dataPointer, int maxdata, float wait, int mask) {
    int status = 0;

    try {
      Message nmMessage = MQ.get(0, (Message)null, (int)Math.ceil((double)wait));

      if (nmMessage != null) {
        XmMessage xmMessage = xmMsgHandler.nmMsgToXm(nmMessage);

        if (xmMessage != null) {
          XmMsgHandler.writeNativeXmMsg(xmMessage, headerPointer, dataPointer, maxdata);
          status = 1;
        }
      }
    } catch(RuntimeException e) {
      M.printStackTrace(e);
      status = 0;
    }

    return status;
  }

  /** Adds an X-Midas message to the queue. This should only be used from the native code.
      @param headerPointer Native pointer to the header.
      @param dataPointer   Native pointer to the data.
      @param reader        ID of the reader.
      @param mask          X-Midas mask value.
      @return 1 on success, -1 on failure.
   */
  int putXmMsg (long headerPointer, long dataPointer, int reader, int mask) {
    int status = -1;

    try {
      XmMessage xmMessage = XmMsgHandler.readNativeXmMsg(headerPointer, dataPointer);
      Message   nmMessage = xmMsgHandler.xmMsgToNm(xmMessage);

      sendMessage(nmMessage);
      status = 1;
    } catch(RuntimeException e) {
      M.printStackTrace(e);
      status = -1;
    }

    return status;
  }

  /** Runs the host primitive. */
  private class RunNativeCode implements Runnable {
    public void run() {
      try {
        exec(nmJackets);
      } catch (Throwable t) {
        M.printStackTrace("Error while running "+getName(), t);
      }
      setHostState(HOST_DONE);
    }
  }
}