/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.virbo.idlsupport;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.LinkedHashMap;
import java.util.Map.Entry;

/**
 * write data to IDL Save File
 * http://www.physics.wisc.edu/~craigm/idl/savefmt/node20.html
 * @author jbf
 */
public final class WriteIDLSav {
    public static final int DATATYPE_DOUBLE = 5;
    public static final int RECTYPE_ENDMARKER = 6;
    public static final int RECTYPE_TIMESTAMP = 10;
    public static final int RECTYPE_VARIABLE = 2;
    public static final int RECTYPE_VERSION = 14;
    public static final int VARFLAG_ARRAY = 4;

    private String nameFor( int type ) {
        if ( type==RECTYPE_VARIABLE ) {
                return "VARIABLE";
        } else if ( type==RECTYPE_TIMESTAMP ) {
          return "TIMESTAMP";
        } else if ( type==RECTYPE_VERSION) {
          return "VERSION";
        } else if ( type==RECTYPE_ENDMARKER ) {
          return "ENDMARKER";
        } else {
            return ""+type;
        }
    }
    
    private ByteBuffer timestamp() {
        //ByteBuffer date= writeString( "Sat Feb 11 08:43:55 2012" ); //Calendar.getInstance().toString() );
        //ByteBuffer user= writeString( "jbf" );
        //ByteBuffer host= writeString( "Jeremy-Fadens-MacBook-Air.local" ); //TODO: get these from java props
        //ByteBuffer date= writeString( "Mon Feb 20 06:49:42 2012" ); //Calendar.getInstance().toString() );
        //ByteBuffer user= writeString( "jbf" );
        //ByteBuffer host= writeString( "spot5" ); //TODO: get these from java props

        ByteBuffer date= writeString( new java.util.Date().toString() ); //Calendar.getInstance().toString() );
        ByteBuffer user= writeString( System.getProperty("user.name") );

        String shost;
        try {
            shost=  InetAddress.getLocalHost().getHostName();
        } catch ( UnknownHostException ex ) {
            shost= "localhost"; // this shouldn't happen
        }
        ByteBuffer host= writeString( shost );

        ByteBuffer result= ByteBuffer.allocateDirect( 257*4 + date.limit() + user.limit() + host.limit() );
        for ( int i=0; i<257*4; i++ ) {
            result.put((byte)0);
        }
        result.put(date); //System.err.println(4+result.position());
        result.put(user); //System.err.println(4+result.position());
        result.put(host); //System.err.println(4+result.position());
        result.flip();
        return result;
    }

    private ByteBuffer version() {
        ByteBuffer format= ByteBuffer.allocateDirect(4);
        format.order(ByteOrder.BIG_ENDIAN);
        format.putInt(9);
        format.flip();
        ByteBuffer arch= writeString( System.getProperty("os.arch") );
        ByteBuffer os= writeString( System.getProperty("os.name"));
        ByteBuffer release= writeString("(Autoplot)");
        
        ByteBuffer result= ByteBuffer.allocateDirect( 4 + 4 + arch.limit() + os.limit() + release.limit() );
        result.putInt( 0 );
        result.put( format );
        result.put( arch );
        result.put( os );
        result.put( release );
        result.flip();
        return result;
    }

    static ByteBuffer getBytesStr( String s ) {
        return ByteBuffer.wrap(s.getBytes());
    }

    static ByteBuffer getBytesByte( byte b ) {
        return ByteBuffer.wrap(new byte[]{b});
    }

    private ByteBuffer writeString( String s ) {
        int len= 4 * (int)Math.ceil( ( s.length()+4 ) / 4. );
        ByteBuffer result= ByteBuffer.allocateDirect(len);
        result.order( ByteOrder.BIG_ENDIAN );
        result.putInt(s.length());
        try {
            result.put(s.getBytes("US-ASCII"));
        } catch ( UnsupportedEncodingException ex ) { // this doesn't happen with JavaSE
            throw new RuntimeException(ex);
        } 
        for ( int i=result.position(); i<result.limit(); i++  ) {
            result.put((byte)0);
        }
        result.flip();
        return result;
    }

