package nxm.sys.lib;

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.Units;

/** A class to define and handle MatLab Format Data files.

    @author  Jeff Schoen
*/
@ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
public class MatLabFile extends DataFile {

  private static final int
    miINT8=1, miUINT8=2, miINT16=3, miUINT16=4, miINT32=5, miUINT32=6, miSINGLE=7,
    miDOUBLE=9, miINT64=12, miUINT64=13, miMATRIX=14, miCOMPRESSED=15, miUTF8=16, miUTF16=17, miUTF32=18;

  /** Fixed Header length */
  private static final int FHDR = 128;
  /** complex data */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected boolean complex;
  /** compressed data */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected boolean compress;
  /** data length */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected long length;
  /** data offset */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected long dataoff;
  /** compressed size  offset */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected long csizeoff;
  /** tag buffer */
  private byte[] gbuf=null;
  /** ZIP resource */
  private MatLabZipResource zio=null;

  private String version;

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

  /** Override */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public String getVersion() { return version; }

  /** Override */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public String getClassVersion() { return "MATL"; }

  /** Read the header into local buffer, extend if necessary */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected void readHeader () {
    ioh.read (hb,0,FHDR,0L);
    version = Convert.unpackS(hb,0,4);
    if (version.equals("MATL")) convertHeader();
    else throw new MidasException("MatLabFile "+getURL()+" does not have a valid MatLab Header");
  }

  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  protected int writeKeywords () {
    // no keywords in MATLAB files
    return 0;
  }

  private void convertHeader () {
    Table qt = getQualifiers();
    String aname = qt.getS("ARRAY");
    String text = Convert.unpackS(hb,0,124);
    dataoff  = FHDR;
    int hlen = 64;
    gbuf = new byte[hlen];
    int goff = 0;
    int glen = ioh.read(gbuf,0, 64,dataoff);            // read extended header
    Tag tag = new Tag(gbuf,goff); goff+=tag.hbytes;     // tag header
    if (tag.type==miCOMPRESSED) {                       // wrap this array data element with a compressor and reread tag
      compress = true;
      dataoff += tag.hbytes;
      io = zio = new MatLabZipResource(ioh,dataoff,0);        // wrap this array data element with a zipper
      glen = io.read(gbuf,0, hlen,dataoff);             // read compressed header into buffer
      goff = 0;
      tag = new Tag(gbuf,goff); goff+=tag.hbytes;       // reread tag
    }
//for (int i=0; i<hlen/4; i++) System.out.println("OK "+i+" "+Convert.l2x(Convert.unpackL(gbuf,i*4)));
    Tag array = tag;
    if (tag.type == miMATRIX) {                         // read matrix sub-elements
      Tag af = new Tag(gbuf,goff); goff+=af.tbytes;     // array flags
      complex = (af.getX(0)&0x0800) != 0;               // look at complex flag
      Tag ad = new Tag(gbuf,goff); goff+=ad.tbytes;     // array dimensions
      int cols = ad.getL(0), rows=ad.getL(1);
      if ( (cols>1 && rows>1) || (ad.size/getBytes(ad.type)) != 2 )
      throw new MidasException("MatLabFile "+getURL()+" cannot handle multiple dimensioned arrays");
      length = (cols==1)? rows : cols;                  // set length
      Tag an = new Tag(gbuf,goff); goff+=an.tbytes;     // array name
      aname = an.getS(an.size);
      tag = new Tag(gbuf,goff); goff+=tag.hbytes;       // set array tag
    } else {
      length = tag.size / getBytes(tag.type);
    }
    int bps = getBytes(tag.type);
    dataoff += goff;

//System.out.println("UM "+goff+" "+tag.type+" "+tag.size+" "+bps+" "+length);
    if (complex) { // reorder to Midas interleaved format in Memory
      int blength = (int)length*bps;
      byte[] rbuf = new byte[blength];
      byte[] ibuf = new byte[blength];
      byte[] cbuf = new byte[blength*2];
      io.read(rbuf,0,blength,dataoff);
      io.read(ibuf,0,blength,dataoff+blength+tag.hbytes);
      for (int i=0,j=0; i<blength; i+=bps) {
        System.arraycopy(rbuf,i, cbuf,j, bps); j+=bps;
        System.arraycopy(ibuf,i, cbuf,j, bps); j+=bps;
      }
      if (zio==null) io = zio = new MatLabZipResource(io,dataoff,0);
      zio.setBuffer(cbuf,dataoff,blength*2);
    }

    // construct Midas internal header
    for (int i=0; i<512; i++) hb[i]=0;
    initMain();
    switch (tag.type) {
      case miINT8:  case miUINT8:  setFormatType(DataTypes.BYTE); break;
      case miINT16: case miUINT16: setFormatType(DataTypes.INT); break;
      case miINT32: case miUINT32: setFormatType(DataTypes.LONG); break;
      case miINT64: case miUINT64: setFormatType(DataTypes.XLONG); break;
      case miSINGLE:               setFormatType(DataTypes.FLOAT); break;
      case miDOUBLE:               setFormatType(DataTypes.DOUBLE); break;
      case miUTF8:                 setFormatType(DataTypes.ASCII); break;
      default: throw new MidasException("MatLabFile: Unsupported MatLab data type: "+tag.type);
    }
    int spa = complex? 2:1;
    double rate = 1.0e6;
         if (spa==1) setFormatMode(DataTypes.SCALAR);
    else if (spa==2) setFormatMode(DataTypes.COMPLEX);
    else if (spa==3) setFormatMode(DataTypes.VECTOR);
    else throw new MidasException("MatLabFile: Unsupported data vertor dimensions: "+spa);
    if (rate>0) setXDelta( 1.0/rate );
    setXUnits(Units.TIME_S);
    setDataStart((double)(dataoff));
    setDataSize((double)(length*bps*spa));
    keywords.putMain("ARRAY",aname,false);
    int t1 = text.indexOf("Created on: ");
    if (t1>0) keywords.putMain("CREATED",text.substring(t1+12),false);
  }

