package org.autoplot.idlsupport;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Read data from IDL Save Files.  This was written using
 * http://www.physics.wisc.edu/~craigm/idl/savefmt/node20.html
 * https://cow.physics.wisc.edu/~craigm/idl/savefmt.pdf
 * and https://github.com/scipy/scipy/blob/master/scipy/io/idl.py
 * for reference, and with no involvement from individuals at
 * Harris Geospacial.  No warranties are implied and this must
 * be used at your own risk.
 * 
 * <pre>{@code
 * from  org.autoplot.idlsupport import ReadIDLSav
 * reader= ReadIDLSav()
 * aFile= File('/tmp/aDataFile.sav')
 * inChannel = aFile.getChannel
 * fileSize = inChannel.size()
 * buffer = ByteBuffer.allocate( inChannel.size() )
 * bytesRead= 0;
 * while ( bytesRead<fileSize ) :
       bytesRead+= inChannel.read(buffer)
 * v= reader.readVar( buffer, 'avar' )
 * }</pre>
 * @author jbf
 */
public class ReadIDLSav {
        
    private static final Logger logger= Logger.getLogger("apdss.idlsav");
            
    private static final int RECTYPE_VARIABLE = 2;
    private static final int RECTYPE_ENDMARKER = 6;
    private static final int RECTYPE_TIMESTAMP = 10;
    private static final int RECTYPE_VERSION = 14;
    private static final int RECTYPE_PROMOTE64 = 17;
    
    private static final int VARFLAG_ARRAY = 0x04;
    private static final int VARFLAG_STRUCT = 0x20;
    
