package nxm.sys.lib;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import nxm.sys.inc.InternalUseOnly;

/**
 * A class that will parse and maintain all the header information from a file.
 * Using this class can result in significant time savings over a DataFile open()
 * @author Kevin Esterheld
 * @since NeXtMidas 3.9.2
 */
@InternalUseOnly
public class DataFileHeader {

  private DataFile df;

  private byte[] hbuf;

  // header values
  private Midas M;
  private Keywords extendedHeaderKeywords;
  private Keywords mainKeywords;
  private Table headerValues;
  private Table headKeyTable;
  private Table headerValuesCS;
  private Table headKeyTableCS;
  private String headerRep;
  private String dataRep;
  private int type;
  private String format;
  private byte formatMode;
  private byte formatType;
  private double xStart;
  private double xDelta;
  private double yStart;
  private double yDelta;
  private short flagMask;
  private double dataSize;
  private double dataStart;
  private int extSize;
  private int extStart;
  private String version = "1.1"; //should be 1.2 if has J DataType
  private int pipeId;
  private int detached;
  private int protectedFromOverwrite;
  private double timeCode;
  private int keyLength;
  private short inlet;
  private short outlet;
  private int pipeLoc;
  private int pipeSize;
  private double inByte;
  private double outByte;
  InputStream fis;
  IOResource res;
  PipeResource resPipe;
  private boolean ableToParseHeader = false;

  /** DataFileHeader Constructor
      @param df DataFile to be referenced
   */
  public DataFileHeader (DataFile df) {
    this.df = df;
    this.M = df.M;
    readHeader();
  }

  /** Get the extended header keywords
   * @return the extended header keywords
   */
  @InternalUseOnly
  public Keywords getExtendedHeaderKeywords () {
    if(res instanceof PipeResource) {
      return resPipe.pipe.getDataFile().getKeywordsObject();
    }
    if (extendedHeaderKeywords == null && ableToParseHeader) {
      long off = ((long) extStart) * DataFile.HEADER_SIZE;
      if (!res.isOpen) res.open();
      // extended keywords
      // get the length of the keywords
      int len = (extSize + 511) / DataFile.HEADER_SIZE * DataFile.HEADER_SIZE;
      // create keywords buffer
      byte[] bufWithKeys = new byte[len];
      // read the keywords into the buffer
      int bytes = res.read(bufWithKeys, 0, extSize, off);
      // create and init the keywords with bufwithkeys
      extendedHeaderKeywords = new Keywords();
      extendedHeaderKeywords.init(bufWithKeys, bytes, hbuf[DataFile.OFF_HEADREP]);
      res.close();
    }
    return extendedHeaderKeywords;
  }

