package nxm.sys.lib;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.Units;

/**
 * A class that supports creation of a DataFile stream by allowing
 * header and footer data to be created in memory for 1000 and 2000 files
 * @author Jeanette McCall
 * @since NeXtMidas 4.1.4 (prototype only)
 */
@InternalUseOnly
public class DataFileStreamSupport {

  // Main Header Keyword support
  /** byte offset in header of keywords string */
  private static final int IKOFF =  164;
  /** The ASCII null (<code>'\0'</code>) character.   */ char NULL   =   0;
  /** MainHeader Keyword String */
  private byte[] mainKWBytes     = ("VER=1.1"+NULL+"IO=NeXtMidas"+NULL+"CREATOR=NXM"+Installation.NM_VERSION+NULL).getBytes();

  // Data Section support
  /** Used for calculating number of blocks needed for data section */
  private int BLOCK_SIZE_MINUS_1 = DataFile.BYTE_BLOCK_SIZE - 1;


  // header values - Midas Blue File Format used for order and names (when possible)
  /** Midas file version - BLUE */
  private String version    = "BLUE";
  /** The byte ordering for information stored in the header */
  private String header_rep = Shell.osrep;
  /** The byte ordering for information in the data portion of the file */
  private String data_rep   = Shell.osrep;
  /** A non-zero value indicates the data itself exists in a separate file (DEF=1) */
  private int detached      = 1;
  /** A non-zero value means file is read-only - this is for stream (not file) */
  private int protectedFlag = 0;
  /** X-Midas only - 0 for BLUE files */
  private int pipe          = 0;
  /** Extended header starting offset - indicates the number of 512 byte blocks
   *  from the start of the file to the start of the extended header (DEF=1) */
  private int ext_start     = 1;
  /** Extended header size (calculated) */
  private int ext_size;
  /** The offset of the data from the start of the file, in bytes (multiple of 512)
      (DEF=0 for detached) */
  private double data_start = 0;
  /** The size, in bytes, of the data portion of the file - (DEF=0) */
  private double data_size  = 0;
  /** Midas File Type - type divided by 1000 indicates overall file structure - (DEF=1000) */
  private int type          = 1000;
  /** Format of Data elements - 2 character digraph - mode and type - (DEF=SF) */
  private String format     = "SF";
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private short flagmask    = 0;
  /** Specifies the epoch time for file data, in seconds since 1 January 1950 (DEF=0)*/
  private double timeCode   = 0.0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private short inlet       = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private short outlets     = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private int   outmask     = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private int pipeloc       = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private int pipesize      = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private double in_byte    = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private double out_byte   = 0;
  /** used in X-Midas pipes and should be set to 0 for BLUE files */
  private double out_bytes  = 0;
  /** The keylength field specifies the valid length, in bytes, of the
      keyword section located in the main header. (calculated) */
  private int keylength     = mainKWBytes.length;

  // Type 1000 and 2000 adjunct structure
  /** 1000 - Abscissa value for first sample, 2000 - Frame (row) starting value (DEF=0)*/
  private double xstart = 0;
  /** 1000 - Abscissa interval between samples, 2000 - Interval between consecutive samples in a frame (DEF=1) */
  private double xdelta = 1;
  /** 1000 - Units of measure for xstart and xdelta (DEF=TIME_S), 2000 - Frame (row) units (DEF=Hz)*/
  private int    xunits;

  // 2000 specific adjunct structure
  /** Number of data points per frame (row) - MANDATORY if 2000 */
  private int    subsize = 0; // 1000 default is 0
  /** Abscissa (column) start - start time of the first frame (DEF=0) */
  private double ystart  = 0;
  /** Increment between frames (DEF=1) */
  private double ydelta  = 1;
  /** Units associated with the ystart and ydelta (DEF=TIME_S))*/
  private int    yunits  = Units.TIME_S;

  // Format designators
  /** multiplier code */
  private byte formatMode;
  /** data type */
  private byte formatType;


  /** Extended Header Keywords */
  private Keywords extHeaderKWs = null;

  /** Constructor for DataFile Stream Support */
  public DataFileStreamSupport () {}

  /** Pack an entire Blue File into a byte buffer
   *  <pre>
   *  ****************************
   *  *  Main Header (512 bytes) *
   *  ****************************
   *  *           Data           *
   *  ****************************
   *  *     Extended Header      *
   *  ****************************
   *  </pre>
   *
   *  @param data byte buffer filled with data for the file
   *  @return byte buffer representing Blue File
   */
  public byte[] packBlueFile (byte[] data) {
    return packBlueFile(data, 0, data.length);
  }