  private class Tag {

    int type;   // parsed type
    int size;   // parsed size
    int doff;   // offset in buffer where data starts
    int hbytes; // bytes in tag header
    int dbytes; // bytes in tag data
    int tbytes; // total bytes including data portion
    byte[] buf;

    @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
    public Tag (byte[] buf, int off) {
      this.buf = buf;
      type = Convert.unpackL(buf, off);
      if ((type&0xFFFF0000) != 0) { // small form
        size = type>>16;
        type = type&0xFFFF;
        hbytes=4;
        dbytes=(size+3)&0xFFFFFFFC;
      } else {
        size = Convert.unpackL(buf, off+4);
        hbytes=8;
        dbytes=(size+7)&0xFFFFFFF8;
      }
      doff   = off+hbytes;
      tbytes = hbytes + dbytes;
    }
    public long getX(int i) { return Convert.unpackX(buf,doff+i*8); }
    public int getL(int i) { return Convert.unpackL(buf,doff+i*4); }
    public String getS(int i) { return Convert.unpackS(buf,doff,i); }
    public String toString() { return "Tag Type="+type+" Size="+size+" hby="+hbytes+" tby="+tbytes; }
  }

  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public void writeHeader () {
    if (gbuf!=null) return; // already done
    gbuf = new byte[512];
    int goff = createHeader(gbuf);
    setDataStart((double)goff);
    io.write (gbuf,0,goff,0L);
  }