    private ByteBuffer writeArrayDesc( Object data ) {
        int nmax= 8; // ?? see python code.
        int capacity= ( 8 + nmax ) * 4; //TODO: 1D
        int eleLen= 8; // bytes
        int ndims= 1;

        ByteBuffer result= ByteBuffer.allocateDirect( capacity );
        result.order(ByteOrder.BIG_ENDIAN);
        result.putInt( 8 ); // normal array.  There's a 64-bit array read as well that is not implemented.
        result.putInt( eleLen ); // bytes per element TODO: only 8 byte.
        result.putInt( Array.getLength(data)*eleLen ); //TODO: 1D
        result.putInt( Array.getLength(data) );
        result.putInt( ndims );
        result.putInt( 0 );  // should be 1*256+83
        result.putInt( 0 );  // should be 1*256+83

        result.putInt( nmax );

        for ( int i=0; i<nmax; i++ ) {
            result.putInt( i==0 ? Array.getLength(data) : 1 ); //TODO: guess
        }

        result.flip();
        return result;
    }

    private int dataTypeCode( Object data ) {
        if ( data.getClass()==Short.class ) {
            return 2;
        } else if ( data.getClass()==Integer.class ) {
            return 3;
        } else if ( data.getClass()==Float.class ) {
            return 4;
        } else if ( data.getClass()==Double.class ) {
            return 5;
        } else {
            throw new IllegalArgumentException("unsupported type: "+data.getClass() );
        }
    }

    private ByteBuffer writeScalarDesc( Object data ) {
        
        ByteBuffer result= ByteBuffer.allocateDirect( 8 );
        result.order(ByteOrder.BIG_ENDIAN);
        result.putInt( dataTypeCode(data) ); // 2=16bit 3=32bit int 5=float,etc.
        result.putInt( 0);  // bytes per element TODO: only 8 byte.

        result.flip();
        return result;
    }

    private ByteBuffer writeTypeDesc( Object data ) {
        if ( data.getClass().isArray() ) {
            return writeArrayDesc( data );
        } else if ( data.getClass()==Short.class ) {
            return writeScalarDesc( data );
        } else if ( data.getClass()==Integer.class ) {
            return writeScalarDesc( data );
        } else if ( data.getClass()==Float.class ) {
            return writeScalarDesc( data );
        } else if ( data.getClass()==Double.class ) {
            return writeScalarDesc( data );
        } else {
            throw new RuntimeException("not implemented");
        }
    }

    private ByteBuffer writeDoubleArray( double[] data ) {
        ByteBuffer buf= ByteBuffer.allocateDirect( data.length*8 );
        buf.order(ByteOrder.BIG_ENDIAN);
        for ( int i=0; i<data.length; i++ ) {
            buf.putDouble(data[i]);
        }
        buf.flip();
        return buf;
    }

    private ByteBuffer writeLongArray( long[] data ) {
        ByteBuffer buf= ByteBuffer.allocateDirect( data.length*8 );
        buf.order(ByteOrder.BIG_ENDIAN);
        for ( int i=0; i<data.length; i++ ) {
            buf.putLong(data[i]);
        }
        buf.flip();
        return buf;
    }
    
    private ByteBuffer writeShort( short data ) {
        ByteBuffer buf= ByteBuffer.allocateDirect( 4 );
        buf.order(ByteOrder.BIG_ENDIAN);
        buf.putShort((short)0);
        buf.putShort(data);
        buf.flip();
        return buf;
    }    

    private ByteBuffer writeTypeDescArray( Object data ) {

        ByteBuffer arrayDesc= writeArrayDesc(data);
        ByteBuffer result= ByteBuffer.allocateDirect( 8 + arrayDesc.limit() );
        result.order(ByteOrder.BIG_ENDIAN);
        result.putInt( DATATYPE_DOUBLE );
        result.putInt( VARFLAG_ARRAY);
        result.put(arrayDesc);

        result.flip();
        return result;
    }