  /** Pack an entire Blue File into a byte buffer
   *  <pre>
   *  ****************************
   *  *  Main Header (512 bytes) *
   *  ****************************
   *  *           Data           *
   *  ****************************
   *  *     Extended Header      *
   *  ****************************
   *  </pre>
   *
   *  @param data      byte buffer filled with data for the file
   *  @param off       start of data
   *  @param data_size size of data in bytes
   *  @return byte buffer representing Blue File
   */
  public byte[] packBlueFile (byte[] data, int off, double data_size) {

    // set values that are calculated based on data and extended keywords
    this.detached = 0;
    this.data_start = DataFile.HEADER_SIZE;
    this.data_size = data_size;
    int numBlocksData = (int)((data_size + BLOCK_SIZE_MINUS_1) / DataFile.BYTE_BLOCK_SIZE);
    int dataBlocksSize = numBlocksData*DataFile.BYTE_BLOCK_SIZE;
    ext_start = 1 + numBlocksData;
    calcExtHeaderSize();
    int extKWBlocksSize = (ext_size + BLOCK_SIZE_MINUS_1) / DataFile.BYTE_BLOCK_SIZE;
    int blueFileSize = DataFile.HEADER_SIZE + dataBlocksSize + extKWBlocksSize*DataFile.BYTE_BLOCK_SIZE;

    byte[] blueFileBytes = new byte[blueFileSize];

    // pack Main Header
    packHeaderControlBlock(blueFileBytes);

    // pack data
    if (data_size > 0) {
      System.arraycopy(data, off, blueFileBytes, (int)data_start, (int)data_size);
    }

    // pack extended header
    if (ext_size!=0) {
      System.arraycopy(extHeaderKWs.buf, 0, blueFileBytes, ext_start*DataFile.BYTE_BLOCK_SIZE, ext_size);
    }

    return blueFileBytes;
  }



  /** Method to create a detached header in memory
   *
   *  <pre>
   *  ****************************
   *  *  Main Header (512 bytes) *
   *  ****************************
   *  *     Extended Header      *
   *  ****************************
   *  </pre>
   *
   *  @return detached header as byte array
   */
  public byte[] packDetachedHeader () {

    // set / calculate entries based on this being a detached header and the extended keywords
    this.detached = 1;
    this.data_start = 0;
    calcExtHeaderSize();

    // get size of detached header = Main Header + Extended Header
    byte[] detachedHeader = new byte[DataFile.HEADER_SIZE + ext_size];

    packHeaderControlBlock(detachedHeader);

    // Place Extended header right after Main Header, since we are making a detached header
    if (ext_size!=0) {
      System.arraycopy(extHeaderKWs.buf, 0, detachedHeader, ext_start*DataFile.BYTE_BLOCK_SIZE, ext_size);
    }

    return detachedHeader;
  }

  /** Pack a byte buffer with the Main Header
   *
   *  <pre>
   *  ***********************************
   *  * HCB - fixed portion (256 bytes) *
   *  ***********************************
   *  *       Main Header Keywords      *
   *  ***********************************
   *  </pre>
   *
   *  @return 512 byte buffer with the Main Header
   */
  public byte[] packMainHeaderBuffer () {

    // some HCB entries need to be calculated first
    calcExtHeaderSize();

    byte[] mainHeader = new byte[DataFile.HEADER_SIZE];
    packHeaderControlBlock(mainHeader);
    return mainHeader;
  }

  /** Pack byte buffer with Extended Header
   *  buffer is padded to 512 byte boundary
   *
   *  @return byte buffer with Extended Header
   */
  public byte[] packExtendedHeaderBuffer () {

    calcExtHeaderSize();
    int extKWBlocksSize = (ext_size + BLOCK_SIZE_MINUS_1) / DataFile.BYTE_BLOCK_SIZE;

    byte[] extendedHeader = new byte[extKWBlocksSize];

    if (ext_size!=0) {
      System.arraycopy(extHeaderKWs.buf, 0, extendedHeader, 0, ext_size);
    }

    return extendedHeader;
  }