  /** Write Matlab header
      @param buf packed byte array
      @return offset
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public int createHeader (byte[] buf) {

    int array_type = getArrayType(getFormatType());
    int gstep = getBytes(array_type);
    int bps = getBPS();
    int bpe = (int)getBPE();
    int blength = (int)getSize()*bpe;
    int length = blength/gstep;
    int off = -1;
    Table qt = getQualifiers();
    compress = qt.getState("CMP");
    complex = (getSPA()==2);
    if (complex) length/=2;
    // build the descriptive text
    Date date = new Date();
    String ts = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy").format(new Timestamp(date.getTime()));
    String ostring = getOsString();
    String desc = String.format("MATLAB 5.0 MAT-file, Platform: %s, Created on: %s", ostring, ts);
    // copy to header
    System.arraycopy(desc.getBytes(),0, buf,0, desc.length());
    // fill the remainder of the descriptive text with spaces
    for ( int i=desc.length(); i<124; i++ ) buf[i] = (byte) 32;
    // version
    boolean be = (Shell.rep==IEEE);
    buf[124] = (byte) 0;
    buf[125] = (byte) 1;
    // Endian, assume little endian (IM -> 73,77), big endian would be (MI -> 77,73)
    buf[126] = (byte) (be? 77:73);
    buf[127] = (byte) (be? 73:77);
    off = FHDR;
    if (compress) {
      // set the data type
      Convert.packL(buf,off, miCOMPRESSED); off+=4;
      csizeoff=off; // note: bytes will have to be set later (when it is known)
      Convert.packL(buf,off, 0); off+=4; // size placeholder
      dataoff = off;
      io = zio = new MatLabZipResource(ioh,dataoff,0);  // wrap this array data element with a zipper
    }
    // array type
    Convert.packL(buf,off, miMATRIX); off+=4;   // type
    int tag_size = complex ? (blength+56) : (blength+48);
    Convert.packL(buf, off, tag_size); off+=4;  // size
    // array flags
    Convert.packL(buf, off, miUINT32); off+=4;  // type
    Convert.packL(buf, off, 8); off+=4;         // size
    int class_type = getClassType(complex, array_type);
    Convert.packL(buf, off, class_type); off+=4;
    Convert.packL(buf, off, 0); off+=4;
    // dimensions
    Convert.packL(buf, off, miINT32); off+=4;   // type
    Convert.packL(buf, off, 8); off+=4;         // size
    Convert.packL(buf, off, 1); off+=4;         // rows
    Convert.packL(buf, off, length); off+=4;    // cols
    // array name
    buf[off++] = (byte) 1;
    buf[off++] = (byte) 0;
    buf[off++] = (byte) 4;
    buf[off++] = (byte) 0;
    Convert.packS(buf, off, 4, "data"); off+=4;
    // write the array type
    Convert.packL(buf, off,  array_type); off+=4;
    Convert.packL(buf, off, gstep*length); off+=4;
    dataoff = off;

    // reorder to Midas interleaved format in Memory
    if (complex) {
      byte[] cbuf = new byte[blength];
      if (zio==null) io = zio = new MatLabZipResource(io,dataoff,0);
      zio.setBuffer(cbuf,dataoff,blength);
    }
    return off;
  }

  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public synchronized void close() {
    if (isOutput && complex) {
      int gstep = getBPS();
      int blength = (int)getDataSize()/2;
      byte[] rbuf = new byte[blength];
      byte[] ibuf = new byte[blength];
      byte[] cbuf = zio.getBuffer();
      for (int i=0,j=0; i<blength; i+=gstep) {
        System.arraycopy(cbuf,j, rbuf,i, gstep); j+=gstep;
        System.arraycopy(cbuf,j, ibuf,i, gstep); j+=gstep;
      }
      io = ioh;
      io.write(rbuf,0,blength,dataoff);
      io.write(gbuf,(int)dataoff-8,8,dataoff+blength);
      io.write(ibuf,0,blength,dataoff+blength+8);
    }
    if (isOutput && compress) {
      int csize = (int)(zio.getCompressedOffset()-csizeoff+4);
      Convert.packL(gbuf, (int)csizeoff, csize);
      ioh.write(gbuf,(int)csizeoff,4,csizeoff);
    }
    super.close();
  }

  private int getBytes (int type) {
    switch ( type ) {
      case miINT8:
      case miUINT8:
      case miUTF8:
        return 1;
      case miINT16:
      case miUINT16:
        return 2;
      case miINT32:
      case miUINT32:
      case miSINGLE:
        return 4;
      case miDOUBLE:
      case miINT64:
      case miUINT64:
        return 8;
      default:
        System.out.printf("Unhandled Data Type!\n");
        return -1;
    }
  }

  /** get the Matlab OS String
      @return Matlab OS String
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public String getOsString () {
    String osname = System.getProperty("os.name").toLowerCase();
    boolean x86 = ( System.getProperty("os.arch").indexOf("64") < 0 ) ? true: false;

    if ( osname.contains("linux") )
      return x86 ? "GLNX86" : "GLNXA64";
    else if ( osname.contains("win") )
      return x86 ? "PCWIN" : "PCWIN64";
    else if ( osname.contains("mac") )
      return "MACI";
    // return linux for anything else
    return x86 ? "GLNX86" : "GLNXA64";
  }

  private int getClassType ( boolean complex, int dtype ) {
    switch ( dtype ) {
      case miINT8:
        return complex ? 2056 : 8;
      case miINT16:
        return complex ? 2058 : 10;
      case miINT32:
        return complex ? 2060 : 12;
      case miINT64:
        return complex ? 2062 : 14;
      case miDOUBLE:
        return complex ? 2054 : 6;
      default:
        return complex ? 2054 : 6;
    }
  }

  /** Get array type
      @param type data type
      @return corresponding array type
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public int getArrayType ( byte type ) {
    int at = -1;
    switch (type) {
      case DataFile.BYTE: at=miINT8; break;
      case DataFile.INT:  at=miINT16; break;
      case DataFile.LONG: at=miINT32; break;
      case DataFile.XLONG: at=miINT64; break;
      case DataFile.DOUBLE: at=miDOUBLE; break;
      case DataFile.FLOAT: at=miSINGLE; break;
      case DataFile.ASCII: at=miUTF8; break;
    }
    return at;
  }

}
