package nxm.train.lib;

import nxm.sys.inc.*;
import nxm.sys.lib.*;
import nxm.sys.libg.*;


/** Supports reading/writing ORANGE files.
 
    @author  J. Czechowski IV
    @version $Id: OrangeFile.java,v 1.1 2008/08/15 06:25:55 jxc Exp $
 */
public class OrangeFile extends BaseFile implements PlotFile {
  private static final int HEADER_SIZE = 24; // header size in bytes
  private static final int BPE         =  4; // number of bytes per element
  
  private byte[] header;
  
  public OrangeFile () { }

  public OrangeFile (Object ref, Object filename) {
    init(ref,filename);
  }
  
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Part 1 - The File Header
  //   - The file header information is all included in the header of our file.
  //   - Since we are likely to reference the header information frequently, it is a good idea to
  //     just keep a locak copy of it (header) rather than going back to disk every time.
  //   - Once we read in the header, we will need to make set/get methods that access it. Most of
  //     these are simple one-line methods that just unpack the data from the header buffer.
  //     Reading a value looks like:
  //
  //          Convert.unpack?(header, boff, EEEI)
  //
  //     Where ? is the type (D for DOUBLE, etc.) and boff is the byte offset in the header. The
  //     EEEI simply ways that the value is in little-endian byte ordering.
  //
  //     Setting a value looks like:
  //
  //          Convert.pack?(header, boff, val, EEEI)
  //
  //     This has an additional parameter val that holds the value to set.
  //   - Finally, we want to be able to list the header. We do this by overriding the 
  //////////////////////////////////////////////////////////////////////////////////////////////////
  
  protected void setInternals () {
    // The name "internals" originated in X-Midas, for our purposes it just means any header and
    // run-time information we need in order to read/write the file. For this file type, all of
    // our "internals" are included in the file header.
    
    // This method will be called multiple times if the user provided any file qualifiers. Only
    // on the first call (i.e. when header is null) do we want to initialize the header.
    if (header == null) {
      initHeader();
    }
    // now that we've initialized the header, seek to the start point in the file.
    if ((flags & APPEND) != 0) seek(getSize()); // start at the end of the data section of the file
    else                       seek(0.0);       // start at the beginning of the data section of the file
  }
  
  private void initHeader () {
    if (isInput) {
      header = new byte[HEADER_SIZE];
      // When we read in the file header we intentionally use io.read(..) rather than one of the
      // other methods in this class since we are reading from the header rather than the data
      // portion of the file.
      io.read(header, 0, HEADER_SIZE, 0);
    }
    else {
      header = new byte[HEADER_SIZE];
      setVersion("ORANGE");
      setFormat("SF");
      setXStart(0.0);
      setXDelta(1.0);
    }
  }
  
  public void update () {
    // In update() we need to flush the header back out to disk. Similar to when we read in the
    // header, we need to use the special io.write(..) method when writing out the header.
    io.write(header, 0, HEADER_SIZE, 0);
  }
  
  public String getVersion () {
    return Convert.unpackS(header, 0, 6); // 7 is the length of the string
  }
  
  public String getFormat () {
    return Convert.unpackS(header, 6, 2); // 2 is the length of the string
  }
  
  public double getXStart () {
    return Convert.unpackD(header, 8, EEEI);
  }
  
  public double getXDelta () {
    return Convert.unpackD(header, 16, EEEI);
  }
  
  public void setVersion (String val) {
    Convert.packS(header, 0, 6, val); // 6 is the length of the string
  }
  
  public void setFormat (String val) {
    Convert.packS(header, 6, 2, val); // 2 is the length of the string
  }
  
  public void setXStart (double val) {
    Convert.packD(header, 8, val, EEEI);
  }
  
  public void setXDelta (double val) {
    Convert.packD(header, 16, val, EEEI);
  }
  
  public String listHeader() {
    return "OrangeFile  :  "+getURL()+"\n"
         + "Size        :  "+getSize()+"\n"
         + "Format      :  "+getFormat()+"\n"
         + "XStart      :  "+getXStart()+"\n"
         + "XDelta      :  "+getXDelta()+"\n";
  }
  
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Part 2 - indexing into the file
  //   - We need to override the methods concerned with the size of the file since we
  //     want to return the file size in number of elements not bytes, and we need to
  //     make sure that the size of the header is not considered part of the data portion
  //     of the file.
  //   - IOResource provides three methods that we will make use of:
  //       - io.getLength()    - Returns the total length of the file on disk (in bytes).
  //       - io.seek()         - Returns the current offset into the file that the next
  //                             read/write will take place at. Note that this is in bytes
  //                             and includes the size of the file header.
  //       - io.seek(boff)     - Moves the read/write offset to boff. Note that boff is in
  //                             bytes and includes the size of the file header.
  //   - We have defined two constants at the top of the class that will help us here:
  //       - HEADER_SIZE       - The size of the header (in bytes).
  //       - BPE               - The number of bytes per element. Remember that SF and SL
  //                             are both 32-bits (or 4 bytes) long. That is why this is
  //                             a constant here. If we supported more complicated data this
  //                             would need to be a variable (or a method call).
  //////////////////////////////////////////////////////////////////////////////////////////////////
  