  /** Method to grab only the header information from a data file.
   *  This is helpful when it is necessary to get the header
   *  information often from the file.
   */
  private void readHeader () {
    try {


      res = M.io.findResource(df.getFileName(), -1, "", null, false);

      hbuf = new byte[DataFile.HEADER_SIZE];

      if(res instanceof PipeResource) {
        resPipe = (PipeResource) res;
        res.exists();
        resPipe.df = df;
        Convert.packD(hbuf, DataFile.OFF_DATASTART,0);
        Convert.packL(hbuf, DataFile.OFF_PIPE,0);
        resPipe.pipeSize = 131072;
        resPipe.open();
        resPipe.readHeader(hbuf);
      }
      else {

        res.open();
        fis = res.getInputStream();
        fis.read(hbuf);
      }

      version = Convert.unpackS(hbuf, DataFile.OFF_VERSION, DataFile.CHARS_VERSION);
      headerRep = Convert.unpackS(hbuf, DataFile.OFF_HEADREP, DataFile.CHARS_REP);
      dataRep = Convert.unpackS(hbuf, DataFile.OFF_DATAREP, DataFile.CHARS_REP);
      detached = Convert.unpackL(hbuf, DataFile.OFF_DETACHED);
      protectedFromOverwrite = Convert.unpackL(hbuf, DataFile.OFF_PROTECTED);
      pipeId = Convert.unpackL(hbuf, DataFile.OFF_PIPE);
      extStart = Convert.unpackL(hbuf, DataFile.OFF_EXTSTART);
      extSize = Convert.unpackL(hbuf, DataFile.OFF_EXTSIZE);
      dataStart = Convert.unpackD(hbuf, DataFile.OFF_DATASTART);
      dataSize = Convert.unpackD(hbuf, DataFile.OFF_DATASIZE);
      type = Convert.unpackL(hbuf, DataFile.OFF_TYPE);
      format = Convert.unpackS(hbuf, DataFile.OFF_FORMAT, DataFile.CHARS_FORMAT);
      formatMode = Convert.unpackB(hbuf, DataFile.OFF_FORMAT);
      formatType = Convert.unpackB(hbuf, DataFile.OFF_FORMAT + 1);
      flagMask = Convert.unpackI(hbuf, DataFile.OFF_FLAGMASK);
      timeCode = Convert.unpackD(hbuf, DataFile.OFF_TIMECODE);
      inlet = Convert.unpackI(hbuf, DataFile.OFF_INLET);
      outlet = Convert.unpackI(hbuf, DataFile.OFF_OUTLETS);
      pipeLoc = Convert.unpackL(hbuf, DataFile.OFF_PIPELOC);
      pipeSize = Convert.unpackL(hbuf, DataFile.OFF_PIPESIZE);
      inByte = Convert.unpackD(hbuf, DataFile.OFF_INBYTE);
      outByte = Convert.unpackD(hbuf, DataFile.OFF_OUTBYTE);
      // parse outbytes? - 96 - 159
      keyLength = Convert.unpackL(hbuf, DataFile.OFF_KEYLGTH);
      xStart = Convert.unpackD(hbuf, DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_XSTART_OFF);
      xDelta = Convert.unpackD(hbuf, DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_XDELTA_OFF);
      if (type >= 2000) {
        yStart = Convert.unpackD(hbuf, DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_YSTART_OFF);
        yDelta = Convert.unpackD(hbuf, DataFile.OFF_ADJUNCT + DataFile.ADJUNCT_YDELTA_OFF);
      }
      ableToParseHeader = true;
    } catch (Exception e) {
      Shell.getMidasContext().warning("Unable to parse the header from " + df.getFileName());
    }
    finally {
      if (res!=null) res.close();
      if (fis!=null) {
        try {
          fis.close();
        }
        catch (IOException e) {
          // should already have given warning if things are bad
        }
      }
    }
  }

  /**
   * Add add a header keyword to the file.
   * This will allow the adding of custom types and values.
   * @param type The char type to identify the value type
   * @param val the object to be added.
   * @param name The key name of the keyword.
   * @since 3.9.3
   */
  @InternalUseOnly
  public void addToFile(char type, Object val, String name) {
    FileInputStream fis;
    byte[] buf;
    File file = new File(df.filename);

    try {
      fis = new FileInputStream(file);
      buf = new byte[(int) file.length()];
      fis.read(buf);
      fis.close();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      return;
    }
    byte[] data = packTestDataToBuffer(type,val);
    int off = ((extStart*512) + extSize);
    if(off==0) off = (int) (dataStart+dataSize);
    byte ltag = (byte)name.length();
    int len = data.length;
    int length = (len+ltag+15)/8*8;
    short lext = (short)(length - len);
    if (buf.length < (length+off)) {
      int size = buf.length;
      byte[] bufo = buf;
      buf = new byte[length+off];
      System.arraycopy(bufo, 0, buf, 0, size);
    }
    Convert.packL(buf, off+0, length);
    Convert.packI(buf, off+4, lext);
    buf[off+6] = ltag;
    buf[off+7] = (byte) type;
    System.arraycopy (data,0, buf,off+Keywords.HEADER_LEN, len);
    for (int i=0,j=off+Keywords.HEADER_LEN+len; i<ltag;) buf[j++] = (byte)name.charAt(i++); // set tag name
    Convert.packL(buf, DataFile.OFF_EXTSIZE, extSize+length);
    Convert.packL(buf, DataFile.OFF_EXTSTART, off/512);

    // update changed fields
    extSize += length;
    try (FileOutputStream fos = new FileOutputStream(file); BufferedOutputStream bos = new BufferedOutputStream(fos);){
      bos.write(buf);
    } catch (IOException e) {
      e.printStackTrace();
    }
    readHeader();
  }

  /** pack test data into buffer - for testing only
   *
   *  @param type type of data 'U' or 'V'
   *  @param val  value
   *  @return byte buffer
   */
  @InternalUseOnly
  public byte[] packTestDataToBuffer(char type, Object val) {
    byte[] res = new byte[type=='U' ? 2 : 4];
    if(type=='U') {
      Convert.packI(res, 0,(short) val);
    }
    else {
      Convert.packL(res, 0,(int) val);
    }

    return res;
  }

  /** Get the Header Buffer
   *  @return the Header Buffer
   */
  @InternalUseOnly
  public byte[] getHeaderbuf () {
    return hbuf;
  }

