/*
 * DDataSet.java
 *
 * Created on April 24, 2007, 11:08 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package org.autoplot.cdf;

import java.util.HashMap;
import java.util.Map;
import org.das2.qds.AbstractDataSet;
import org.das2.qds.DataSetUtil;
import org.das2.qds.QDataSet;
import org.das2.qds.RankZeroDataSet;
import org.das2.qds.Slice0DataSet;
import org.das2.qds.WritableDataSet;

/**
 * hacked DDataSet implementation does transpose for column major files.
 * rank 1,2,or 3 dataset backed by double array.  Note this is not
 * simply a transpose of DDataSet, as the name implies.  The zeroth index is 
 * the same, and the remaining index are reversed.
 * 
 * Mutable datasets warning: No dataset should be mutable once it is accessible to the
 * rest of the system.  This would require clients make defensive copies which would 
 * seriously degrade performance.  
 *
 * @author jbf
 */
public final class TrFDataSet extends TrArrayDataSet implements WritableDataSet, RankZeroDataSet {

    float[] back;
    int rank;
    int len0;
    int len1;
    int len2;
    int len3;

    private static final boolean RANGE_CHECK = false;
    public static final String version = "20090605";

    public static TrFDataSet createRank1(int len0) {
        return new TrFDataSet(1, len0, 1, 1, 1);
    }

    public static TrFDataSet createRank2(int len0, int len1) {
        return new TrFDataSet(2, len0, len1, 1, 1);
    }

    public static TrFDataSet createRank3(int len0, int len1, int len2) {
        return new TrFDataSet(3, len0, len1, len2, 1);
    }

    public static TrFDataSet createRank4(int len0, int len1, int len2, int len3) {
        return new TrFDataSet(4, len0, len1, len2, len3);
    }

    /**
     * Makes an array from array of dimension sizes.  The result will have
     * rank qube.length(). 
     * @param qube array specifying the rank and size of each dimension
     * @return DDataSet
     */
    public static TrFDataSet create(int[] qube) {
        if ( qube.length==0 ) {
            return new TrFDataSet( 0, 1, 1, 1, 1 );
        } else if (qube.length == 1) {
            return TrFDataSet.createRank1(qube[0]);
        } else if (qube.length == 2) {
            return TrFDataSet.createRank2(qube[0], qube[1]);
        } else if (qube.length == 3) {
            return TrFDataSet.createRank3(qube[0], qube[1], qube[2]);
        } else if (qube.length == 4) {
            return TrFDataSet.createRank4(qube[0], qube[1], qube[2], qube[3]);
        } else {
            throw new IllegalArgumentException("bad qube");
        }
    }

    /**
     * Wraps an array from array of dimension sizes.  The result will have
     * rank qube.length(). 
     * @param data array containing the data, with the last dimension contiguous in memory.
     * @param qube array specifying the rank and size of each dimension
     * @return DDataSet
     */
    public static TrFDataSet wrap( float[] data, int[] qube ) {
        if (qube.length == 0 ) {
            return new TrFDataSet( 1, 1, 1, 1, 1, data );
        } else if (qube.length == 1) {
            return new TrFDataSet( 1, qube[0], 1, 1, 1, data );
        } else if (qube.length == 2) {
            return new TrFDataSet( 2, qube[0], qube[1], 1, 1, data );
        } else if (qube.length == 3) {
            return new TrFDataSet( 3, qube[0], qube[1], qube[2], 1, data );
        } else if (qube.length == 4) {
            return new TrFDataSet( 4, qube[0], qube[1], qube[2], qube[3], data);
        } else {
            throw new IllegalArgumentException("bad qube");
        }
    }

        
    /** Creates a new instance of DDataSet */
    private TrFDataSet(int rank, int len0, int len1, int len2, int len3) {
        this(rank, len0, len1, len2, len3, new float[len0 * len1 * len2 * len3]);
    }

    private TrFDataSet(int rank, int len0, int len1, int len2, int len3, float[] back) {
        if ( back==null ) throw new NullPointerException("back was null");
        this.back = back;
        this.rank = rank;
        this.len0 = len0;
        this.len1 = len1;
        this.len2 = len2;
        this.len3 = len3;
        DataSetUtil.addQube(this);
    }