  /** Fill in the Main Header values
   *  <pre>
   *  Off   Name         Value
   *  ---   ----------   --------
   *  0     version       1.1
   *  4     head_rep      OS rep
   *  8     data_rep      OS rep
   *  12    detached      DEF=1
   *  16    protected     DEF=0
   *  20    pipe          0 - N/A
   *  24    ext_start     DEF=512
   *  28    ext_size      calculated
   *  32    data_start    DEF=0
   *  40    data_size     set/calculated
   *  48    type          DEF=1000
   *  52    format        DEF=SF
   *  54    flagmask      0 - N/A
   *  56    timecode      DEF=0
   *  64    inlet         0 - N/A
   *  66    outlets       0 - N/A
   *  68    outmask       0 - N/A
   *  72    pipeloc       0 - N/A
   *  76    pipesize      0 - N/A
   *  80    in_byte       0 - N/A
   *  88    out_byte      0 - N/A
   *  96    outbytes      0 - N/A
   *  160   keylength     set based on predetermined Main Header Keywords
   *  164   keywords      predetermined Main Header Keywords
   *  256   adjunct       1000/2000 adjunct values (ex: xstart,xunits,subsize,ydelta)
   *  </pre>
   *  @param hb byte buffer to pack
   */
  private void packHeaderControlBlock (byte[] hb) {

    //***************************************************
    // pack fixed version portion - Header Control Block
    //***************************************************

    // Get byte value for Header Rep
    byte headerRep = Data.getRep(header_rep);

    Convert.packS(hb,DataFile.OFF_VERSION,DataFile.CHARS_VERSION,version);
    Convert.packS(hb,DataFile.OFF_HEADREP,DataFile.CHARS_REP,header_rep);
    Convert.packS(hb,DataFile.OFF_DATAREP,DataFile.CHARS_REP,data_rep);
    Convert.packL(hb,DataFile.OFF_DETACHED,detached,headerRep);
    Convert.packL(hb,DataFile.OFF_PROTECTED,protectedFlag,headerRep);
    Convert.packL(hb,DataFile.OFF_PIPE,pipe,headerRep);
    Convert.packL(hb,DataFile.OFF_EXTSTART,ext_start,headerRep);
    Convert.packL(hb,DataFile.OFF_EXTSIZE,ext_size,headerRep);
    Convert.packD(hb,DataFile.OFF_DATASTART,data_start,headerRep);
    Convert.packD(hb,DataFile.OFF_DATASIZE,data_size,headerRep);
    Convert.packL(hb,DataFile.OFF_TYPE,type,headerRep);
    Convert.packS(hb,DataFile.OFF_FORMAT,DataFile.CHARS_FORMAT, format);
    // slip flagmask as it is X-Midas only and 0 for NeXtMidas
    Convert.packD(hb,DataFile.OFF_TIMECODE,timeCode,headerRep);
    // skip X-Midas only and 0 for NeXtMidas entries

    Convert.packL(hb,DataFile.OFF_KEYLGTH,keylength,headerRep);
    Convert.packD(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_XSTART_OFF,xstart,headerRep);
    Convert.packD(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_XDELTA_OFF,xdelta,headerRep);
    Convert.packL(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_XUNITS_OFF,xunits,headerRep);

    // Type 2000 information - put in defaults if 1000
    Convert.packL(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_SUBSIZE_OFF,subsize,headerRep);
    Convert.packD(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_YSTART_OFF,ystart,headerRep);
    Convert.packD(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_YDELTA_OFF,ydelta,headerRep);
    Convert.packL(hb,DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_YUNITS_OFF,yunits,headerRep);

    // hb fields that are 0, because they only apply to X-Midas
    if (false) {
      Convert.packI(hb,DataFile.OFF_FLAGMASK,flagmask,headerRep);
      Convert.packI(hb,DataFile.OFF_INLET,inlet,headerRep);
      Convert.packI(hb,DataFile.OFF_OUTLETS,outlets,headerRep);
      Convert.packL(hb,DataFile.OFF_OUTMASK,outmask,headerRep);
      Convert.packL(hb,DataFile.OFF_PIPELOC,pipeloc,headerRep);
      Convert.packL(hb,DataFile.OFF_PIPESIZE,pipesize,headerRep);
      Convert.packD(hb,DataFile.OFF_INBYTE,in_byte,headerRep);
      Convert.packD(hb,DataFile.OFF_OUTBYTE,out_byte,headerRep);
      Convert.packD(hb,DataFile.OFF_OUTBYTES,out_bytes,headerRep);
    }

    //********************************************
    // Write main header keywords
    //********************************************
    System.arraycopy(mainKWBytes,0, hb,IKOFF, keylength);

  }

  private void calcExtHeaderSize () {
    ext_size = ((extHeaderKWs!=null) && (extHeaderKWs.buf!=null)) ?
                   extHeaderKWs.size : 0;
  }


