/*
 * JoinDataSet.java
 *
 * Created on April 27, 2007, 10:52 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.das2.qds;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.das2.datum.Units;

/**
 * Create a higher rank dataset with dim 0 being a JOIN dimension.  Join implies
 * that the joined datasets occupy the same physical dimension, and this can
 * be thought of as the "array of" index.  Note DEPEND_0 is treated as a
 * special case of join.
 * Note this dataset is mutable, and clients should not mutate it once the reference is made public.
 * @author jbf
 */
public class JoinDataSet extends AbstractDataSet {

    /**
     * if we haven't joined any datasets, then report this size when length(i) is
     * queried.  QDataSet supports DataSet[0,20,20].  Somewhere there is code
     * that assumes a qube...
     */
    private static final int NO_DATASET_SIZE = 999;

    /**
     * return a copy of the dataset, creating a new JoinDataSet and 
     * copying each slice into the JoinDataSet.
     * @param rds any dataset
     * @return a copy of the original dataset.
     * @deprecated see WritableJoinDataSet
     */
    public static QDataSet deepCopy( QDataSet rds ) {
        if ( DataSetUtil.isQube(rds) ) {
            return ArrayDataSet.copy(rds);
        }
        JoinDataSet result= new JoinDataSet( rds.rank() );
        for ( int i=0; i<rds.length(); i++ ) {
            QDataSet s1= rds.slice(i);
            if ( DataSetUtil.isQube(s1) ) { //Note QDataSet all but says that Join can only have slices that are qubes.
                result.join( ArrayDataSet.copy( s1 ) );
            } else {
                result.join( deepCopy(s1) );
            }
        }
        Map srcProps= DataSetUtil.getProperties(rds);
        DataSetUtil.putProperties( srcProps, result );
        return result;
    }
    
    List<QDataSet> datasets;
    /**
     * rank of the dataset.  Joined DataSets should have rank rank-1.
     */
    int rank;
    
    /** 
     * Creates a new instance of JoinDataSet 
     * @param rank The rank of the JoinDataSet.  Each dataset joined must have rank <tt>rank</tt>-1.
     */
    public JoinDataSet( int rank ) {
        this.rank= rank;
        putProperty(QDataSet.JOIN_0,  DDataSet.create( new int[0] ) ); // rank 0 dataset allows properties like CACHE_TAG to be stored here.
        //putProperty(QDataSet.JOIN_0,  "DEPEND_1" );
        datasets= new ArrayList<>();
    }

    /**
     * create a new JoinDataSet, and join the first dataset.
     *<blockquote><pre>
     *ds1= Ops.rand(30);
     *jds= new JoinDataSet( ds1 );
     *assert( ds1.rank()==1 );
     *assert( jds.rank()==2 );
     *assert( jds.slice(0).equals(ds1) );
     *</pre></blockquote>
     * @param ds1 rank N-1 dataset that will be the first slice to add to this rank N dataset.
     */
    public JoinDataSet( QDataSet ds1 ) {
        this( ds1.rank()+1 );
        join(ds1);
    }

    /**
     * copy the JoinDataSet without copying each dataset it contains.
     * @param joinDataSet another JoinDataSet
     * @return a copy of the dataset.
     */
    public static JoinDataSet copy(JoinDataSet joinDataSet) {
        JoinDataSet result= new JoinDataSet(joinDataSet.rank());
        result.datasets.addAll( joinDataSet.datasets );
        DataSetUtil.putProperties( joinDataSet.properties, result );
        DataSetUtil.putProperties( DataSetUtil.getProperties(joinDataSet), result );
        result.putProperty(QDataSet.DEPEND_0, joinDataSet.property(QDataSet.DEPEND_0));
        result.putProperty(QDataSet.JOIN_0, joinDataSet.property(QDataSet.JOIN_0) ); //might need to clear it if DEPEND_0 is set.
        return result;
    }


