package nxm.sys.libxm;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Pipe;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Time;

/** Maps an X-Midas pipe so that it is accessible in NeXtMidas using the same methods used to access
    normal NeXtMidas pipes.<br>
    <span style="color:red">Do NOT use this class directly, this class is for use
    by the SYS option tree ONLY and is subject to change without notice!</span> <br>
    <br>
    This class is the only one that directly accesses X-Midas pipes (other than Header which wraps
    the X-Midas HCB created by this class).<br>
    <br>
    One important difference between {@link Pipe} and {@link XmPipe} is that a single instance of
    {@link Pipe} is used to handle the input and all of the outputs of the pipe. Since a separate
    HCB is used for each input and output of an X-Midas pipe, no attempt was made to have have a
    common {@link XmPipe} instance (i.e. each connection to an X-Midas pipe has a separate instance
    of {@link XmPipe} -- this accounts for the "outlet" field being ignored by many of the methods
    in this class).<br>
    <br>
    <h2>Wrapping of the X-Midas Header Control Block (HCB)</h2>
    The HCB is an X-Midas structure that contains the both the pipe header (the first 512 bytes of
    the HCB which matches the Blue File standard) and many special X-Midas-specific parameters for
    managing pipe i/o. <br>
    <br>
    To keep things clean the following naming conventions are usually followed:
    <table>
      <caption style="visibility:hidden">""</caption>
      <tr>
        <th>Variable Name</th>
        <th>Description</th>
        <th>Accessible From</th>
      </tr>
      <tr>
        <td>hcb</td>
        <td>The native X-Midas HCB data structure (defined in Fortran/C)</td>
        <td>JNI/C/Fortran</td>
      </tr>
      <tr>
        <td>buffer</td>
        <td>The {@link ByteBuffer} that maps to the <code>hcb</code> so it is visible in Java</td>
        <td>JNI/Java</td>
      </tr>
      <tr>
        <td>xmHeader</td>
        <td>The auto-generated wrapper for the <code>buffer</code> with standard get/set methods
            for each field</td>
        <td>Java Only</td>
      </tr>
    </table>
    <h2>Special Restrictions</h2>
    The pipes managed by this class are true X-Midas pipes and have all of the limitations that
    X-Midas pipes have (fixed size, no re-starts or warm-starts, etc). The offset positions are
    all managed by X-Midas and can not be changed from the NeXtMidas side.

    @author  J. Czechowski IV / Neon Ngo
    @since   NeXtMidas 2.1.0
*/
@InternalUseOnly
public class XmPipe extends Pipe {
  private static final boolean DEBUG = false;             // Turns on/off debugging messages
  private static final int     XMIDAS_OUTLET_COUNT =   8; // Number of outlets supported by X-Midas
  private static final int     BLUE_HEADER_LENGTH  = 512; // Number of bytes in a blue header

  private static final Method GET_INTLET_METHOD = getMethod("getInlet"); // reflection for Header.getInlet()
  private static final Method GET_OUTLET_METHOD = getMethod("getOutlets"); // reflection for Header.getOutlets()
  private static final Method GET_OUTMASK_METHOD = getMethod("getOutmask"); // reflection for Header.getOutmask()

  private Header xmHeader; // Wraps the X-Midas HCB (see comments at top of class)

  /* Load the JNI library that implements the native methods */
  static {
    XMidas.loadXmJvmLib();
  }

  /** Creates a new instance.
      @param M    The midas context to use.
      @param name The name of the X-Midas pipe to connect to.
   */
  public XmPipe (Midas M, String name) {
    super(M, name, XMIDAS_OUTLET_COUNT);
  }