    /**
     * format the data
     * @param name an IDL identifier.
     * @param data a supported type.
     * @param pos
     * @return the encoding of variable into a reset ByteBuffer.
     */
    private ByteBuffer variable( String name, Object data, long pos ) {

        checkVariableType(name, data);

        ByteBuffer nameBuf= writeString( name.toUpperCase() );
        ByteBuffer typedesc= writeTypeDesc( data );

        ByteBuffer varData;
        if ( data.getClass().isArray() && data.getClass().getComponentType()==double.class ) {
            varData= writeDoubleArray( (double[])data );
        } else if (data.getClass().isArray() && data.getClass().getComponentType() == long.class) {
            varData= writeLongArray( (long[])data );
        } else if ( data.getClass()==Short.class ) {
            varData= writeShort( (Short)data );
        } else {
            throw new RuntimeException("not supported "+data.getClass());
        }

        ByteBuffer result= ByteBuffer.allocateDirect( 4 + nameBuf.limit() + 8 + typedesc.limit() + 4 + varData.limit() );
        result.order(ByteOrder.BIG_ENDIAN);
        result.put(ByteBuffer.allocateDirect( 4 ) );
        result.put(nameBuf);
        //result.put(ByteBuffer.allocateDirect( 8 ) );
        result.putInt(5);   // why?
        result.putInt(20);  // why?
        result.put(typedesc);
        result.putInt(7);
        result.put(varData);

        result.flip();
        return result;
    }

    private ByteBuffer endMarker() {
        ByteBuffer result= ByteBuffer.allocate(4);
        for ( int i=0; i<4; i++ ) result.put((byte)0);
        result.flip();
        return result;
    }


     private int writeRecord( WritableByteChannel ch, int recType, ByteBuffer buf, int pos ) throws IOException {

        int len= (int)( 4 * Math.ceil( ( buf.limit()+12. ) / 4 ) );

        ByteBuffer rec= ByteBuffer.allocateDirect( len );
        rec.order( ByteOrder.BIG_ENDIAN );

        rec.putInt( recType );

        if ( recType==RECTYPE_ENDMARKER ) {
            rec.putInt( 0 );
        } else {
            rec.putInt( pos + len );
        }
        rec.putInt( 0 ); // skip 4 bytes.
        rec.put(buf);

        int padBytes=0;
        for ( int i=rec.position(); i<rec.limit(); i++ ) {
            rec.put((byte)0);  // pad out to 4 byte boundary
            padBytes++;
        }

        rec.flip();

        //String stype= nameFor( recType ) ;


        //System.err.println( "writing "+stype + " to buffer bytes "+ pos + " to " + ( pos + rec.limit() ) + " " + padBytes );
        //System.err.printf( "%d %d %s\n", pos, pos +rec.limit(), stype );
        ch.write(rec);

        pos+= rec.limit();
        return pos;
    }

    private LinkedHashMap<String,Object> variables= new LinkedHashMap();

    public void checkVariableType( String name, Object data ) {
        Class c= data.getClass();
        if ( !( c.isArray() && ( c.getComponentType()==double.class || c.getComponentType()==long.class ) ) && c!=Short.class ) {
            throw new IllegalArgumentException("\"" + name + "\" is unsupported data type: "+data.getClass() );
        }
    }

    public void addVariable( String name, Object data ) {
        checkVariableType( name, data );
        variables.put(name, data);
    }

    public void write( OutputStream out ) throws IOException {

        WritableByteChannel ch= Channels.newChannel(out);

        ch.write(getBytesStr("SR"));

        ch.write(getBytesByte((byte) 0));
        ch.write(getBytesByte((byte) 4));

        int pos= 4;
        pos= writeRecord( ch, RECTYPE_TIMESTAMP, timestamp(), pos );
        pos= writeRecord( ch, RECTYPE_VERSION, version(), pos );

        for ( Entry<String,Object> var: variables.entrySet() ) {
            pos= writeRecord( ch, RECTYPE_VARIABLE, variable( var.getKey(), var.getValue(), pos ), pos );
        }

        pos= writeRecord( ch, RECTYPE_ENDMARKER, endMarker(), pos );
        ch.close();

    }

    public static void main(String[] args) throws FileNotFoundException, IOException {

        FileOutputStream fos = new FileOutputStream(new File("/tmp/test.autoplot.idlsav"));
        
        WriteIDLSav widls= new WriteIDLSav();
        widls.addVariable( "myvar", new double[] { 120,100,120,45,46,47,48,49,120,100,120 } );
        widls.addVariable( "second", new double[] { -1,-1,-2,-3,-3,4,5,6,7,7,8,9,9,10 } );
        widls.addVariable( "mylong", new long[] { -1, 100000, 100000000000L } );

        //widls.addVariable( "oneval", 19.95 );
        widls.write(fos);

        fos.close();
    }
}