  //*****************************************
  //    Setters and getters for HCB values
  //   (listed in order they appear in HCB)
  //*****************************************

  /** Set if Detached Header
   *  Any non-zero value means detached
   *  <pre>
   *  Defaults:
   *    initial                   : 1 - detached
   *    packDetachedHeader called : 1 - detached
   *    packBlueFile called       : 0 - not detached
   *  </pre>
   *
   *  @param isDetached true for detached
   */
  public void setDetached (boolean isDetached) {
    this.detached = (isDetached) ? 1 : 0;
  }

  /** Set if Detached Header
   *  Any non-zero value means detached
   *  <pre>
   *  Defaults:
   *    initial                   : 1 - detached
   *    packDetachedHeader called : 1 - detached
   *    packBlueFile called       : 0 - not detached
   *  </pre>
   *
   *  @param detached 0 - not detached, non-zero for detached
   */
  public void setDetached (int detached) {
    this.detached = detached;
  }

  /** Get if Detached Header
   *
   *  @return 0 if not detached, non-zero if detached
   */
  public int getDetached () {
    return this.detached;
  }

  /** Set if file is protected
   *  Since this class primarily for streaming a file
   *  the default is 0 (i.e. not protected)
   *
   *  @param isProtected true if any resulting file is to be protected from overwrite
   */
  public void setProtected (boolean isProtected) {
    this.protectedFlag = (isProtected) ? 1 : 0;
  }

  /** Set if file is protected
   *  Since this class primarily for streaming a file
   *  the default is 0 (i.e. not protected)
   *
   *  @param protectedFlag 1 if any resulting file is to be protected from overwrite
   */
  public void setProtected (int protectedFlag) {
    this.protectedFlag = protectedFlag;
  }

  /** Get if the file is protected from overwrite
   *  @return non-zero if any resulting file is to be protected from overwrite
   */
  @InternalUseOnly
  public int getProtected () {
    return protectedFlag;
  }

  /** Set the extended header start
   *  @param ext_start num 512-byte sections before start of extended header
   */
  public void setExtStart (int ext_start) {
    this.ext_start = ext_start;
  }

  /** Get extended header start
   *  @return the extended header start, in 512-byte blocks
   */
  @InternalUseOnly
  public int getExtStart () {
    return ext_start;
  }

  /** Set the extended header size
   *  WARNING: This should almost NEVER be set by the user.
   *  It is caluclated based on the Extended Header Keywords.
   *  @param ext_size the extended header size in bytes
   */
  @InternalUseOnly
  public void setExtSize (int ext_size) {
    this.ext_size = ext_size;
  }

  /** Get the extended header size
   *  @return the extended header size in bytes
   */
  @InternalUseOnly
  public int getExtSize () {
    return ext_size;
  }

  /** Set the offset of the data from the start of the file
   *  <pre>
   *  Defaults:
   *    initial                   : 0   - to match start of .det file
   *    packDetachedHeader called : 0   - start of .det file
   *    packBlueFile called       : 512 - after main header
   *  </pre>
   *  @param data_start the data start in bytes
   */
  public void setDataStart (double data_start) {
    this.data_start = data_start;
  }

  /** Get the offset of the data from the start of the file
   *  @return the data start in bytes
   */
  @InternalUseOnly
  public double getDataStart () {
    return data_start;
  }

  /** Set the data size
   *  @param data_size the size of the data portion of the file (in bytes)
   */
  public void setDataSize (double data_size) {
    this.data_size = data_size;
  }

  /** Get the data size
   *  @return the size of the data portion of the file (in bytes)
   */
  @InternalUseOnly
  public double getDataSize () {
    return data_size;
  }

  /** Set the File type code
   *  Note: Only 1xxx and 2000 files are currently supported
   *  @param type the file type code
   */
  @InternalUseOnly
  public void setType (int type) {
    this.type = type;
  }

  /** Get the File type code
   *  @return the file type code
   */
  @InternalUseOnly
  public int getType () {
    return type;
  }

  /** Get the format 2-character designator
   *  String of multiplier char and type char
   *  @param format the format as a 2-char string
   */
  @InternalUseOnly
  public void setFormat (String format) {
    if (format.length()!=2) {
      throw new IllegalArgumentException("DataFileStream.setFormat requires a 2-char string - illegal:"+format);
    }
    this.format = format;
  }

  /** Get the format
   *  @return the format
   */
  @InternalUseOnly
  public String getFormat () {
    return format;
  }