  /** Get the Main Keywords
   *  @return the Main Keywords
   */
  @InternalUseOnly
  public Keywords getMainKeywords () {
    if(res instanceof PipeResource) return ((PipeResource)res).pipe.getDataFile().getKeywordsObject();
    if (mainKeywords == null && ableToParseHeader) {
      if (!res.isOpen) res.open();
      byte[] mainKeysBuf = new byte[keyLength];
      res.read(mainKeysBuf, 0, keyLength, DataFile.OFF_KEYWORDS);
      String keytemp = new String(mainKeysBuf);
      mainKeywords = new Keywords(df); // NXM-4004 need to use this constructor, so MAIN keyword buf initialized
      mainKeywords.init(null, 0, hbuf[DataFile.OFF_HEADREP]);
      String[] keyValueArray = keyValueArrayFromNullSeparatedKeys(keytemp);
      if (keyValueArray == null) return mainKeywords;
      for (int i = 0; i < keyValueArray.length - 1; i += 2) {
        mainKeywords.put(keyValueArray[i], keyValueArray[i + 1]);
      }
    }
    return mainKeywords;
  }

  /** Main Header Keyword entries are separated by ASCII null char
   *  (i.e. char(0)). Replace these with '=' and strip off last one,
   *  then split by '=' to give a key, value, key, value ... array.<br>
   *  @param nullSeparatedKeys String with null separated key=value pairs
   *  @return String[] with key, value, key, value ...
   */
  private String[] keyValueArrayFromNullSeparatedKeys (String nullSeparatedKeys) {
    nullSeparatedKeys = nullSeparatedKeys.replaceAll(String.valueOf((char)0), "=");
    int lastIndex = nullSeparatedKeys.length()-1;
    if (lastIndex==-1) return null;
    if (nullSeparatedKeys.charAt(lastIndex)=='=') {
      nullSeparatedKeys = nullSeparatedKeys.substring(0, lastIndex);
    }
    return nullSeparatedKeys.split("=");
  }

  /** Get the header representation
   *  @return the header representation
   */
  @InternalUseOnly
  public String getHeaderRep () {
    return headerRep;
  }

  /** Get the data representation
   *  @return the data representation
   */
  @InternalUseOnly
  public String getDataRep () {
    return dataRep;
  }

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

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

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

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

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

  /** Get the X Delta
   *  @return the x Delta
   */
  @InternalUseOnly
  public double getxDelta () {
    return xDelta;
  }

  /** Get the Y Start
   *  @return the y Start
   */
  @InternalUseOnly
  public double getyStart () {
    return yStart;
  }

  /** Get the Y Delta
   *  @return the y Delta
   */
  @InternalUseOnly
  public double getyDelta () {
    return yDelta;
  }

  /** Get the flagmask used in X-Midas pipes
   *  @return the flagmask (Not Applicable to NextMidas)
   */
  @InternalUseOnly
  public short getFlagMask () {
    return flagMask;
  }

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

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

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

  /** Get extended header start
   * @return the extended header start
   */
  @InternalUseOnly
  public int getExtStart () {
    return extStart;
  }

  /** Get the Midas file type (a.k.a version)
   *  @return the version (should be BLUE)
   */
  @InternalUseOnly
  public String getVersion () {
    return version;
  }

  /** Get pipe field (used only in X-Midas pipes and not applicable to BLUE)
   *  @return the pipe field
   */
  @InternalUseOnly
  public int getPipeId () {
    return pipeId;
  }

  /** Get if the data itself exists in a different file
   *  @return true if detached
   */
  @InternalUseOnly
  public int getDetached () {
    return detached;
  }

  /** Get if the file is protected from overwrite
   *  @return true if the file is protected from overwrite
   */
  @InternalUseOnly
  public int getProtectedFromOverwrite () {
    return protectedFromOverwrite;
  }

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

  /** Get the length of keyword string
   *  @return the length of keyword string
   */
  @InternalUseOnly
  public int getKeyLength () {
    return keyLength;
  }

  /** Get the inlet owner
   *  @return the inlet owner
   */
  @InternalUseOnly
  public short getInlet () {
    return inlet;
  }

  /** Get the number of outlets
   *  @return the number of outlets
   */
  @InternalUseOnly
  public short getOutlet () {
    return outlet;
  }

  /** Get the pipe location
   *  @return the pipe location
   */
  @InternalUseOnly
  public int getPipeLoc () {
    return pipeLoc;
  }

  /** Get the pipe size
   *  @return the pipe size
   */
  @InternalUseOnly
  public int getPipeSize () {
    return pipeSize;
  }