  /** Frees the X-Midas HCB structure being used and disconnects the pipe (if still connected).
   *  @deprecated in NeXtMidas 4.1.3 */
  @Deprecated
  public void finalize() {
    if (xmHeader != null) {
      synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
        close(xmHeader.getByteBuffer());
        freeHeaderBuffer(xmHeader.getByteBuffer());
      } // end of mutex for calling X-Midas API
    }
  }

  /** Frees the X-Midas HCB structure being used and disconnects the pipe (if still connected).
   *  @since NeXtMidas 4.1.3 */
  public void delete() {
    if (xmHeader != null) {
      synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
        close(xmHeader.getByteBuffer());
        freeHeaderBuffer(xmHeader.getByteBuffer());
      } // end of mutex for calling X-Midas API
    }
  }

  /**
   * This is a helper method that will attempt to get the method from {@link Header}
   * @param methodName The name of the method to retrieve
   * @return The method if it exists, otherwise null
   */
  private static final Method getMethod (String methodName) {
    Method m = null;
    try {
      m = Header.class.getMethod(methodName);
    } catch (NoSuchMethodException | SecurityException e) {
      try {
        m = Header.class.getMethod(methodName+"Unused");
      } catch (NoSuchMethodException | SecurityException e1) {
        e1.printStackTrace();
      }
    }
    return m;
  }

  @Override
  public String toString() {
    return "Raw X-Midas Pipe Name=" + getName();
  }

  @Override
  public synchronized int attach(int dir, DataFile df) {
    if (DEBUG) { System.out.println("XmPipe.attach("+dir+", "+df+")"); }
    if (!isAttached()) {
      attach(getName(), dir, df, df.types, df.formats);
    } else {
      M.warning("XmPipe: Already attached to pipe " + getName());
    }
    return xmHeader.getOutlet();
  }

  /** Opens a pipe and attaches to it.
      @param name The name of the pipe.
      @param dir  The pipe direction.
   */
  private void attach(String name, int dir, DataFile df, String types, String formats) {
    int functions = (dir < 0)? Header.HCBF_INPUT: Header.HCBF_OUTPUT; // The X-Midas direction flag

    if (types   == null) { types   = "1000,2000"; }
    if (formats == null) { formats = "S#,C#"; }

    if (DEBUG) { System.out.println("XmPipe.attach("+name+","+dir+", "+types+", "+formats+")"); }

    /* We found that using use m_initialize(hcb) in the native code and setting hcb.file_name,
       hcb.type_allow, and hcb.format_allow manually is much easier than using m_init(..).
    */
    xmHeader = new Header(initHeaderBuffer());
    xmHeader.setFileName(name.toUpperCase());
    xmHeader.setTypeAllow(types);
    xmHeader.setFormatAllow(formats);

    /* For OUTPUT pipes only: (1) Need to propagate header values but DO NOT CALL updateHeader(..).
                              (2) Need to set file size (not included in header but in HCB), if the
                                  size is zero, issue a warning as this may cause issues later on.
    */
    if ((df != null) && (df.hb != null) && (dir >= 0)) {
      writeHeader(df.hb, true);

      if (df.getSize() > 0.0) {
        xmHeader.setSize(df.getSize());
      } else {
        M.warning("File size set to "+xmHeader.getSize()+" for X-Midas pipe "+name);
      }
    }

    synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
      open(xmHeader.getByteBuffer(), functions);
    } // end of mutex for calling X-Midas API

    /* For OUTPUT and INPUT pipes: Copy the header to the DataFile... note that the header will be
                                   updated during the call to open(..) and the new values need to
                                   be copied back (failure to do this will cause core dumps for
                                   output pipes since open(..) sets the pipe size).
    */
    if ((df != null) && (df.hb != null)) {
      readHeader(df.hb);
    }
  }

  /** detach from pipe */
  @Override
  public synchronized int detach (int outlet, DataFile df) {
    if (DEBUG) { System.out.println("XmPipe.detach("+outlet+", "+df+")"); }
    if (isAttached()) {
      synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
        close(xmHeader.getByteBuffer());
      } // end of mutex for calling X-Midas API
    }

    return xmHeader.getOutlet();
  }

  /** update the combined read pointers */
  @Override
  public double lastOut () {
    if (DEBUG) { System.out.println("XmPipe.lastOut()"); }
    double out = getInByte();
    for (int i=0; i<getOutlets(); i++) {
      if ((getOutMask()&(1<<i)) != 0 && getOutByte(i+1)<out) {
        out=getOutByte(i+1);
      }
    }
    setOutByte(out);
    return out;
  }

  @Override
  public int readHeader(byte[] hb, int outlet) {
    if (DEBUG) { System.out.println("XmPipe.readHeader(hb, "+outlet+")"); }
    int bytes = 0; // Number of bytes read

    if (isAttached()) {
      bytes = readHeader(hb);
    }

    return bytes;
  }

  @Override
  public int writeHeader(byte[] hb, int outlet) {
    if (DEBUG) { System.out.println("XmPipe.writeHeader(hb, "+outlet+")"); }
    int bytes = 0; // Number of bytes written

    if (isAttached()) {
      bytes = writeHeader(hb, false);

      if (bytes > 0) {
        // JC4: There may be a few cases where writing to the pipe are ok, but I'll
        //      leave that as a TBD.
        String msg = "XmPipe: Can not write header to X-Midas pipe "+ xmHeader.getFileName()
                   + ": X-Midas pipes can not be modified after they are open.";
        throw new MidasException(msg);
      }
    }
    return 0;
  }

  /** Reads the contents of the HCB into the byte buffer. This does not check the status of the HCB.
      @param hb The buffer to receive the bytes.
      @return The number of bytes read.
   */
  private int readHeader(byte[] hb) {
    if (DEBUG) { System.out.println("XmPipe.readHeader(hb)"); }
    int bytes = Math.min(hb.length, BLUE_HEADER_LENGTH); // Number of bytes to be read

    for (int i = 0; i < bytes; i++) {
      hb[i] = xmHeader.getByteBuffer().get(i);
    }

    return bytes;
  }

  /** Writes the contents of the byte buffer to the HCB. This does NOT call updateHeader(..) and
      does not check the status of the HCB.
      @param hb    The bytes to write.
      @param write Should the changes be written? (Set this to false to just check for changes.)
      @return The number of bytes written or <0 if no changes need to be made.
   */
  private int writeHeader(byte[] hb, boolean write) {
    if (DEBUG) { System.out.println("XmPipe.writeHeader(hb)"); }
    int bytes = Math.min(hb.length, BLUE_HEADER_LENGTH); // Number of bytes to be written
    boolean anyChanges = false;

    for (int i = 0; i < bytes; i++) {
      byte oldByte = xmHeader.getByteBuffer().get(i);
      byte newByte = hb[i];

      if ((i >= 24) && (i <= 27)) { newByte = oldByte; } // Ignore changes to ext_start

      if (oldByte != newByte) {
        if (write) { xmHeader.getByteBuffer().put(i, newByte); }
        anyChanges = true;
      }
    }
    return (anyChanges)? bytes: -bytes;
  }

  /** write to pipe section */
  @Override
  public int write (byte[] buf, long lbuf, int boff, int bytes, int outlet) {
    if (DEBUG) { System.out.println("XmPipe.write(buf, "+lbuf+", "+boff+", "+bytes+", "+outlet+")");}
    int bpe          = xmHeader.getBpe();
    int nele         = Math.min(bytes, (buf.length-boff) / bpe); // Number of elements
    int bytesWritten = bpe * nele;                               // Number of bytes written

    synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
      write(xmHeader.getByteBuffer(), buf, boff, nele);
    } // end of mutex for calling X-Midas API

    if (DEBUG) { System.out.println("wrote "+bytesWritten+" bytes"); }
    return bytesWritten;
  }

  /** read from pipe section */
  @Override
  public int read (byte[] buf, long lbuf, int boff, int bytes, int outlet) {
    if (DEBUG) { System.out.println("XmPipe.read(buf, "+lbuf+", "+boff+", "+bytes+", "+outlet+")"); }
    int bpe       = xmHeader.getBpe();
    int xferLen   = Math.min(bytes, (buf.length-boff)) / bpe;
    int bytesRead = 0;

    xmHeader.setXferLen(xferLen);
    xmHeader.setConsLen((byte)-1); // -1 = Always matches XferLen, -2 = asynchronous pipe mode

    synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
      bytesRead = bpe * read(xmHeader.getByteBuffer(), buf, boff);
    } // end of mutex for calling X-Midas API

    if (bytesRead == 0) {
      if (xmHeader.getOffset() == xmHeader.getSize()) {
        bytesRead = -1; // End of file reached
        if (DEBUG) { System.out.println("XmPipe: read: End of file reached!"); }

      } else if ((xmHeader.getSize()*bpe) == xmHeader.getLastByte()) {
        M.warning("XmPipe: Can not read from pipe "+name+": pipe has shut down.");
        bytesRead = -1; // Pipe has closed
      }
    }

    if (DEBUG) { System.out.println("XmPipe: read: read "+bytesRead+" bytes"); }
    return bytesRead;
  }

  /**
   * {@inheritDoc}
   * @deprecated since NeXtMidas 3.9.0 - X-Midas 5+ drops support for this method.
   */
  @Deprecated
  @Override
  public short getInlet () {
    try {
      return (short) GET_INTLET_METHOD.invoke(xmHeader);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      // DO NOTHING, THE METHOD DOES NOT EXIST
      return 0;
    }
  }

  /**
   * {@inheritDoc}
   * @deprecated since NeXtMidas 3.9.0 - X-Midas 5+ drops support for this method.
   */
  @Deprecated
  @Override
  public short getOutlets () {
    try {
      return (short) GET_OUTLET_METHOD.invoke(xmHeader);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      // DO NOTHING, THE METHOD DOES NOT EXIST
      return 0;
    }
  }

  /**
   * {@inheritDoc}
   * @deprecated since NeXtMidas 3.9.0 - X-Midas 5+ drops support for this method.
   */
  @Deprecated
  @Override
  public int getOutMask () {
    try {
      return (int) GET_OUTMASK_METHOD.invoke(xmHeader);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      // DO NOTHING, THE METHOD DOES NOT EXIST
      return 0;
    }
  }

  @Override
  public int    getSize()         { return xmHeader.getPipesize();         }
  @Override
  public double getInByte()       { return xmHeader.getInByte();           }
  @Override
  public double getOutByte()      { return getOutByte(0);                  }
  @Override
  public double avail(int outlet) { return getAvail();                     }

  @Override
  public double getAvail() {
    double avail = 0.0; // Number of bytes available

    if (isAttached()) {
      synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
        int elements = avail(xmHeader.getByteBuffer()); // Number of logical elements available
        avail = (double)(elements * xmHeader.getBpe());
      } // end of mutex for calling X-Midas API

    } else {
      throw new MidasException("XmPipe: Can not get number of available bytes until pipe is open.");
    }

    return avail;
  }

  @Override
  public void setInByte(double value)  {
    throw new MidasException("XmPipe: Can not set the in byte of an X-Midas pipe.");
  }

  @Override
  public void setSize(int value) {
    throw new MidasException("XmPipe: Can not adjust size of X-Midas pipes.");
  }

  @Override
  public void setOutByte(double value) {
    throw new MidasException("XmPipe: Can not set the out byte of an X-Midas pipe.");
  }

  @Override
  public void setOutByte(int outlet, double value) {
    throw new MidasException("XmPipe: Can not set the out byte of an X-Midas pipe.");
  }

  @Override
  public double getOutByte(int outlet) {
    double outByte = 0.0;

    if (outlet == 0) {
      outByte = xmHeader.getOutByte();             // get the general offset
    } else {
      outByte = xmHeader.getOutBytes()[outlet-1];  // get the outlet-specific offset
    }

    return outByte;
  }

  @Override
  public double getRate() {
    // JC4: This method is based on that in Pipe... I have not re-checked the algorithm
    if (DEBUG) { System.out.println("XmPipe.getRate()"); }
    double time = Time.current();
    double xfer = (getInByte()-inPtrLast);
    double rate = xfer/(time-timeLast);
    timeLast = time;
    inPtrLast = getInByte();
    return rate;
  }

  @Override
  public DataFile getDataFile() {
    return getDataFile(0);
  }

  @Override
  public DataFile getDataFile (int outlet) {
    if (DEBUG) { System.out.println("XmPipe.getDataFile("+outlet+")"); }
    return null;
  }

  @Override
  public Object getOwner (int outlet) {
    if (DEBUG) { System.out.println("XmPipe.getOwner("+outlet+")"); }
    return null;
  }

  /** Seeks to the given offset in the file. This is only used by {@link XmPipeResource}.
      @param offset The offset.
      @return Success (true) or failure (false).
   */
  boolean seek(long offset) {
    if (DEBUG) { System.out.println("XmPipe.seek("+offset+")"); }
    boolean status = true;

    if (isAttached()) {
      if (offset == xmHeader.getOffset()) {
        /* Already there */

      } else if (offset == xmHeader.getOffset()) {
        synchronized(XMidas.getLockObj()) { // mutex for calling X-Midas API
          seek(xmHeader.getByteBuffer(), (double)offset);
        } // end of mutex for calling X-Midas API

      } else {
        /* JC4: This is the easy-way-out here... we can seek backwards provided those bytes are
                still in the pipe, but it is a bit tricky. If someone wants to add this in later
                it's fine with me.
        */
        M.warning("XmPipe: Can not seek backwards in an X-Midas pipe, ignoring seek("+offset+").");
        status = false;
      }
    } else if (offset == 0.0) {
      /* JC4: Expect seek(0.0) calls to be made before the pipe is open (DataFile does this to
              ensure it will start reading from the start of the pipe). Since X-Midas pipes will
              automatically start at 0.0 (if opened in Primitive.open()) or at the earliest byte
              available (if connecting to a running pipe) it is usually safe to ignore this case.
      */
    } else {
      M.warning("XmPipe: Can not seek until the pipe is open, ignoring seek("+offset+").");
      status = false;
    }

    return status;
  }

  /** Gets the names of all available X-Midas pipes. This is only used by {@link XmPipeResource}. */
  static String[] getPipeNames() {
    if (DEBUG) { System.out.println("XmPipe.getPipeNames()"); }
    String[] pipeNames = null;

    if (Mt.getNpipe() > 0) {
      pipeNames = new String[Mt.getNpipe()];

      for (int i = 0; i < pipeNames.length; i++) {
        pipeNames[i] = pipeName(i+1);
      }

    } else {
      pipeNames = new String[0];
    }

    if (DEBUG) { System.out.println("XmPipe.getPipeNames() = "+StringUtil.join(pipeNames)); }
    return pipeNames;
  }

  /** Indicates if currently attached to the X-Midas pipe. */
  private boolean isAttached() {
    return (xmHeader != null) && xmHeader.getOpen();
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Native methods used by XmPipe
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Allocates and initializes a new HCB structure in the JNI.
      @return The newly-created buffer.
   */
  private static ByteBuffer initHeaderBuffer() {
    ByteBuffer buffer = initHeaderBuffer0();

    if (buffer == null) {
      throw new MidasException("Can not allocate new X-Midas HEADER: Out of Memory.");
    }

    buffer.order(ByteOrder.nativeOrder());

    return buffer;
  }

  /** This should only be called by initHeaderBuffer(). The buffer returned will not have the
      byte order set correctly, may return null if an error occurs.
   */
  private static native ByteBuffer initHeaderBuffer0();

  /** Frees a HEADER structure.
      @param buffer The HEADER structure to free.
   */
  private static native void freeHeaderBuffer(ByteBuffer buffer);

  /** Opens a file.
      @param buffer    The byte buffer wrapping the header control block for the file.
      @param functions The functions mask.
   */
  private static native void open(ByteBuffer buffer, int functions);

  /** Closes a file.
      @param buffer The byte buffer wrapping the header control block for the file.
   */
  private static native void close(ByteBuffer buffer);

  /** Gets the number of elements available.
      @param buffer The byte buffer wrapping the header control block for the file.
   */
  private static native int avail(ByteBuffer buffer);

  /** Updates the actual pipe header with the values inserted into the HCB.
      @param buffer The byte buffer wrapping the header control block for the file.
   */
  private static native void updateHeader(ByteBuffer buffer);

  /** Seeks to the given position in the pipe.
      @param buffer   The byte buffer wrapping the header control block for the file.
      @param position The position to seek to.
   */
  private static native void seek(ByteBuffer buffer, double position);

  /** Read logical elements from the file.
      @param buffer The byte buffer wrapping the header control block for the file.
      @param bbuf   Buffer to get the data from.
      @param boff   The element number to start at in the byte buffer.
      @return The number to elements actually read.
   */
  private static native int read(ByteBuffer buffer, byte[] bbuf, int boff);

  /** Write logical elements to the file.
      @param buffer The byte buffer wrapping the header control block for the file.
      @param bbuf   Buffer to insert the data into.
      @param boff   The element number to start at in the byte buffer.
      @param nele   The number of elements to write.
   */
  private static native void write(ByteBuffer buffer, byte[] bbuf, int boff, int nele);

  /** Seek to the given position in the file.
      @param pipeNum The pipe number (**ONE_BASED**).
      @return Always returns true.
   */
  private static native String pipeName(int pipeNum);
}