    /**
     * return the next record buffer, or returns null at the end.
     * @param ch the bytebuffer
     * @param pos the position.
     * @return the record, including the twelve bytes at the beginning
     * @throws IOException 
     */    
    private ByteBuffer readRecord( ByteBuffer ch, int pos ) throws IOException {

        ch.order( ByteOrder.BIG_ENDIAN );
        
        int type= ch.getInt(pos);
        int endpos= ch.getInt(pos+4);
        
        String stype;
        if ( type==RECTYPE_ENDMARKER ) {
            return null;
        } else {
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    stype= "variable";
                    StringData varName= readString( ch, pos+20 );
                    return slice(ch, pos, endpos, stype, varName.string );
                case RECTYPE_VERSION:
                    stype= "version";
                    break;
                case RECTYPE_TIMESTAMP:
                    stype="timestamp";
                    break;
                case RECTYPE_PROMOTE64:
                    stype="promote64";
                    break;
                default:
                    stype="???";
                    break;
            }
            return slice(ch, pos, endpos, stype, "" );
        }
    }
    
    /**
     * return the next record buffer, or returns null at the end.
     * @param ch the bytebuffer
     * @param pos the position.
     * @return the record, including the twelve bytes at the beginning
     * @throws IOException 
     */    
    private ByteBuffer readRecord( FileChannel inch, int pos ) throws IOException {

        ByteBuffer b8= ByteBuffer.allocate(8);
        inch.read(b8,pos);
        b8.order( ByteOrder.BIG_ENDIAN );
        
        int type= b8.getInt(0);
        int endpos= b8.getInt(4);
        
        ByteBuffer ch= ByteBuffer.allocateDirect(endpos-pos);
        inch.read(ch,pos);
        
        String stype;
        if ( type==RECTYPE_ENDMARKER ) {
            return null;
        } else {
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    stype= "variable";
                    StringData varName= readString( ch, pos+20 );
                    return slice(ch, pos, endpos, stype, varName.string );
                case RECTYPE_VERSION:
                    stype= "version";
                    break;
                case RECTYPE_TIMESTAMP:
                    stype="timestamp";
                    break;
                case RECTYPE_PROMOTE64:
                    stype="promote64";
                    break;
                default:
                    stype="???";
                    break;
            }
            return slice(ch, pos, endpos, stype, "" );
        }
    }
    
    /**
     * somehow I didn't notice the length before other strings.  In the Python 
     * code they have "_read_string" and "_read_string_data" which has a 
     * second length.
     * @param rec
     * @param pos
     * @return StringDesc to describe the string.
     */
    private StringData readStringData( ByteBuffer rec, int pos ) {
        int len= rec.getInt(pos);
        byte[] mybytes= new byte[len];
        rec.position(pos+4);
        rec.get(mybytes);
        StringData result= new StringData();
        result.string= new String( mybytes );
        result._lengthBytes= 4 + Math.max( 4, (int)( 4 * Math.ceil( ( len ) / 4.0 ) ) ); 
        return result;
    }
    
    
    private StringData readString( ByteBuffer rec, int pos ) {
        int endPos= pos;
        while ( rec.get(endPos)!=0 ) {
            endPos++;
        }
        byte[] mybytes= new byte[endPos-pos];
        rec.position(pos);
        rec.get(mybytes);
        StringData result= new StringData();
        result.string= new String( mybytes );
        result._lengthBytes= Math.max( 4, (int)( 4 * Math.ceil( ( result.string.length() ) / 4.0 ) ) ); 
        return result;
    }
    
    public static final int TYPECODE_COMPLEX_FLOAT_SCALAR=0;
    public static final int TYPECODE_BYTE=1;
    public static final int TYPECODE_INT16=2;
    public static final int TYPECODE_INT32=3;
    public static final int TYPECODE_FLOAT=4;
    public static final int TYPECODE_DOUBLE=5;
    public static final int TYPECODE_COMPLEX_FLOAT=6;
    public static final int TYPECODE_STRING=7;
    public static final int TYPECODE_STRUCT=8;
    public static final int TYPECODE_COMPLEX_DOUBLE=9;
    public static final int TYPECODE_INT64=14;
    public static final int TYPECODE_UINT64=15;

    /**
     * return a string representing the type code, if supported.
     * @param typeCode for example 4 which means float or 7 which means string.
     * @return "float" or "string" or whatever the code is, or the numeric code if not supported.
     */
    public static String decodeTypeCode( int typeCode ) {
        switch ( typeCode ) {
            case TYPECODE_BYTE: {
                return "byte";
            }
            case TYPECODE_INT16: {
                return "short";
            }
            case TYPECODE_INT32: {
                return "int";
            }
            case TYPECODE_INT64: {
                return "long";
            }
            case TYPECODE_FLOAT: {
                return "float";
            }   
            case TYPECODE_DOUBLE: {
                return "double";
            }
            case TYPECODE_COMPLEX_DOUBLE: {
                return "complex_double";
            }            
            case TYPECODE_COMPLEX_FLOAT: {
                return "complex_float";
            }            
            case TYPECODE_STRUCT: {
                return "struct";
            }            
            case TYPECODE_STRING: {
                return "string";
            }
            default:
                return String.valueOf(typeCode);
        }
    }
    
    /**
     * return the size of the IDL data type in bytes.  Note shorts are stored
     * in 4-bytes.
     * @param typeCode
     * @return 
     */
    private static int sizeOf( int typeCode ) {
        int[] sizes= new int[] { 0, 4, 4, 4, 4,   8, 16, 1, 0, 32,   0, 0, 0, 0, 8, 8 };
        return sizes[typeCode];
    }
    
    /**
     * read the TypeDesc for the variable.
     * @param in
     * @param name
     * @return
     * @throws IOException 
     */
    private TypeDesc readTypeDesc( ByteBuffer in, String name ) throws IOException {
        int magic= in.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect");
        }
        int pos= 4;
        
        ByteBuffer rec= readRecord( in, pos );
        while ( rec!=null ) {
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    logger.config("variable");
                    StringData varName= readString( rec, 20 );
                    if ( name.startsWith(varName.string) ) {
                        int nextField= 20 + varName._lengthBytes;
                        ByteBuffer var= slice(rec, nextField, rec.limit(), "variablestruct", name );
                        TypeDesc td= readTypeDesc(var);
                        return td; // struct
                    } else if ( varName.string.equals(name) ) {
                        int nextField= 20 + varName._lengthBytes;
                        ByteBuffer var= slice(rec, nextField, rec.limit(), "variable", name );
                        TypeDesc td= readTypeDesc(var);
                        return td;
                    }
                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( in, pos );
        }
        throw new IllegalArgumentException("unable to find variable: "+name);        
    }
    /**
     * return true if the name refers to an array
     * @param in ByteBuffer for the entire file
     * @param name the variable name
     * @return td.isStructure();
     */
    public boolean isArray(ByteBuffer in, String name) throws IOException {
        TypeDesc td= readTypeDesc(in, name);
        return isArray( td.varFlags );
    }
    
    /**
     * return true if the name refers to a structure
     * @param in ByteBuffer for the entire file
     * @param name the variable name
     * @return true if the name refers to a structure
     */
    public boolean isStructure(ByteBuffer in, String name) throws IOException {
        TypeDesc td= readTypeDesc(in, name);
        return isStructure( td.varFlags );
    }

    private TagDesc findStructureTag(StructDesc structDesc, String s) {
        String[] ss= s.split("\\.",2);
        int istruct= 0;
        int iarray= 0;
        if ( ss.length==1 ) {
            int itagfind=-1;
            for ( int itag=0; itag<structDesc.ntags; itag++ ) {
                if ( ( structDesc.tagtable[itag].tagflags & VARFLAG_STRUCT )==VARFLAG_STRUCT ) {
                    if ( structDesc.tagnames[itag].equals(s) ) {
                        itagfind=itag;
                        break;
                    }
                    istruct++;
                }
                if ( ( structDesc.tagtable[itag].tagflags & VARFLAG_ARRAY )==VARFLAG_ARRAY ) {
                    if ( structDesc.tagnames[itag].equals(s) ) {
                        itagfind=itag;
                        break;
                    }
                    iarray++;
                }
                if ( structDesc.tagnames[itag].equals(s) ) {
                    itagfind=itag;
                    break;
                }
            }
            if ( itagfind==-1 ) {
                throw new IllegalArgumentException("tag not found");
            }
            if ( ( structDesc.tagtable[itagfind].tagflags & VARFLAG_STRUCT )==VARFLAG_STRUCT ) {
                return structDesc.structTable[istruct];
            } else if ( ( structDesc.tagtable[itagfind].tagflags & VARFLAG_ARRAY )==VARFLAG_ARRAY ) {
                return structDesc.arrTable[iarray];
            } else {
                return structDesc.tagtable[itagfind];
            }
        } else {
            TagDesc td= findStructureTag(structDesc, ss[0]);
            if ( td instanceof StructDesc ) {
                return findStructureTag( (StructDesc)td, ss[1] );
            } else {
                throw new IllegalArgumentException("no such location, expected structure at: "+ss[0]);
            }
        }
    }
        
    private static class TypeDescScalar extends TypeDesc {
        int offs= 12;
        @Override
        Object readData( ByteBuffer buf ) {
            switch ( typeCode ) {
                case TYPECODE_INT16:
                    return (short)buf.getInt(offs);
                case TYPECODE_INT32:
                    return buf.getInt(offs);
                case TYPECODE_INT64:
                    return buf.getLong(offs);
                case TYPECODE_FLOAT:
                    return buf.getFloat(offs);
                case TYPECODE_DOUBLE:
                    return buf.getDouble(offs);
                case TYPECODE_COMPLEX_FLOAT:
                    return new float[] { buf.getFloat(offs), buf.getFloat(offs+4) };
                case TYPECODE_COMPLEX_DOUBLE:
                    return new double[] { buf.getDouble(offs), buf.getDouble(offs+8) };
                case TYPECODE_STRING:
                    int len= buf.getInt(offs);
                    if ( len<0 || len>1024 ) {
                        throw new IllegalArgumentException("unbelievable len, something has gone wrong.");
                    }
                    byte[] bb= new byte[len];
                    for ( int i=0; i<len; i++ ) {
                        bb[i]= buf.get(offs+8+i);
                    }
                    return new String( bb );
                default:
                    throw new IllegalArgumentException("unsupported");
            }
        }
    }
    
    /**
     * structure containing a string and metadata used to read it.
     */
    public static class StringData {
        public String string;
        int _lengthBytes; // note not necessarily the length of the string.
        @Override
        public String toString() {
            return string;
        }
    }
    
    /**
     * structure containing an array and dimension information.
     */
    public static class ArrayData {
        Object array;
        int[] dims;
        /** 
         * number of bytes within the IDLSAV file.
         */
        int _lengthBytes; 
        /**
         * offset to the data within the IDLSAV file.
         */
        int _fileOffset;
        
        int typeCode;
        
        public ArrayData() {
            logger.fine("new ArrayData");
        }
        
        @Override
        public String toString() {
            StringBuilder b= new StringBuilder("["+dims[0]);
            for ( int i=1; i<dims.length; i++ ) {
                b.append(",").append(dims[i]);
            }
            b.append("]");
            return "" + this.array.getClass().getComponentType().getName() + b.toString();            
        }
    }
    
    public static class ScalarDesc extends TagDesc {
        public String toString() {
            return "ScalarDesc nbytes: " + this._lengthBytes + " typeCode: " + this.typecode;
        }
    }
    
    public static class ArrayDesc extends TagDesc {
        int nbytesEl;
        int nbytes;
        int nelements;
        int ndims;
        int nmax;
        int[] dims;
        @Override
        public String toString() {
            return "ArrayDesc nbytes:"+nbytes+" nelements:" +nelements+ " ndims:" +ndims + " nmax:"+nmax + " nbytesEl:"+ nbytesEl;
        }
    }

    /**
     * represents a tag within a structure
     */
    public static class TagDesc {
        /**
         * offset into the structure of the thing described.  When the thing is a structure, the descriptor is the target.
         */
        int offset; 
        
        /**
         * offset into the file
         */
        int fileOffset;
        
        /**
         * the type of thing pointed to.
         */
        int typecode;
        int tagflags;
        /**
         * for convenience, keep track of the total length of the descriptor within the IDLSAV file.
         */
        int _lengthBytes;
        @Override
        public String toString() {
            return "tagdesc  offset: "+offset+ "  tagflags: " +tagflags + "  typecode: " + typecode;
        }
    }
    
    private TagDesc readTagDesc( ByteBuffer rec ) {
        TagDesc result= new TagDesc();
        result.offset= rec.getInt(0);
        result.fileOffset= bufferOffsets.get(getKeyFor(rec)) + result.offset;
        result.typecode= rec.getInt(4);
        result.tagflags= rec.getInt(8);
        return result;
    }    
    
    public static class StructDesc extends TagDesc {
        int predef;
        int ntags;
        int nbytes;
        TagDesc[] tagtable;
        String[] tagnames;
        ArrayDesc[] arrTable;
        StructDesc[] structTable;
        //String className;
        //int nsupClasses;
        //String[] supClassNames;
        //StructDesc[] supClassTable;
        
        @Override
        public String toString() {
            return "predef: "+ predef + " ntags:"+ntags+ " nbytes:"+nbytes;
        }
    }
    
    private static class TypeDescArray extends TypeDesc {
        ArrayDesc arrayDesc;
        int offsToArray= 76;
        int _lengthBytes; // length of the array.
        
        private ArrayData makeArrayData( Object array, int fileOffset, int lengthBytes ) {
            ArrayData result= new ArrayData();
            result.array= array;
            result.dims= arrayDesc.dims;
            result._fileOffset= fileOffset;
            result._lengthBytes= lengthBytes;
            result.typeCode= typeCode;
            return result;
        }
        
        /**
         * read the data as an array.  Note Java's arrays are 1-D,
         * and only 1-D arrays are used to return the data.  For
         * 2-D arrays (and higher dimension), use the convenience
         * method readArrayData to get a Java N-D array.
         * 
         * @param buf
         * @return 
         */
        @Override
        Object readData( ByteBuffer buf ) {
            _lengthBytes= sizeOf(typeCode) * arrayDesc.nelements;
            
            int offsetToFile= bufferOffsets.get(getKeyFor(buf));
            logger.log(Level.CONFIG, "readData @ {0,number,#}", offsetToFile+ offsToArray );
            
            switch (typeCode) {
                case TYPECODE_INT16: {
                    short[] result= new short[arrayDesc.nelements];
                    for ( int i=0; i<result.length; i++ ) {
                        result[i]= (short)buf.getInt(offsToArray+4*i);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*4 );
                }
                case TYPECODE_INT32: {
                    
                    int[] result= new int[arrayDesc.nelements];
                    for ( int i=0; i<result.length; i++ ) {
                        result[i]= buf.getInt(offsToArray+4*i);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*4 );
                }
                case TYPECODE_INT64: {
                    //TODO: test me
                    long[] result= new long[arrayDesc.nelements];
                    for ( int i=0; i<result.length; i++ ) {
                        result[i]= buf.getLong(offsToArray+8*i);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*8 );
                }
                case TYPECODE_UINT64: {
                    logger.warning("unsigned longs handled with signed longs");
                    long[] result= new long[arrayDesc.nelements];
                    for ( int i=0; i<result.length; i++ ) {
                        result[i]= buf.getLong(offsToArray+8*i);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*8 );
                }
                case TYPECODE_FLOAT: {
                    float[] result= new float[arrayDesc.nelements];
                    for ( int i=0; i<result.length; i++ ) {
                        result[i]= buf.getFloat(offsToArray+4*i);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*4 );
                }
                case TYPECODE_COMPLEX_FLOAT: {
                    float[] result= new float[arrayDesc.nelements*2];
                    for ( int i=0; i<arrayDesc.nelements; i++ ) {
                        result[i*2]= buf.getFloat(offsToArray+8*i);
                        result[i*2+1]= buf.getFloat(offsToArray+8*i+4);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*8 );
                }
                case TYPECODE_DOUBLE: {
                    double[] result= new double[arrayDesc.nelements];
                    for ( int i=0; i<result.length; i++ ) {
                        result[i]= buf.getDouble(offsToArray+8*i);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*8 );
                }
                case TYPECODE_COMPLEX_DOUBLE: {
                    double[] result= new double[arrayDesc.nelements*2];
                    for ( int i=0; i<arrayDesc.nelements; i++ ) {
                        result[i*2]= buf.getDouble(offsToArray+16*i);
                        result[i*2+1]= buf.getDouble(offsToArray+16*i+8);
                    }
                    return makeArrayData(result, offsetToFile+ offsToArray, result.length*16 );
                }
                case TYPECODE_STRING: {
                    String[] result= new String[arrayDesc.nelements];
                    int offs= offsToArray;
                    //for ( int i=0; i<buf.limit(); i++ ) {
                    //    System.err.println( String.format( "%4d: %3d %c",i,buf.get(i), (char)buf.get(i) ) );
                    //}
                    //System.err.println("");
                    for ( int i=0; i<result.length; i++ ) {
                        int len= buf.getInt(offs);
                        if ( len<0 || len>1024 ) {
                            throw new IllegalArgumentException("string has unbelievable len, something has gone wrong.");
                        }
                        byte[] bb= new byte[len];
                        for ( int k=0; k<len; k++ ) {
                            bb[k]= buf.get(offs+8+k);
                        }
                        result[i]= new String( bb );
                        if ( result[i].length()==0 ) {
                            offs= offs+4;
                        } else {
                            offs= offs+sizeOfString(result[i])+8;
                        }
                    }
                    ArrayData adresult= makeArrayData(result, offsetToFile+ offsToArray, offs-offsToArray );
                    return adresult;
                }
                default:
                    break;
            }
            return null;
        }
                
        @Override
        public String toString() {
            StringBuilder b= new StringBuilder("["+this.arrayDesc.dims[0]);
            for ( int i=1; i<this.arrayDesc.ndims; i++ ) {
                b.append(",").append(this.arrayDesc.dims[i]);
            }
            b.append("]");
            return "" + decodeTypeCode(this.typeCode) + b.toString();
        }
        
    }
    
    /**
     * read the data into 1-D and 2-D arrays.  This is provided for reference, but 
     * can be extended to 3-D and higher arrays, if the need arrises.
     * @param data
     * @return 
     */
    public static Object readArrayDataIntoArrayOfArrays( ArrayData data ) {
        Object flattenedArray= data.array;
        if ( flattenedArray==null ) return null;

        switch ( data.dims.length ) {
            case 1:
                return flattenedArray;
            case 2:
                Object result= Array.newInstance( flattenedArray.getClass(), data.dims[0] );
                for ( int i=0; i<data.dims[0]; i++ ) {
                    Object a1= Array.newInstance( flattenedArray.getClass().getComponentType(), data.dims[1] );
                    int nj= data.dims[1];
                    for ( int j=0; j<nj; j++ ) {
                        Array.set( a1, j, Array.get( flattenedArray, i*nj+j ) );
                    }
                    Array.set( result, i, a1 );
                }
                return result;
            default:
                throw new UnsupportedOperationException("only 1-D and 2-D arrays are supported for now.");
        }
    }

    private static Class getPrimativeClass( Class t ) {
        if ( t==Integer.class ) {
            return int.class;
        } else if ( t==Long.class ) {
            return long.class;
        } else if ( t==Short.class ) {
            return short.class;
        } else if ( t==Double.class ) {
            return double.class;
        } else if ( t==Float.class ) {
            return float.class;
        } else if ( t==String.class ) {
            return String.class;
        } else {
            throw new UnsupportedOperationException("not implemented: "+t);
        }
    }
        
    private static void accumulate( Map<String,Object> accumulator, Map<String,Object> rec, int j, int nj ) {
        if ( accumulator.entrySet().isEmpty() ) {
            for ( Entry<String,Object> e: rec.entrySet() ) {
                Object o;
                if ( e.getValue() instanceof ArrayData ) {
                    ArrayData ad= (ArrayData)e.getValue(); // Java 14 is coming and we won't need a cast, so exciting.
                    ArrayData ac= new ArrayData();
                    ac.typeCode= ad.typeCode;
                    ac.dims= new int[ad.dims.length+1];
                    ac.dims[0]= nj;
                    System.arraycopy( ad.dims, 0, ac.dims, 1, ad.dims.length );
                    ac.array= Array.newInstance( ad.array.getClass(), nj );
                    Array.set( ac.array, j, ad.array );
                    o= ac;
                } else if ( e.getValue() instanceof Map ) {
                    Map accumulator1= new LinkedHashMap();
                    accumulate( accumulator1, (Map<String,Object>)e.getValue(), j, nj );
                    o= accumulator1;
                } else  {
                    Object d= e.getValue();
                    ArrayData ac= new ArrayData();
                    ac.dims= new int[] { nj };
                    Class t= getPrimativeClass( d.getClass() );
                    ac.array= Array.newInstance( t, nj );
                    Array.set( ac.array, j, d );
                    o= ac;
                }
                accumulator.put( e.getKey(), o );
            }
        }
        for ( Entry<String,Object> e: accumulator.entrySet() ) {
            Object o= rec.get(e.getKey());
            if ( o instanceof ArrayData ) {
                ArrayData ad= (ArrayData)o;
                ArrayData ac= (ArrayData)e.getValue();
                Array.set( ac.array, j, ad.array );
            } else if ( e.getValue() instanceof Map ) {
                accumulate( (Map<String,Object>)e.getValue(), (Map<String,Object>)o, j, nj);
            } else {
                ArrayData ac= (ArrayData)e.getValue();
                Array.set( ac.array, j, o );
            }
        }
    }
    
    private static class TypeDescStructure extends TypeDesc {
        ArrayDesc structArrayDesc;
        StructDesc structDesc;
        int offsetToData;
        boolean isSubstructure; // structure within a structure.
        /**
         * length of the data within the IDLSav file.
         */
        int _lengthBytes;
        
        @Override
        Object readData(ByteBuffer data) {
            LinkedHashMap<String,Object> result;
            int nj= structArrayDesc.nelements;
            if ( nj>1 ) {
                result= new LinkedHashMap<>();
                int iptr= offsetToData + ( isSubstructure ? 0 : 4 );
                int iptr0= iptr;
                for ( int j=0; j<nj; j++ ) {
                    int iarray= 0;
                    int istructure= 0;
                    for ( int i=0; i<structDesc.tagnames.length; i++ ) {
                        String tag= structDesc.tagnames[i];
                        if ( isStructure( structDesc.tagtable[i].tagflags ) ) {
                            TypeDescStructure struct1= new TypeDescStructure();
                            StructDesc structDesc1= structDesc.structTable[istructure];
                            struct1.structDesc= structDesc1;
                            struct1.structArrayDesc= structDesc.arrTable[iarray];
                            struct1.offsetToData= iptr;
                            struct1.isSubstructure= true;
                            logger.log(Level.CONFIG, "readstruct {0} {1,number,#} {2,number,#} {3}", new Object[]{data.position(), 0, data.limit(), tag});
                            Object map1= struct1.readData(data);                        
                            if ( j==0 ) {
                                Map mapd= (Map)map1;
                                Map accumulator= new LinkedHashMap();
                                accumulate( accumulator, mapd, j, nj );
                                result.put( tag, accumulator );
                            } else {
                                Map mapd= (Map)map1;
                                Map accumulator= (Map)result.get( tag );
                                accumulate( accumulator, mapd, j, nj );
                            }
                            iptr= iptr + struct1._lengthBytes;
                            iarray= iarray + 1;
                            istructure= istructure + 1;
                            //iptr= iptr + struct1._lengthBytes;
                        } else if ( isArray( structDesc.tagtable[i].tagflags ) ) {
                            TypeDescArray arr1= new TypeDescArray();
                            arr1.arrayDesc= structDesc.arrTable[iarray];
                            arr1.offsToArray= iptr;
                            arr1.typeCode= structDesc.tagtable[i].typecode;
                            arr1.varFlags= structDesc.tagtable[i].tagflags;
                            logger.log(Level.CONFIG, "readarray {0} {1,number,#} {2,number,#} {3}", new Object[]{data.position(), 0, data.limit(), tag});
                            Object arr= arr1.readData(data);
                            if ( j==0 && arr instanceof ArrayData ) {
                                ArrayData ad= (ArrayData)arr;
                                ArrayData accumulator= new ArrayData();
                                accumulator.dims= new int[ad.dims.length+1];
                                accumulator.dims[0]= structArrayDesc.nelements;
                                System.arraycopy( ad.dims, 0, accumulator.dims, 1, ad.dims.length );
                                accumulator.array= Array.newInstance( ad.array.getClass(), structArrayDesc.nelements );
                                Array.set( accumulator.array, j, ad.array );
                                result.put( tag, accumulator );
                            } else {
                                ArrayData ad= (ArrayData)arr;
                                ArrayData accumulator= (ArrayData) result.get( tag );
                                Array.set( accumulator.array, j, ad.array );
                            }
                            iarray= iarray+1;
                            iptr= iptr + arr1._lengthBytes;
                        } else if ( !isStructure( structDesc.tagtable[i].tagflags ) ) {
                            TypeDescScalar scalarTypeDesc= new TypeDescScalar();
                            scalarTypeDesc.offs= iptr;
                            scalarTypeDesc.typeCode= structDesc.tagtable[i].typecode;
                            logger.log(Level.CONFIG, "readscalar {0} {1,number,#} {2,number,#} {3}", new Object[]{data.position(), 0, data.limit(), tag});
                            Object scalar= scalarTypeDesc.readData(data);                            
                            if ( j==0 ) {
                                if ( scalar.getClass().isArray() ) throw new IllegalArgumentException("scalar should not be an array");
                                ArrayData accumulator= new ArrayData();
                                accumulator.dims= new int[] {  structArrayDesc.nelements };
                                Class t= getPrimativeClass( scalar.getClass() );
                                accumulator.array= Array.newInstance( t, structArrayDesc.nelements );
                                Array.set( accumulator.array, j, scalar );
                                result.put( tag, accumulator );
                            } else {
                                ArrayData accumulator= (ArrayData) result.get( tag );
                                Array.set( accumulator.array, j, scalar );
                            }
                            if ( scalar instanceof String ) {
                                String string= (String)scalar;
                                if ( string.length()==0 ) {
                                    iptr = iptr + 4;
                                } else {
                                    iptr = iptr + 8 + sizeOfString( string );
                                }
                            } else {
                                iptr= iptr + sizeOf( scalarTypeDesc.typeCode );
                            }
                            
                        }
                    }
                }
                this._lengthBytes= iptr-iptr0;
            } else {
                result= new LinkedHashMap<>();
                int iptr= offsetToData + ( isSubstructure ? 0 : 4 );
                int iptr0= iptr;
                int iarray= 0;
                int istructure= 0;
                for ( int i=0; i<structDesc.tagnames.length; i++ ) {
                    String tag= structDesc.tagnames[i];
                    logger.log(Level.FINE, "reading tag {0}", tag);
                    if ( isStructure( structDesc.tagtable[i].tagflags ) ) {
                        TypeDescStructure struct1= new TypeDescStructure();
                        StructDesc structDesc1= structDesc.structTable[istructure];
                        struct1.structDesc= structDesc1;
                        struct1.structArrayDesc= structDesc.arrTable[iarray];
                        struct1.offsetToData= iptr;
                        struct1.isSubstructure= true;
                        logger.log(Level.CONFIG, "readstruct_1 {0} {1,number,#} {2,number,#} {3}", new Object[]{iptr, 0, data.limit(), tag});
                        Object map= struct1.readData(data);
                        result.put( tag, map );
                        iptr= iptr + struct1._lengthBytes;
                        iarray= iarray + 1;
                        istructure= istructure + 1;
                        //iptr= iptr + struct1._lengthBytes;
                    } else if ( isArray( structDesc.tagtable[i].tagflags ) ) {
                        TypeDescArray arr1= new TypeDescArray();
                        arr1.arrayDesc= structDesc.arrTable[iarray];
                        arr1.offsToArray= iptr;
                        arr1.typeCode= structDesc.tagtable[i].typecode;
                        arr1.varFlags= structDesc.tagtable[i].tagflags;
                        logger.log(Level.CONFIG, "readarray_1 {0} {1,number,#} {2,number,#} {3}", new Object[]{iptr, 0, data.limit(), tag});
                        Object arr= arr1.readData(data);
                        int strLenBytes= ((ArrayData)arr)._lengthBytes;
                        result.put( tag, arr );
                        iarray= iarray+1;
                        iptr= iptr + strLenBytes;
                    } else { 
                        TypeDescScalar scalarTypeDesc= new TypeDescScalar();
                        scalarTypeDesc.offs= iptr;
                        scalarTypeDesc.typeCode= structDesc.tagtable[i].typecode;
                        logger.log(Level.CONFIG, "readscalar_1 {0} {1,number,#} {2,number,#} {3}", new Object[]{iptr, 0, data.limit(), tag});
                        Object scalar= scalarTypeDesc.readData(data);     
                        result.put( tag, scalar );
                        if ( scalarTypeDesc.typeCode==7 ) {
                            String string= (String)scalar;
                            if ( string.length()==0 ) {
                                iptr = iptr + 4;
                            } else {
                                iptr = iptr + 8 + sizeOfString( string );
                            }
                        } else {
                            iptr= iptr + sizeOf( scalarTypeDesc.typeCode );
                        }
                    }
                }
                this._lengthBytes= iptr-iptr0;
            }
            return result;
        }
        
    }
    
    private static int sizeOfString( String string ) {
        int n= string.length();
        if ( n==0 ) {
            return 0;
        }
        switch ( n%4 ) {
            case 0: return n;
            case 1: return n+3;
            case 2: return n+2;
            case 3: return n+1;
            default: throw new IllegalArgumentException("implementation error");
        }
    }
    
    private static boolean isArray( int varFlags ) {
        return ( varFlags & 0x04 ) == 0x04;
    }
    
    private static boolean isStructure( int varFlags ) {
        return ( varFlags & 0x20 ) == 0x20;
    }
    
    /**
     * a TypeDesc is a description of a thing that is in the IDLSav file.  Its readData
     * method will return something of the type.
     */
    private static abstract class TypeDesc {
        int typeCode;
        int varFlags;
        /**
         * read the data, where data is a byte buffer starting
         * with the TypeDesc.
         * @param data
         * @return 
         */
        abstract Object readData( ByteBuffer data );
    }
    
    private TypeDescScalar readTypeDescScalar( ByteBuffer rec ) {
        logger.log(Level.FINER, "readTypeDescScalar @ {0}", bufferOffsets.get(getKeyFor(rec)));
        TypeDescScalar result= new TypeDescScalar();
        result.typeCode= rec.getInt(0);
        result.varFlags= rec.getInt(4);
        return result;
    }
    
    private ArrayDesc readArrayDesc( ByteBuffer rec ) {
        logger.log(Level.FINER, "readArrayDesc @ {0}", bufferOffsets.get(getKeyFor(rec)));
        ArrayDesc result= new ArrayDesc();
        if ( rec.getInt(0)!=8 ) {
            throw new IllegalArgumentException("expected 8 for ARRSTART");
        }
        result.nbytesEl= rec.getInt(4);
        result.nbytes= rec.getInt(8);
        result.nelements= rec.getInt(12);
        result.ndims= rec.getInt(16);
        result.nmax= rec.getInt(28);
        result.dims= new int[result.ndims];
        for ( int i=0; i<result.ndims; i++ ){
            result.dims[result.ndims-1-i]= rec.getInt(32+4*i); 
        }
        result._lengthBytes= 32+4*result.nmax;
        return result;
    }
    
    public StructDesc readStructDesc( ByteBuffer rec ) {
        logger.log(Level.FINER, "readStructDesc @ {0}", bufferOffsets.get(getKeyFor(rec)));
        StructDesc result= new StructDesc();
        if ( rec.getInt(0)!=9 ) {
            throw new IllegalArgumentException("expected 9 for STRUCTSTART");
        }
        StringData name= readString( rec, 4 );
        int nextField= name._lengthBytes + 4;
        
        final int PREDEF_PREDEF= 0x01;
        final int PREDEF_INHERITS= 0x02;
        final int PREDEF_IS_SUPER= 0x04;
        
        result.predef= rec.getInt(nextField+0);
        
        if ( ( result.predef & PREDEF_PREDEF ) == PREDEF_PREDEF ) {
            //not supported.
            logger.warning("PREDEF predefined structures are not supported.");
            return null;
        }
        
        result.ntags= rec.getInt(nextField+4); 
        result.nbytes= rec.getInt(nextField+8);
        result.tagtable= new TagDesc[result.ntags];
        int ipos= nextField + 12;
        
        Map<Integer,Integer> arrayMap= new HashMap<>();
        Map<Integer,Integer> structMap= new HashMap<>();
        
        int narray= 0;
        int nstruct= 0;
        for ( int i=0; i<result.ntags; i++ ) {
            result.tagtable[i]= readTagDesc(slice(rec, ipos, ipos+12, "tagDesc", name.string ) );
            if ( ( result.tagtable[i].tagflags & VARFLAG_ARRAY ) == VARFLAG_ARRAY ) {
                arrayMap.put(narray,i);
                narray++;
            }
            if ( ( result.tagtable[i].tagflags & VARFLAG_STRUCT ) == VARFLAG_STRUCT ) {
                structMap.put(nstruct,i);
                nstruct++;
            }
            ipos+= 12;
        }
        
        result.tagnames= new String[result.ntags];
        for ( int i=0; i<result.ntags; i++ ) {
            StringData stringDesc= readStringData( rec, ipos );
            result.tagnames[i]= stringDesc.string;
            ipos+= stringDesc._lengthBytes;
        }
        
        result.arrTable= new ArrayDesc[narray];
        for ( int i=0; i<narray; i++ ) {
            result.arrTable[i]= readArrayDesc(slice(rec, ipos, rec.limit(), "arrayDesc", result.tagnames[arrayMap.get(i)] ) );
            ipos+= result.arrTable[i]._lengthBytes;           
        }
        
        result.structTable= new StructDesc[nstruct];
        for ( int i=0; i<nstruct; i++ ) {
            result.structTable[i]= readStructDesc(slice(rec, ipos, rec.limit(), "structDesc", result.tagnames[structMap.get(i)] ) );
            ipos+= result.structTable[i]._lengthBytes;           
        }
        if ( ( result.predef & PREDEF_INHERITS ) == PREDEF_INHERITS
              || ( result.predef & PREDEF_IS_SUPER ) == PREDEF_IS_SUPER ) {
            //StringDesc stringDesc= readStringData( rec, ipos );
            //result.className= stringDesc.string;
            //result.nsupClasses= rec.getInt(ipos);
            logger.warning("PREDEF classes are not supported.");
            return null;
        } else {
            result._lengthBytes= ipos;
            return result;
        }
                
    }
    
    private TypeDescStructure readTypeDescStructure( ByteBuffer rec ) {
        logger.log(Level.FINER, "readTypeDescStructure @ {0}", bufferOffsets.get(getKeyFor(rec)));
        TypeDescStructure result= new TypeDescStructure();
        result.typeCode= rec.getInt(0);
        result.varFlags= rec.getInt(4);
        result.structArrayDesc= readArrayDesc(slice(rec, 8, rec.limit(), "arrayDesc", "" ) );
        result.structDesc= readStructDesc(slice(rec, 10*4+result.structArrayDesc.nmax*4, rec.limit(), "structDesc", "" ) );
        result.offsetToData= 10*4+result.structArrayDesc.nmax*4 + result.structDesc._lengthBytes;
        result.isSubstructure= false;
        return result;
    }
    
    private TypeDescArray readTypeDescArray( ByteBuffer rec ) {
        logger.log(Level.FINER, "readTypeDescStructure @ {0}", bufferOffsets.get(getKeyFor(rec)));
        TypeDescArray result= new TypeDescArray();
        result.typeCode= rec.getInt(0);
        result.varFlags= rec.getInt(4);
        result.arrayDesc= readArrayDesc(slice(rec, 8, rec.limit(), "arrayDesc", "" ) );
        return result;
    }
    
    /**
     * return the TypeDesc, which is after the name.
     * @param typeDescBuf
     * @return 
     */
    private TypeDesc readTypeDesc( ByteBuffer typeDescBuf ) {
        logger.log(Level.FINER, "readTypeDesc @ {0}", bufferOffsets.get(getKeyFor(typeDescBuf)));
        int typeCode= typeDescBuf.getInt(0);
        int varFlags= typeDescBuf.getInt(4);
        if ( typeCode<0 || typeCode>15 ) {
            throw new IllegalArgumentException("expected 0-14 for type code in readTypeDesc");
        }
        if ( ( varFlags & VARFLAG_STRUCT ) == VARFLAG_STRUCT ) {
            return readTypeDescStructure(typeDescBuf);
        } else if ( ( varFlags & VARFLAG_ARRAY ) == VARFLAG_ARRAY ) {
            return readTypeDescArray(typeDescBuf);
        } else {
            return readTypeDescScalar(typeDescBuf);
        }
    }
    
    /**
     * read the scalar, array, or structure at this position.  An
     * array is returned flattened, and readTypeDesc should be used
     * to unflatten it.  Structures are returned as a LinkedHashMap.
     * @param rec the byte buffer
     * @param offset offset into rec
     * @param vars map containing read data.
     * @return the read data.
     */
    private Object variable( ByteBuffer rec, int offset, Map<String,Object> vars) {
        logger.log( Level.FINER, "variable @ {0}", bufferOffsets.get(getKeyFor(rec)) );
        int type= rec.getInt(0+offset);
        if ( type!=RECTYPE_VARIABLE ) {
            throw new IllegalArgumentException("not a variable");
        }
        //printBuffer(rec);
        StringData varName= readString( rec, 20+offset );
        logger.log(Level.FINE, "variable name is {0}", varName );

        int nextField= 20 + varName._lengthBytes + offset;

        ByteBuffer data= slice(rec, nextField, rec.limit(), "typeDesc", "" );
        TypeDesc typeDesc= readTypeDesc( data );
        
        logger.log(Level.CONFIG, "variable_972 {0} {1,number,#} {2,number,#} {3}", new Object[]{data.position(), 0, data.limit(), varName});
        Object result= typeDesc.readData( data );
        
        vars.put( varName.string, result );
        
        return result;
        
    }
    
    private static final Map<Long,Integer> bufferOffsets= new HashMap<>();
    private static final Map<Long,String> bufferLabels= new HashMap<>();
    
    private String nameFor( ByteBuffer buf ) {
        return bufferLabels.get(getKeyFor(buf));
    }
    
    private static Long getKeyFor( ByteBuffer buf ) {
        return ((long)buf.limit())*Integer.MAX_VALUE + buf.position();
    }
    
    /**
     * slice out just the object 
     * @param src
     * @param position
     * @param limit
     * @param label
     * @return 
     */
    private ByteBuffer slice( ByteBuffer src, int position, int limit, String type, String label ) {
        if ( label==null ) throw new IllegalArgumentException("no label");
        Integer offset= bufferOffsets.get(getKeyFor(src));
        if ( offset!=null ) {
            logger.log(Level.CONFIG, "slice {0} {1,number,#} {2,number,#} {3}", 
                    new Object[]{ type, position+offset, limit+offset, label });
        } else {
            logger.log(Level.CONFIG, "slice {0} {1,number,#} {2,number,#} {3}", new Object[]{ type, position, limit, label });
            offset=0;
            if ( bufferLabels.get(getKeyFor(src))==null ) {
                bufferLabels.put(getKeyFor(src),"file");
            }
        }
        int position0= src.position();
        int limit0= src.limit();
        src.position(position);
        src.limit(limit);
        ByteBuffer r1= ByteBuffer.allocate(limit-position);
        r1.put(src.slice());
        r1.flip();
        src.limit(limit0);
        src.position(position0);
        
        bufferOffsets.put( getKeyFor(r1), position+offset );
        bufferLabels.put( getKeyFor(r1), label );
        return r1;
    }
    
    private String labelType( int type ) {
        switch (type) {
            case RECTYPE_TIMESTAMP:
                return "timeStamp";                
            case RECTYPE_VERSION:
                return "version";                
            case RECTYPE_VARIABLE:
                return "variable";
            case RECTYPE_ENDMARKER:
                return "endmarker";
            default:
                return "<unsupported>";
        }
    }
    
    public static ByteBuffer readFileIntoByteBuffer( File f ) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile(f,"r");
        FileChannel inChannel = aFile.getChannel();
        long fileSize = inChannel.size();
        if ( fileSize>Integer.MAX_VALUE ) {
            throw new IllegalArgumentException("file is too large to read, and must be less than 2GB: "+f);
        }
        ByteBuffer buffer = ByteBuffer.allocate((int) fileSize);        
        int bytesRead= 0;
        while ( bytesRead<fileSize ) {
            bytesRead+= inChannel.read(buffer);
        }
        return buffer;
    }
   
    public static FileChannel readFileIntoChannel( File f ) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile(f,"r");
        FileChannel inChannel = aFile.getChannel();
        return inChannel;
    }
   
    public Map<String,Object> readVars( ByteBuffer in ) throws IOException {
        
        //  2  ch.write(getBytesStr("SR"));

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

        int magic= in.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect");
        }
        int pos= 4;
        
        Map<String,Object> result= new LinkedHashMap<>();
        
        ByteBuffer rec= readRecord( in, pos );
        while ( rec!=null ) {
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            if ( rec.getInt(8)!=0 ) {
                throw new IllegalArgumentException("records bigger than 2**32 bytes are not supported.");
            }
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    logger.config("variable");
                    variable(rec, 0, result);
                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( in, pos );
        }
        return result;
    }

    public Map<String,Object> readVars( FileChannel inChannel ) throws IOException {
        
        //  2  ch.write(getBytesStr("SR"));

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

        ByteBuffer buf= ByteBuffer.allocate(4);
        if ( !(inChannel.read(buf)==4) ) {
            throw new IllegalArgumentException("not 4 bytes");
        }
        int magic= buf.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect");
        }
        int pos= 4;
        
        Map<String,Object> result= new LinkedHashMap<>();
        
        ByteBuffer rec= readRecord( inChannel, pos );
        while ( rec!=null ) {
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            if ( rec.getInt(8)!=0 ) {
                throw new IllegalArgumentException("records bigger than 2**32 bytes are not supported.");
            }
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    logger.config("variable");
                    variable(rec, 0, result);
                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( inChannel, pos );
        }
        return result;
    }
    
    /**
     * list the names in the IDLSav file.  This is only the supported
     * variable types.
     * @param in
     * @return the names found.
     * @throws IOException 
     */
    public String[] readVarNames( ByteBuffer in ) throws IOException {
        int magic= in.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect");
        }
        int pos= 4;
        
        List<String> names= new ArrayList<>();
        
        ByteBuffer rec= readRecord( in, pos );  
        while ( rec!=null ) {
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    logger.config("variable");
                    StringData varName= readString( rec, 20 );

                    int nextField= varName._lengthBytes;

                    ByteBuffer var= slice(rec, 20+nextField, rec.limit(), "var_x", "" );
                    
                    names.add(varName.string); 

                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( in, pos );
        }
        return names.toArray( new String[names.size()] );
    }
    
    /**
     * list the names in the IDLSav file.  This is only the supported
     * variable types.
     * @param in
     * @return the names found.
     * @throws IOException 
     */
    public String[] readVarNames( FileChannel inChannel ) throws IOException {
        ByteBuffer buf= ByteBuffer.allocate(4);
        if ( !(inChannel.read(buf)==4) ) {
            throw new IllegalArgumentException("not 4 bytes");
        }
        int magic= buf.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect");
        }
        int pos= 4;
        
        List<String> names= new ArrayList<>();
        
        ByteBuffer rec= readRecord( inChannel, pos );  
        while ( rec!=null ) {
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    logger.config("variable");
                    StringData varName= readString( rec, 20 );

                    int nextField= varName._lengthBytes;

                    ByteBuffer var= slice(rec, 20+nextField, rec.limit(), "var_x", "" );
                    
                    names.add(varName.string); 

                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( inChannel, pos );
        }
        return names.toArray( new String[names.size()] );
    }    
    
    /**
     * scan through the IDLSav and return just the one variable.
     * @param in the IDLSav mapped into a NIO ByteBuffer.
     * @param name the variable name to look for.
     * @return
     * @throws IOException 
     */
    public Object readVar( ByteBuffer in, String name ) throws IOException {
        int magic= in.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect, file should start with should be 1397882884");
        }
        if ( in.order()!=ByteOrder.BIG_ENDIAN ) {
            throw new IllegalArgumentException("buffer must be big endian");
        }
        if ( in.position()==0 ) {
            logger.log(Level.CONFIG, "readVar {0} buffer size: {1,number,#}", new Object[] { name, in.limit() } );
        }

        bufferOffsets.put( getKeyFor(in), 0 );
        bufferLabels.put( getKeyFor(in), "<file>" );

        int pos= 4;
        String name0= name; // keep name for reference.
        ByteBuffer rec= readRecord( in, pos );
        
        while ( rec!=null ) {
    
            int offset = bufferOffsets.get(getKeyFor(rec));
        
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    StringData varName= readString( rec, 20 );
                    logger.log(Level.CONFIG, "variable {0} {1,number,#} {2,number,#} {3}", 
                            new Object[] { type, pos, nextPos, varName } );
                    String rest= null;
                    int i= name.indexOf(".");
                    if ( i>-1 ) {
                        rest= name.substring(i+1);
                        name= name.substring(0,i);
                    }
                    if ( i==-1 ) {
                        if ( varName.string.equals(name) ) {
                            Map<String,Object> result= new HashMap<>();
                            variable( in, offset, result);
                            return result.get(name);
                        }
                    } else {
                        if ( varName.string.equals(name) ) {
                            Map<String,Object> result= new HashMap<>();
                            variable( in, offset, result );
                            Map<String,Object> res= (Map<String,Object>) result.get(name);
                            assert rest!=null;
                            i= rest.indexOf('.');
                            while ( i>-1 ) {
                                res= (Map<String,Object>)res.get( rest.substring(0,i) );
                                rest= rest.substring(i+1);
                                i= rest.indexOf('.');
                            }
                            return res.get(rest);
                        }
                    }
                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                case RECTYPE_PROMOTE64:
                    logger.config("promote64");
                    throw new IllegalArgumentException("promote64 is not supported.");
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( in, pos );
            
            
        }
        return null;        
        
    }    
    /**
     * scan through the IDLSav and retrieve information about the array.
     * @param in the idlsav loaded into a ByteBuffer.
     * @param name the name of the array
     * @return
     * @throws IOException 
     */
    public TagDesc readTagDesc( ByteBuffer in, String name ) throws IOException {
        int magic= in.getInt(0);
        if ( magic!=1397882884 ) {
            logger.warning("magic number is incorrect");
        }
        int pos= 4;
        
        ByteBuffer rec= readRecord( in, pos );
        while ( rec!=null ) {
            int type= rec.getInt(0);
            int nextPos= rec.getInt(4);
            logger.log(Level.CONFIG, "RecType: {0} Length: {1,number,#}", new Object[]{labelType(type), nextPos-pos});
            switch ( type ) {
                case RECTYPE_VARIABLE:
                    logger.config("variable");
                    StringData varName= readString( rec, 20 );
                    if ( name.startsWith(varName.string+".") || name.equals(varName.string) ) {
                        int nextField= varName._lengthBytes;
                        ByteBuffer var= slice(rec, 20+nextField, rec.limit(), "variable", varName.string );
                        if ( var.getInt(0)==8 ) { // TODO: what is 8?
                            if ( ( var.getInt(4) & VARFLAG_STRUCT ) == VARFLAG_STRUCT ) {
                                TypeDescStructure typeDescStructure= readTypeDescStructure(var);
                                if ( name.equals(varName.string) ) {
                                    return typeDescStructure.structDesc;
                                } else {
                                    return findStructureTag( typeDescStructure.structDesc, name.substring(varName.string.length()+1) );
                                }
                            } else {
                                return readTypeDescArray(var).arrayDesc;
                            }
                        } else {
                            if ( ( var.getInt(4) & VARFLAG_ARRAY ) == VARFLAG_ARRAY ) {
                                TagDesc dd= readTypeDescArray(var).arrayDesc;
                                dd.typecode= readTypeDescArray(var).typeCode;
                                return dd;
                            } else {
                                return readTagDesc(var);
                            }
                        }
                    }
                    break;
                case RECTYPE_VERSION:
                    logger.config("version");
                    break;
                case RECTYPE_TIMESTAMP:
                    logger.config("timestamp");
                    break;
                default:
                    logger.config("???");
                    break;
            }
            pos= nextPos;
            rec= readRecord( in, pos );
        }
        return null;        
    }
    
    private static void arrayToString( Object o, StringBuilder b ) {
        char delim=',';
        for ( int j=0; j<4; j++ ) {
            Object i= Array.get(o,j);
            if ( i.getClass().isArray() ) {
                delim=';';
                if ( j>0 ) b.append(delim);
                arrayToString( i, b );
            } else {
                if ( j>0 ) b.append(delim);
                b.append(i.toString());
            }
        }
        if ( Array.getLength(o)>4 ) {
            b.append(delim);
            b.append("...");
        }
    }
    