    protected Object getBack() {
        return this.back;
    }


    public int rank() {
        return rank;
    }

    @Override
    public int length() {
        return len0;
    }

    @Override
    public int length(int i) {
        return len1;
    }

    @Override
    public int length(int i0, int i1) {
        return len2;        
    }

    @Override
    public int length(int i0, int i1, int i2) {
        return len3;
    }

    @Override
    public double value() {
        float v= back[0];
        return v==fill ? dfill : v;
    }

    @Override
    public double value(int i0) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
        }
        float v= back[i0];
        return v==fill ? dfill : v;
    }

    @Override
    public double value(int i0, int i1) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
            if (i1 < 0 || i1 >= len1) {
                throw new IndexOutOfBoundsException("i1=" + i1 + " " + this);
            }
        }
        float v= back[i0 * len1 + i1];
        return v==fill ? dfill : v;
    }

    @Override
    public double value(int i0, int i1, int i2) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
            if (i1 < 0 || i1 >= len1) {
                throw new IndexOutOfBoundsException("i1=" + i1 + " " + this);
            }
            if (i2 < 0 || i2 >= len2) {
                throw new IndexOutOfBoundsException("i2=" + i2 + " " + this);
            }
        }
        float v= back[i0 * len1 * len2 + i2 * len1 + i1];
        return v==fill ? dfill : v;
    }

    @Override
    public double value(int i0, int i1, int i2, int i3) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
            if (i1 < 0 || i1 >= len1) {
                throw new IndexOutOfBoundsException("i1=" + i1 + " " + this);
            }
            if (i2 < 0 || i2 >= len2) {
                throw new IndexOutOfBoundsException("i2=" + i2 + " " + this);
            }
            if (i3 < 0 || i3 >= len3) {
                throw new IndexOutOfBoundsException("i3=" + i3 + " " + this);
            }
        }
        float v= back[i0*len1*len2*len3 + i3*len2*len1 + i2*len1 +i1];
        return v==fill ? dfill : v;
    }

    public void putValue(double value) {
        back[0]= (float)value;
    }

    public void putValue(int i0, double value) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
        }
        back[i0] = (float)value;
    }

    public void putValue(int i0, int i1, double value) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
            if (i1 < 0 || i1 >= len1) {
                throw new IndexOutOfBoundsException("i1=" + i1 + " " + this);
            }
        }
        back[i0 * len1 + i1] = (float)value;
    }

    public void putValue(int i0, int i1, int i2, double value) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
            if (i1 < 0 || i1 >= len1) {
                throw new IndexOutOfBoundsException("i1=" + i1 + " " + this);
            }
            if (i2 < 0 || i2 >= len2) {
                throw new IndexOutOfBoundsException("i2=" + i2 + " " + this);
            }
        }
        back[i0 * len1 * len2 + i2 * len1 + i1] = (float)value;
    }

    public void putValue(int i0, int i1, int i2, int i3, double value) {
        if (RANGE_CHECK) {
            if (i0 < 0 || i0 >= len0) {
                throw new IndexOutOfBoundsException("i0=" + i0 + " " + this);
            }
            if (i1 < 0 || i1 >= len1) {
                throw new IndexOutOfBoundsException("i1=" + i1 + " " + this);
            }
            if (i2 < 0 || i2 >= len2) {
                throw new IndexOutOfBoundsException("i2=" + i2 + " " + this);
            }
            if (i3 < 0 || i3 >= len3) {
                throw new IndexOutOfBoundsException("i3=" + i3 + " " + this);
            }
        }
        back[i0 *len1  *len2   *len3 + i3 *len1  *len2 +  i2*len1 +i1]= (float)value;
        
    }

    /**
     * Shorten the dataset by changing it's dim 0 length parameter.  The same backing array is used, 
     * so the element that remain ill be the same.
     * can only shorten!
     */
    public void putLength(int len) {
        if (len > len0) {
            throw new IllegalArgumentException("dataset cannot be lengthened");
        }
        len0 = len;
    }

    @Override
    public String toString() {
        return DataSetUtil.toString(this);
    }

    @Override
    public void putProperty(String name, Object value) {
        super.putProperty(name, value);
        if ( name.equals(QDataSet.FILL_VALUE) ) checkFill(); // because of rounding errors
    }
    
    /**
     * copies the properties, copying depend datasets as well.  
     * @see DataSetUtil.copyProperties, which is a shallow copy.
     */
    protected static Map copyProperties(QDataSet ds) {
        Map result = new HashMap();        
        Map srcProps= DataSetUtil.getProperties(ds);
        
        result.putAll(srcProps);
                
        for ( int i=0; i < ds.rank(); i++) {
            QDataSet dep = (QDataSet) ds.property("DEPEND_" + i);
            if (dep == ds) {
                throw new IllegalArgumentException("dataset is dependent on itsself!");
            }
            if (dep != null) {
                result.put("DEPEND_" + i, copy(dep));
            }
        }

        for (int i = 0; i < QDataSet.MAX_PLANE_COUNT; i++) {
            QDataSet plane0 = (QDataSet) ds.property("PLANE_" + i);
            if (plane0 != null) {
                result.put("PLANE_" + i, copy(plane0));
            } else {
                break;
            }
        }

        return result;
    }

    private static TrFDataSet ddcopy(TrFDataSet ds) {
        int dsLength = ds.len0 * ds.len1 * ds.len2 * ds.len3;

        float[] newback = new float[dsLength];

        System.arraycopy(ds.back, 0, newback, 0, dsLength);

        TrFDataSet result = new TrFDataSet(ds.rank, ds.len0, ds.len1, ds.len2, ds.len3, newback);
        result.properties.putAll(copyProperties(ds)); // TODO: problems... 
        if ( result.properties.containsKey(QDataSet.FILL_VALUE) ) {
            result.checkFill();
        }
        return result;
    }

    /**
     * Copy the dataset to a DDataSet only if the dataset is not already a DDataSet.
     * @param ds
     * @return
     */
    public static TrFDataSet maybeCopy( QDataSet ds ) {
        if ( ds instanceof TrFDataSet ) {
            return (TrFDataSet)ds;
        } else {
            return copy(ds);
        }
    }
    
    /**
     * copies the dataset into a writeable dataset, and all of its depend datasets as well.
     * An optimized copy is used when the argument is a DDataSet.
     */
    public static TrFDataSet copy(QDataSet ds) {
        if (ds instanceof TrFDataSet) {
            return ddcopy((TrFDataSet) ds);
        }
        int rank = ds.rank();
        TrFDataSet result;
        int len1,len2,len3;
        if ( !DataSetUtil.isQube(ds) ) {
            //throw new IllegalArgumentException("copy non-qube");
            logger.fine("copy of non-qube to DDataSet, which must be qube");
        }
        switch (rank) {
            case 0:
                result= createRank1(1);
                result.rank= 0;
                result.putValue(ds.value());
                break;
            case 1:
                result = createRank1(ds.length());
                for (int i = 0; i < ds.length(); i++) {
                    result.putValue(i, ds.value(i));
                }
                break;
            case 2:
                len1= ds.length() == 0 ? 0 : ds.length(0);
                result = createRank2(ds.length(), len1 );
                for (int i = 0; i < ds.length(); i++) {
                    for (int j = 0; j < ds.length(i); j++) {
                        result.putValue(i, j, ds.value(i, j));
                    }
                }
                break;
            case 3:
                len1= ds.length() == 0 ? 0 : ds.length(0) ;
                len2= len1 == 0 ? 0 : ds.length(0,0);
                result = createRank3(ds.length(), len1, len2 );
                for (int i = 0; i < ds.length(); i++) {
                    for (int j = 0; j < ds.length(i); j++) {
                        for (int k = 0; k < ds.length(i, j); k++) {
                            result.putValue(i, j, k, ds.value(i, j, k));
                        }
                    }
                }
                break;
            case 4:
                len1 = (ds.length()==0) ? 0 : ds.length(0);
                len2 = (len1==0) ? 0 : ds.length(0,0);
                len3 = (len2==0) ? 0 : ds.length(0,0,0);
                result = createRank4(ds.length(), len1, len2, len3);
                for (int i=0; i< ds.length(); i++)
                    for (int j=0; j<ds.length(i); j++)
                        for (int k=0; k<ds.length(i,j); k++)
                            for (int l=0; l<ds.length(i,j,k); l++)
                                result.putValue(i, j, k, l, ds.value(i, j, k, l));
                break;
            default:
                throw new IllegalArgumentException("bad rank");
        }
        result.properties.putAll(copyProperties(ds)); // TODO: problems...

        return result;
    }

    /**
     * creates a DDataSet by wrapping an existing double array.
     */
    public static TrFDataSet wrap(float[] back) {
        return new TrFDataSet(1, back.length, 1, 1, 1, back);
    }

    /**
     * creates a DDataSet by wrapping an existing array, and aliasing it to rank2.
     * Note the last index is packed closest in memory.
     * @param n1 the size of the second dimension.
     */
    public static TrFDataSet wrapRank2(float[] back, int n1) {
        return new TrFDataSet(2, back.length / n1, n1, 1, 1, back);
    }

    /**
     * creates a DDataSet by wrapping an existing array, and aliasing it to rank2.
     * Note the last index is packed closest in memory.  The first index length
     * is calculated from the size of the array.
     * @param n1 the size of the second index.
     * @param n2 the size of the third index.
     */
    public static TrFDataSet wrapRank3(float[] back, int n1, int n2) {
        return new TrFDataSet(3, back.length / (n1 * n2), n1, n2, 1, back);
    }

    /**
     * creates a DDataSet by wrapping an existing array, aliasing it to rank 2.
     */
    public static TrFDataSet wrap(float[] back, int nx, int ny) {
        return new TrFDataSet(2, nx, ny, 1, 1, back);
    }

    public static TrFDataSet wrap( float[] back, int rank, int len0, int len1, int len2 ) {
        return new TrFDataSet( rank, len0, len1, len2, 1, back );
    }

    public static TrFDataSet wrap( float[] back, int rank, int len0, int len1, int len2, int len3) {
        return new TrFDataSet( rank, len0, len1, len2, len3, back);
    }
    
    /**
     * join dep0 if found, join auxillary planes if found.
     */
    private void joinProperties(TrFDataSet ds) {
        Map result = new HashMap();
        for (int i = 0; i < 1; i++) {
            QDataSet dep1 = (QDataSet) ds.property("DEPEND_" + i);
            if (dep1 != null) {
                QDataSet dep0 = (QDataSet) this.property("DEPEND_" + i);
                TrFDataSet djoin = TrFDataSet.copy(dep0);
                TrFDataSet ddep1 = dep1 instanceof TrFDataSet ? (TrFDataSet) dep1 : TrFDataSet.copy(dep1);
                djoin.append(ddep1);
                result.put("DEPEND_" + i, djoin);
            }
        }

        for (int i = 0; i < QDataSet.MAX_PLANE_COUNT; i++) {
            QDataSet dep1 = (QDataSet) ds.property("PLANE_" + i);
            if (dep1 != null) {
                QDataSet dep0 = (QDataSet) this.property("PLANE_" + i);
                TrFDataSet djoin = TrFDataSet.copy(dep0); 
                TrFDataSet dd1 = dep1 instanceof TrFDataSet ? (TrFDataSet) dep1 : TrFDataSet.copy(dep1);
                djoin.append(dd1);  
                result.put("PLANE_" + i, djoin);
            } else {
                break;
            }
        }

        this.properties.putAll(result);
    }

    /**
     * copy elements of src DDataSet into dest DDataSet, with System.arraycopy.
     * src and dst must have the same geometry, except for dim 0.  Allows for
     * aliasing when higher dimension element count matches.
     * @param len number of records to copy.
     * @throws IllegalArgumentException if the higher rank geometry doesn't match
     * @throws IndexOutOfBoundsException
     */
    public static void copyElements(TrFDataSet src, int srcpos, TrFDataSet dest, int destpos, int len) {
        if ( src.len1 != dest.len1 || src.len2 != dest.len2 ) {
            throw new IllegalArgumentException("src and dest geometry don't match");
        }
        copyElements( src, srcpos, dest, destpos, len * src.len1 * src.len2, false); 
    }    
    
    /**
     * copy elements of src DDataSet into dest DDataSet, with System.arraycopy.
     * src and dst must have the same geometry, except for dim 0.  Allows for
     * aliasing when higher dimension element count matches.
     * @param src source dataset
     * @param srcpos source dataset first dimension index.
     * @param dest destination dataset
     * @param destpos destination dataset first dimension index.
     * @param len total number of elements to copy
     * @param checkAlias bounds for aliased write (same number of elements, different geometry.)
     * @throws IllegalArgumentException if the higher rank geometry doesn't match
     * @throws IndexOutOfBoundsException
     */
    public static void copyElements( TrFDataSet src, int srcpos, TrFDataSet dest, int destpos, int len, boolean checkAlias ) {
        if ( checkAlias && ( src.len1*src.len2 != dest.len1*dest.len2 ) ) {
            throw new IllegalArgumentException("src and dest geometry don't match");
        }
        int srcpos1 = srcpos * src.len1 * src.len2;
        int destpos1 = destpos * dest.len1 * dest.len2;
        int len1 = len;
        System.arraycopy( src.back, srcpos1, dest.back, destpos1, len1 );
    }

    /**
     * append the second dataset onto this dataset.  Not thread safe!!!
     * TODO: this really should return a new dataset.  Presumably this is to avoid copies, but currently it copies anyway!
     * TODO: this will be renamed "concatenate" or "append" since "join" is the anti-slice.
     * @deprecated use append instead.
     */
    public void join(TrFDataSet ds) {
        append(ds);
    }
    
    /**
     * append the second dataset onto this dataset.  Not thread safe!!!
     * TODO: this really should return a new dataset.  Presumably this is to avoid copies, but currently it copies anyway!
     */
    public void append( TrFDataSet ds ) {
        if (ds.rank() != rank) {
            throw new IllegalArgumentException("rank mismatch");
        }
        if (ds.len1 != len1) {
            throw new IllegalArgumentException("len1 mismatch");
        }
        if (ds.len2 != len2) {
            throw new IllegalArgumentException("len2 mismatch");
        }
        if (ds.len3 != len3) {
            throw new IllegalArgumentException("len3 mismatch");
        }

        int myLength = len0 * len1 * len2 * len3;
        int dsLength = ds.len0 * ds.len1 * ds.len2 * ds.len3;

        float[] newback = new float[myLength + dsLength];

        System.arraycopy(this.back, 0, newback, 0, myLength);
        System.arraycopy(ds.back, 0, newback, myLength, dsLength);

        len0 = this.len0 + ds.len0;
        this.back = newback;

        joinProperties(ds);
    }


    /**
     * trim operator copies the data into a new dataset.
     * @param start
     * @param end
     * @return
     */
    @Override
    public QDataSet trim(int start, int end) {
        int nrank = this.rank;
        int noff1= start * len1 * len2 * len3;
        int noff2= end * len1 * len2 * len3;
        float[] newback = new float[noff2-noff1];
        System.arraycopy( this.back, noff1, newback, 0, noff2-noff1 );
        TrFDataSet result= new TrFDataSet( nrank, end-start, len1, len2, len3, newback );
        Map<String,Object> props= DataSetUtil.getProperties(this);
        Map<String,Object> depProps= DataSetUtil.trimProperties( this, start, end );
        props.putAll(depProps);
        DataSetUtil.putProperties( props, result );
        return result;
    }

    /**
     * TODO: this is untested, but is left in to demonstrate how the capability
     * method should be implemented.  Clients should use this instead of
     * casting the class to the capability class.
     * @param <T>
     * @param clazz
     * @return
     */
    @Override
    public <T> T capability(Class<T> clazz) {
        if ( clazz==WritableDataSet.class ) {
            return (T) this;
        } else {
            return super.capability(clazz);
        }
    }


}