    /**
     * copy all the records into this JoinDataSet.  Note this is
     * a shallow copy, and changes to one of the element datasets is visible
     * in both JoinDataSets.
     * TODO: this is probably under implemented, for example element properties.
     * @param ds1
     */
    public void joinAll( JoinDataSet ds1 ) {
        for ( int j=0; j<ds1.length(); j++ ) {
            join(ds1.slice(j));
        }
        QDataSet dep0= (QDataSet) ds1.property(QDataSet.DEPEND_0);
        if ( dep0!=null ) {
            QDataSet thisDep0= (QDataSet) this.property(QDataSet.DEPEND_0);
            if ( thisDep0!=null ) {
                DDataSet dd= (DDataSet) DDataSet.maybeCopy( thisDep0 );
                DDataSet dd1= (DDataSet) DDataSet.maybeCopy(dep0);
                if ( !dd.canAppend(dd1) ) {
                    dd.grow( dd.length() + dd1.length() );
                }
                dd.append( dd1 );
                this.putProperty( QDataSet.DEPEND_0,dd );
            } else {
                throw new IllegalArgumentException("joinAll datasets one has depend_0 but other doesn't");
            }
        }
    }

    /**
     * add the dataset to this set of joined datasets.
     * @param ds rank N-1 dataset where N is the rank of this JoinDataSet.
     * @throws IllegalArgumentException if the dataset rank is not consistent with the other datasets.
     */
    public final void join( QDataSet ds ) {
        if ( ds==null ) {
            throw new IllegalArgumentException("dataset is null");
        }
        if ( ds.rank()!=this.rank-1 ) {
            throw new IllegalArgumentException("dataset rank must be "+(this.rank-1)+", it is rank "+ds.rank() );
        }
        
        // JoinDataSet is used produce a bounds object of BINS datasets with different units.  This is allowed, 
        // but the "JOIN_0" property is cleared.  "strict" JOINs won't allow this.
        if ( properties.get(QDataSet.JOIN_0)!=null && datasets.size()>0 ) {
            QDataSet firstDataSet = datasets.get(0);
            Units units0= SemanticOps.getUnits(firstDataSet);
            Units units1= SemanticOps.getUnits(ds);
            if ( !units1.isConvertibleTo(units0) ) {
                properties.remove(QDataSet.JOIN_0);
                properties.remove(QDataSet.UNITS);
            } else if ( units0==units1 ) {
                properties.put(QDataSet.UNITS,units0);
            }
        }
        
        if ( ds.rank()==0 ) {
            QDataSet context= (QDataSet) ds.property(QDataSet.CONTEXT_0);
            if ( context!=null ) {
                QDataSet dep0= (QDataSet) property(QDataSet.DEPEND_0);
                if ( dep0 instanceof JoinDataSet ) {
                    ((JoinDataSet)dep0).join(context);
                }
            }
        }
        datasets.add( ds );
    }
    
    /**
     * set the dataset at the index.  This must be used sequentially.
     * @param index the index of the dataset
     * @param ds the dataset
     * @throws IllegalArgumentException if the dataset rank is not consistent with the other datasets.
     */
    public final void join( int index, QDataSet ds ) {
        if ( ds==null )  throw new IllegalArgumentException("dataset is null");
        if ( ds.rank()!=this.rank-1 ) throw new IllegalArgumentException("dataset rank must be "+(this.rank-1)+", it is rank "+ds.rank() );
        if ( index==datasets.size() ) {
            datasets.add(index,ds);            
        } else {
            datasets.set(index,ds);
        }
    }

    @Override
    public int rank() {
        return rank;
    }

    @Override
    public double value(int i0) {
        if ( rank!=1 ) throw new IllegalArgumentException("rank 1 access on rank "+rank+" join dataset");
        return datasets.get(i0).value();
    }

    @Override
    public double value(int i0, int i1) {
        if ( rank!=2 ) throw new IllegalArgumentException("rank 2 access on rank "+rank+" join dataset");
        return datasets.get(i0).value(i1);
    }