  /** Set the format mode
   *  a.k.a. multiplier
   *  @param formatMode the format mode
   */
  @InternalUseOnly
  public void setFormatMode (byte formatMode) {
    this.formatMode = formatMode;
    this.format = ((char)formatMode) + format.substring(1);
  }

  /** Get the format mode
   *  @return the format mode
   */
  @InternalUseOnly
  public byte getFormatMode () {
    return formatMode;
  }

  /** Set the format type
   *  a.k.a. data type
   *  @param formatType the format type
   */
  @InternalUseOnly
  public void setFormatType (byte formatType) {
    this.formatType = formatType;
    this.format = format.substring(0,1) + ((char)formatType);
  }

  /** Get the format type
   *  @return the format type
   */
  @InternalUseOnly
  public byte getFormatType () {
    return formatType;
  }

  /** Set the time code field
   *  @param timeCode the time code as a double
   */
  public void setTimeCode (double timeCode) {
    this.timeCode = timeCode;
  }

  /** Set the time code field
   *  @param time the time code field as a Time
   */
  public void setTimeCode (Time time) { setTimeCode(time.getSec()); }

  /** Get the timecode
   *  @return the timeCode
   */
  @InternalUseOnly
  public double getTimeCode () {
    return timeCode;
  }

  /** Get the length of Main Header Keywords NULL spaced String
   *  @return the length of keyword string
   */
  @InternalUseOnly
  public int getKeyLength () {
    return keylength;
  }

  /** Set the extended header keywords
   *  @param map Map containing the extended header keywords
   */
  @InternalUseOnly
  public void setExtendedHeaderKeywords (LinkedHashMap<String,Object> map) {
    extHeaderKWs = new Keywords();
    extHeaderKWs.setScope("EXT");
    extHeaderKWs.addAll(map);
  }

  //***********************************************
  //   Setters and getters for HCB adjunct values
  //   Type 1000 and 2000 Ajunct Header fields
  //***********************************************

  /** Set the xstart
   *  <pre>
   *  1000 - Absicca value for first sample
   *  2000 - Frame (row) starting value
   *  </pre>
   *  @param xstart value for xstart field
   */
  @InternalUseOnly
  public void setXStart (double xstart) {
    this.xstart = xstart;
  }

  /** Get the X Start
   *  @return xstart
   */
  @InternalUseOnly
  public double getXStart () {
    return xstart;
  }

  /** Set xdelta
   *  <pre>
   *  1000 - Abscissa interval between samples
   *  2000 - Increment between samples in frame
   *  </pre>
   *  @param xdelta value for xdelta field
   */
  @InternalUseOnly
  public void setXDelta (double xdelta) {
    this.xdelta = xdelta;
  }

  /** Get the X Delta
   *  @return xdelta field
   */
  @InternalUseOnly
  public double getXDelta () {
    return xdelta;
  }

  /** Set X Units
   *  @param xunits X Units
   */
  public void setXUnits (int xunits) {
    this.xunits = xunits;
  }

  /** Get X Units
   *  @return xunits
   */
  @InternalUseOnly
  public int getXUnits () {
    return xunits;
  }

  /** Set the number of data points per frame (row)
   *  @param subsize number of data points per frame
   */
  public void setSubsize (int subsize) {
    this.subsize = subsize;
  }

  /** Get the number of data points per frame (row)
   *  @return the subsize
   */
  @InternalUseOnly
  public int getSubsize () {
    return subsize;
  }

  /** Set the Abscissa (column) start
   *  @param ystart the Type 2000 ystart adjunct value
   */
  @InternalUseOnly
  public void setYStart (double ystart) {
    this.ystart = ystart;
  }

  /** Get the Y Start
   *  @return ystart
   */
  @InternalUseOnly
  public double getYStart () {
    return ystart;
  }

  /** Set Increment between frames
   *  @param ydelta the Type 2000 ydelta adjunct value
   */
  @InternalUseOnly
  public void setYDelta (double ydelta) {
    this.ydelta = ydelta;
  }

  /** Get the Y Delta
   *  @return ydelta field
   */
  @InternalUseOnly
  public double getYDelta () {
    return ydelta;
  }

  /** Set Y Units
   *  @param yunits Y Units
   */
  public void setYUnits (int yunits) {
    this.yunits = yunits;
  }

  /** Get Y Units
   *  @return yunits
   */
  @InternalUseOnly
  public int getYUnits () {
    return yunits;
  }
}