  /** Get next input byte
   *  @return Next input byte
   */
  @InternalUseOnly
  public double getInByte () {
    return inByte;
  }

  /** Get next out byte
   *  @return Next out byte
   */
  @InternalUseOnly
  public double getOutByte () {
    return outByte;
  }

  /** Get the header buffer
   *  @return the header buffer
   */
  @InternalUseOnly
  public byte[] getHbuf () {
    return hbuf;
  }

  /** Get the IOResource
   *  @return the IOResorce */
  @InternalUseOnly
  public IOResource getRes () {
    return res;
  }

  /** Gets if the header was able to be parsed
   *  @return true if was able to parse the header
   */
  @InternalUseOnly
  public boolean isAbleToParseHeader () {
    return ableToParseHeader;
  }

  Keywords getKeywords (String type) {
    return (type.toUpperCase().equals("MAIN")) ? getMainKeywords() : getExtendedHeaderKeywords();
  }


  Object getHeaderField (String fieldName) {
    String ucFieldName = fieldName.toUpperCase();
    switch (ucFieldName) {
      case "TYPE"       : return getType();
      case "SIZE"       : return getDataSize();
      case "DATASTART"  : return getDataStart();
      case "DATAREP"    : return getDataRep();
      case "FORMAT"     : return getFormat();
      case "XSTART"     : return getyStart();
      case "XDELTA"     : return getxDelta();
      case "YSTART"     : return getyStart();
      case "YDELTA"     : return getxDelta();
      case "TIMECODE"   : return getTimeCode();
      default: {
        M.warning("DataFieldHeader.getHeaderField() does not currently have support for "+ucFieldName);
        return "";
      }
    }
  }

  /** Get Header Values in a case sensitive Table
   *  This is designed to be an easy to read output format
   *  @return Header Values in Table form
   */
  @InternalUseOnly
  public Table getHeaderValuesCS () {
    if (headerValuesCS == null) {
      headerValuesCS = new Table("SLHM","AutoReadPrev|CaseSensitive");
      headerValuesCS.put("Type", getType());
      headerValuesCS.put("Size", getDataSize());
//      headerValues.put("DATASTART", getDataStart());
      headerValuesCS.put("DataRep", getDataRep());
      headerValuesCS.put("Format", getFormat());
      headerValuesCS.put("XStart", getyStart());
      headerValuesCS.put("XDelta",getxDelta());
      headerValuesCS.put("YStart" , getyStart());
      headerValuesCS.put("YDelta", getyDelta());
      headerValuesCS.put("Timecode", getTimeCode());
    }
    return headerValuesCS;
  }

  /** Get Header and Keyword Values in a case sensitive Table
   *  This is designed to be an easy to read output format
   *  @return Header and Keyword Values in Table form
   */
  @InternalUseOnly
  public Table getHeaderAndKeywordsAsTableCS () {
    if (headKeyTableCS == null) {
      headKeyTableCS = new Table();
      headKeyTableCS.put("FileKeywordsMain", getMainKeywords().makeTable());
      headKeyTableCS.put("FileKeywordsExt", getExtendedHeaderKeywords().makeTable());
      headKeyTableCS.put("FileHeader", getHeaderValuesCS());
    }
    return headKeyTableCS;
  }

  /** Get Header Values in Table form
   *  This is designed for functional use
   *  @return Header Values in Table form
   */
  @InternalUseOnly
  public Table getHeaderValues () {
    if (headerValues == null) {
      headerValues = new Table();
      headerValues.put("TYPE", getType());
      headerValues.put("SIZE", getDataSize());
      headerValues.put("DATASTART", getDataStart());
      headerValues.put("DATAREP", getDataRep());
      headerValues.put("FORMAT", getFormat());
      headerValues.put("XSTART", getyStart());
      headerValues.put("XDELTA",getxDelta());
      headerValues.put("YSTART" , getyStart());
      headerValues.put("YDELTA", getxDelta());
      headerValues.put("TIMECODE", getTimeCode());
    }
    return headerValues;
  }

  /** Get Header and Keyword Values in Table form
   *  Keyword entries match df.getKeywordsObject().makeTable(false)
   *  This is meant for functional use
   *  @return Header and Keyword Values in Table form
   */
  @InternalUseOnly
  public Table getHeaderAndKeywordsAsTable () {
    if (headKeyTable == null) {
      headKeyTable = new Table();
      headKeyTable.put("MAIN", getMainKeywords().makeTable());
      headKeyTable.put("EXTENDED", getExtendedHeaderKeywords().makeTable());
      headKeyTable.put("HEADER", getHeaderValues());
    }
    return headKeyTable;
  }

}