    @Override
    public double value(int i0, int i1, int i2) {
        if ( rank!=3 ) throw new IllegalArgumentException("rank 3 access on rank "+rank+" join dataset");        
        return datasets.get(i0).value(i1,i2);
    }

    @Override
    public double value(int i0, int i1, int i2, int i3 ) {
        if ( rank!=4 ) throw new IllegalArgumentException("rank 4 access on rank "+rank+" join dataset");        
        return datasets.get(i0).value(i1,i2,i3);
    }

    @Override
    public Object property(String name, int i0) {
        Object result= super.property(name, i0);
        if ( result==null ) {
            if ( i0>=datasets.size() ) {
                throw new IndexOutOfBoundsException("no dataset at index: "+i0);
            }
            return datasets.get(i0).property(name);
        } else {
            return result;
        }
    }
    
    @Override
    public Object property(String name) {
        Object result= properties.get(name);
        if ( result==null && name.equals(QDataSet.UNITS) && datasets.size()>0 ) {
            Object p0= datasets.get(0).property(name);
            Object p1= datasets.get(datasets.size()-1).property(name);
            if ( p0==p1 || ( p0!=null && p0.equals(p1) ) ) {
                return p0;
            } else {
                return null;
            }
        } else {
            return result;
        }
    }

    /**
     * We override putProperty here because we remove the JOIN_0 if DEPEND_0 is set.
     * @param name
     * @param value
     */
    @Override
    public final void putProperty(String name, Object value) {
        super.putProperty(name, value);
        if ( name.equals(QDataSet.DEPEND_0) ) {
            super.putProperty(QDataSet.JOIN_0, null);
        }
    }

    @Override
    public int length() {
        return datasets.size();
    }

    @Override
    public int length(int i0) {
        if ( datasets.isEmpty() && i0==0 ) {
            return NO_DATASET_SIZE;
        }
        return datasets.get(i0).length();
    }

    @Override
    public int length(int i0, int i1) {
        if ( datasets.isEmpty() && i0==0 ) {
            return NO_DATASET_SIZE;
        }
        return datasets.get(i0).length(i1);
    }

    @Override
    public int length(int i0, int i1, int i2 ) {
        if ( datasets.isEmpty() && i0==0 ) {
            return NO_DATASET_SIZE;
        }
        return datasets.get(i0).length(i1,i2);
    }

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

    @Override
    public JoinDataSet trim( int imin, int imax ) {
        JoinDataSet result= new JoinDataSet(this.rank);
        result.datasets= new ArrayList<QDataSet>(imax-imin);
        result.datasets.addAll( this.datasets.subList(imin, imax) );
        result.properties.putAll( this.properties );
        QDataSet dep0= (QDataSet) property( QDataSet.DEPEND_0 );
        if ( dep0!=null ) result.properties.put( QDataSet.DEPEND_0, dep0.trim(imin,imax) );
        for ( int i=1; i<this.rank; i++ ) {
            QDataSet dep= (QDataSet)this.property("DEPEND_"+i);
            if ( dep!=null && dep.rank()>1 ) {
                result.properties.put( "DEPEND_"+i, dep.trim(imin,imax) );
            }
        }
        return result;
    }

    /**
     * slice the dataset by returning the joined dataset at this index.  If the
     * dataset is a MutablePropertiesDataSet, then add the properties of this
     * join dataset to it.  The result is made a mutable properties dataset if it 
     * is not already, the danger here is that we may mutate the original data.
     * Capabilities will fix this.
     * @param idx the index for the slice
     * @return the rank N-1 slice at the position.
     */
    @Override
    public QDataSet slice( int idx ) {
        QDataSet result= datasets.get(idx);
        MutablePropertyDataSet mpds= DataSetOps.makePropertiesMutable(result);
        Map<String,Object> props= DataSetOps.sliceProperties0( idx, properties );
        DataSetUtil.putProperties(props, mpds); 
        mpds.makeImmutable();
        return result;
    }
}