//    public static void main( String[] args ) throws IOException {
//        Logger logger= Logger.getLogger("autoplot.idlsav");
//        //logger.setLevel( Level.FINE );
//        Handler h= new ConsoleHandler();
//        h.setLevel(Level.ALL);
//        logger.addHandler(h);
//            
////        FileOutputStream fos = new FileOutputStream(new File("/tmp/test.autoplot.idlsav"));
////        
////        WriteIDLSav widls= new WriteIDLSav();
////        //widls.addVariable( "wxyz", new double[] { 120,100,120,45,46,47,48,49,120,100,120 } );
////        widls.addVariable( "abcd", 240 );
////        //widls.addVariable( "oneval", 19.95 );
////        widls.write(fos);
////        
////        fos.close();
//
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/simple.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/vnames.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/scalars.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/arrayVsScalar.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/floats.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                /home/jbf/public_html/autoplot/data/sav/structureOfLonarr.idlsav    "/home/jbf/public_html/autoplot/data/sav/doublearray.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                  "/home/jbf/public_html/autoplot/data/sav/structureOfLonarr.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/arrayOfStruct.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/arrayOfStruct1Var.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/structure.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/structureWithinStructure.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/stuctOfStruct.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/stuctOfStructOfStruct.idlsav","r");
//        //RandomAccessFile aFile = new RandomAccessFile(
//        //                    "/home/jbf/public_html/autoplot/data/sav/stuctOfStructOfStruct.idlsav","r");
//        RandomAccessFile aFile = new RandomAccessFile(
//                            "/home/jbf/ct/autoplot/data/sav/kristoff/test_fit.idlsav","r");
//                
//        FileChannel inChannel = aFile.getChannel();
//        long fileSize = inChannel.size();
//        
//        ByteBuffer buffer = ByteBuffer.allocate((int) fileSize);
//        int bytesRead= 0;
//        while ( bytesRead<fileSize ) {
//            bytesRead+= inChannel.read(buffer);
//        }
//       
//        Map<String,Object> vars= new ReadIDLSav().readVars(buffer);
//        
//        for ( Entry<String,Object> v : vars.entrySet() ) {
//            System.err.println( v );
//            if ( v.getValue() instanceof Map ) {
//                Map<String,Object> m= (Map<String,Object>)v.getValue();
//                for ( Entry<String,Object> j : m.entrySet() ) {
//                    Object k= j.getValue();
//                    if ( k instanceof ArrayData ) {
//                        System.err.print(j.getKey()+":");
//                        StringBuilder b= new StringBuilder();
//                        arrayToString( ((ArrayData)k).array, b);
//                        System.err.println(b.toString());
//                    } else if ( k==null ) {
//                        System.err.println("<<null>>");
//                    } else {
//                        System.err.println(k.toString());
//                    }
//                }
//            } else {
//                System.err.println(v.getValue());
//            }
//        }
//        
//    }

}