  public double getSize () {
    double blen = io.getLength();        // length of file in bytes (includes header)
    double len  = (blen - HEADER_SIZE)   // subtract the size of the header
                / BPE;                   // convert from bytes to elements
    return len;
  }
  
  public int seek (double off) {
    double boff = (off * BPE)            // convert from number of elements to bytes
                + HEADER_SIZE;           // need to skip over the header    
    io.seek((long)boff);
    return 0;                            // always returns 0
  }

  public double seek() {
    double boff = io.seek();             // current seek offset in bytes (includes header)
    double off  = (boff - HEADER_SIZE)   // subtract the adjustment for the header
                / BPE;                   // convert from bytes to elements
    return off;
  }

  public double avail() {
    // for this file type, the number of elements available for reading is simply the number
    // of elements between our current location (seek()) and the end of the file (getSize()).
    return getSize() - seek();
  }
  
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Part 3 - reading and writing
  //   - BaseFile provides basic read(..) and write(..) methods, but we want ones that work with a
  //     data buffer (similar to the ines in DataFile).
  //   - Also define a getDataBuffer(..) method that matches DataFile to create a buffer to use
  //     when reading/writing.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  
  public Data getDataBuffer (int size) {
    return new Data(getFormat(), size);
  }
  
  public int read (Data data, int elements) {
    int bytes     = elements * BPE;                // convert from number of elements to number of bytes
    int bytesRead = read(data.getBuf(), 0, bytes); // returns number of bytes actually read
    int numRead   = bytesRead / BPE;               // convert back from bytes to elements
    
    // If local system's byte representation (IEEE or EEEI) does not match that
    // of the file (EEEI), we need to convert it to match the local system.
    if (Shell.rep != EEEI) {
      Convert.rep(data.getBuf(), 0, getFormat(), numRead, EEEI, Shell.rep);
    }
    return numRead;
  }
  
  public int write (Data data, int elements) {
    // If local system's byte representation (IEEE or EEEI) does not match that
    // of the file (EEEI), we need to convert it to match the local system.
    if (Shell.rep != EEEI) {
      Convert.rep(data.getBuf(), 0, getFormat(), elements, Shell.rep, EEEI);
    }
    
    int bytes      = elements * BPE;                 // convert from number of elements to number of bytes
    int numWritten = write(data.getBuf(), 0, bytes); // returns number of bytes actually written
    return numWritten / BPE;                         // convert back from bytes to elements
  }
  
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Part 4 - listing
  //   - These methods are used by commands like DATALIST when listing elements in the file.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  public int listElementsPerLine (int lineWidth, String format, int flags) {
    // This method allows us to put multiple elements per line, since we aren't
    // overly concerned with conserving vertical space, we will just return 1.
    return 1;
  }
  
  public String listElements (double start, int elements, String format, int flags) {
    Data data = getDataBuffer(elements);
    seek(start);
    int numRead = read(data, elements);
    if (numRead <= 0) return null;
    return data.toString(0, numRead, 10)+"\n";
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods for PlotFile interface
  //
  //   - BaseFile already provides several of the methods we need. So there is no need to
  //     implement the following methods:
  //         public void init (Object ref, Object filename)
  //         public Object getQualifier (String key)
  //         public boolean find (int dir)
  //         public String getTag ()
  //         public boolean isStream ()
  //         public boolean isStreaming ()
  //         public String getURL ()
  //         public FileName getName ()
  //         public boolean open (int flags)
  //         public boolean isOpen ()
  //         public void close ()
  //
  //   - Additionally we already wrote the following methods:
  //         public double getSize ()
  //         public double getXStart ()
  //         public double getXDelta ()
  //         public String getFormat()
  //         public Data getDataBuffer (int size)
  //         public int read (Data data, int elements)
  //         public int seek (double offset)
  //         public double seek ()
  //         public double avail ()
  //////////////////////////////////////////////////////////////////////////////////////////////////

  public int getXUnits () {
    // This file doesn't specify any units, so just return NONE
    return Units.NONE;
  }

  public int getXFrame () {
    // The X frame (i.e. frame size) is 1 for 1D data.
    return 1;
  }

  // Since this file type only holds 1D data, is just returns the default values for the Y-axis
  public double getYStart () { return 0.0; }
  public double getYDelta () { return 1.0; }
  public int    getYUnits () { return Units.NONE; }
  public int    getYFrame () { return 1;   }
  
  // Since the X-axis is the primary axis, just return its values (this is extremely common)
  public double getStart () { return getXStart(); }
  public double getDelta () { return getXDelta(); }
  public int    getUnits () { return getXUnits(); }
  public int    getFrame () { return getXFrame(); }
  
  public double getTimeAt (double offset) {
    // This file type doesn't specify timecode, so just return 0.0 here.
    return 0.0;
  }

  public int getType () {
    // this is analogous to a Type 1000 BLUE file, so return 1000 for now
    return 1000;
  }
  
  public void connect (int mode) {
    // this is only relevent for a BLUE data pipe, so nothing to do here
  }

  public int getMode () {
    // since we have scalar 1D data the mode (atoms per element) is just 1
    return 1;
  }

  public Layer getLayer () {
    // Specify a default layer for this file type. Since we have 1D data, lets use Layer1D.
    return new Layer1D();
  }
  
}
