/*
 * DataSetUtil.java
 *
 * Created on January 26, 2004, 6:03 PM
 */

package org.das2.dataset;

import org.das2.datum.CacheTag;
import org.das2.datum.DatumRange;
import org.das2.datum.Units;
import org.das2.datum.DatumVector;
import org.das2.datum.Datum;
import java.util.*;

/**
 *
 * @author  Jeremy
 */
public class DataSetUtil {
    
    public static CacheTag guessCacheTag( DataSet ds ) {
        if ( ds.getProperty(DataSet.PROPERTY_CACHE_TAG)!=null ) {
            return (CacheTag)ds.getProperty(DataSet.PROPERTY_CACHE_TAG);
        } else {
            Datum start= ds.getXTagDatum(0);
            Datum end= ds.getXTagDatum( ds.getXLength()-1 );
            Datum resolution= ds.getXTagDatum(1).subtract( start );
            return new CacheTag( start, end, resolution );
        }
    }

    /**
     * returns the xrange of the dataset.  This assumes that the xtags
     * are monotonic.
     * @param ds
     * @return
     */
    public static DatumRange xRange( DataSet ds ) {
        int n=ds.getXLength();
        return new DatumRange( ds.getXTagDatum(0), ds.getXTagDatum(n-1) );
    }
    
    private static DatumRange yRangeTDS( TableDataSet ds ) {
        DatumRange result=null;
        if ( ds.tableCount()==0 ) {
            return new DatumRange(0,10,ds.getYUnits());
        } else {
            for ( int i=0; i<ds.tableCount(); i++ ) {
                int n= ds.getYLength(i);
                DatumRange d;
                if (  ds.getYTagDatum(i,0).le( ds.getYTagDatum(i,n-1) ) ) {
                    d= new DatumRange( ds.getYTagDatum(i,0), ds.getYTagDatum(i,n-1) );
                } else {
                    d= new DatumRange( ds.getYTagDatum(i,n-1), ds.getYTagDatum(i,0)  ); // monotonic decreasing
                }
                
                if ( result==null ) {
                    result= d;
                } else {
                    result= result.include( d.min() ).include( d.max() );
                }
            }
            return result;
        }
    }
    
    public static DatumRange yRange( DataSet ds ) {
        if ( ds.getProperty(DataSet.PROPERTY_Y_RANGE)!=null ) return (DatumRange)ds.getProperty(DataSet.PROPERTY_Y_RANGE);
        if ( ds instanceof VectorDataSet ) {
            VectorDataSet vds= ( VectorDataSet ) ds;
            Datum min=null, max=null;
            DatumRange result;
            for ( int i=0; i<ds.getXLength(); i++ ) {
                Datum d= vds.getDatum(i);
                if ( !d.isFill() ) {
                    if ( min==null ) {
                        max= min= d;
                    } else {
                        if ( d.lt(min) ) min=d;
                        if ( d.gt(max) ) max=d;
                    }
                }
            }
            if ( min==null ) {
                result= new DatumRange(0,10,ds.getYUnits());
            } else {
                result= new DatumRange( min, max );
            }
            return result;
        } else if ( ds instanceof TableDataSet ) {
            
            return yRangeTDS( ( TableDataSet ) ds );
            
        } else throw new IllegalArgumentException("unsupported: "+ds);
    }
    
    
    /**
     * Provide a reasonable xTagWidth either by returning the specified xTagWidth property,
     * or by statistically looking at the X tags.
     */
    public static Datum guessXTagWidth( DataSet table ) {
        if ( table.getProperty(DataSet.PROPERTY_X_TAG_WIDTH)!=null ) return (Datum)table.getProperty(DataSet.PROPERTY_X_TAG_WIDTH);
        if ( table.getXLength()>1 ) {
            Units units= table.getXUnits();
            double min= Math.abs( table.getXTagDouble(1,units) - table.getXTagDouble( 0,units) );
            for ( int i=2; i<table.getXLength(); i++ ) {
                double min0= Math.abs( table.getXTagDouble(i,units) - table.getXTagDouble( i-1,units) );
                if ( min0 < min ) min= min0;
            }
            return units.getOffsetUnits().createDatum( min );
        } else {
            // We're in trouble now!
            return table.getXUnits().getOffsetUnits().createDatum(0);
        }
    }
    
    /**
     * finds the element closest to x in monotonic array xx. 
     * @param xx monotonic array of numbers.  When xx is decreasing, a copy
     *   of the array is made.
     * @param x
     * @return index of closest value.
     */
    protected static int closest( double[] xx, double x ) {
        if ( xx[xx.length-1]<xx[0] ) {
            double[] minusxx= new double[xx.length];
            for ( int i=0; i<xx.length; i++ ) minusxx[i]= -1 * xx[i];
            xx= minusxx;
            x= -1*x;
        }
        if ( xx.length==0 ) {
            throw new IllegalArgumentException("array has no elements");
        }
        int result= Arrays.binarySearch( xx, x );
        if (result == -1) {
            result = 0; //insertion point is 0
        } else if (result < 0) {
            result= ~result; // usually this is the case
            if ( result == xx.length ) {
                result= xx.length-1;
            } else {
                result= ( ( x-xx[result-1] ) / ( xx[result] - xx[result-1] ) < 0.5 ? result-1 : result );
            }
        }
        return result;
    }
    
    /**
     * returns the index of a tag, or the  <tt>(-(<i>insertion point</i>) - 1)</tt>.  (See Arrays.binarySearch)
     */
    public static int xTagBinarySearch( DataSet ds, Datum datum, int low, int high ) {
        Units units= datum.getUnits();
        double key= datum.doubleValue(units);
        while (low <= high) {
            int mid = (low + high) >> 1;
            double midVal = ds.getXTagDouble(mid,units);
            int cmp;
            if (midVal < key) {
                cmp = -1;   // Neither val is NaN, thisVal is smaller
            } else if (midVal > key) {
                cmp = 1;    // Neither val is NaN, thisVal is larger
            } else {
                long midBits = Double.doubleToLongBits(midVal);
                long keyBits = Double.doubleToLongBits(key);
                cmp = (midBits == keyBits ?  0 : // Values are equal
                    (midBits < keyBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
                        1));                     // (0.0, -0.0) or (NaN, !NaN)
            }
            
            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }
    
    
    public static int closestColumn( DataSet table, Datum datum ) {
        int result= xTagBinarySearch( table, datum, 0, table.getXLength()-1 );
        if (result == -1) {
            result = 0; //insertion point is 0
        } else if (result < 0) {
            result= ~result; // usually this is the case
            if ( result >= table.getXLength()-1 ) {
                result= table.getXLength()-1;
            } else {
                double x= datum.doubleValue( datum.getUnits() );
                double x0= table.getXTagDouble(result-1, datum.getUnits() );
                double x1= table.getXTagDouble(result, datum.getUnits() );
                result= ( ( x-x0 ) / ( x1 - x0 ) < 0.5 ? result-1 : result );
            }
        }
        return result;
    }
    
    public static int closestColumn( DataSet table, double x, Units units ) {
        return closestColumn( table, units.createDatum(x) );
    }
    
    /**
     * finds the dataset column closest to the value, starting the search at guessIndex.
     * Handles monotonically increasing or decreasing tags.
     * @param table
     * @param xdatum
     * @param guessIndex
     * @return
     */
    public static int closestColumn( DataSet table, Datum xdatum, int guessIndex ) {
        int monotonicDir= 1;
        int result= guessIndex;
        int len= table.getXLength();
        if ( len==1 ) return 0;
        
        if ( result>=len-1 ) result=len-2;
        if ( result<0 ) result=0;
        
        Units units= xdatum.getUnits();
        
        if ( table.getXTagDouble(result, units) > table.getXTagDouble(result+1, units ) ) {
            monotonicDir= -1;
        }
        
        double x= xdatum.doubleValue(units);
        if ( monotonicDir==1 ) {
            while ( result<(len-1) && table.getXTagDouble(result,units) < x ) result++;
            while ( result>0 && table.getXTagDouble(result,units) > x ) result--;
        } else {
            while ( result<(len-1) && table.getXTagDouble(result,units) > x ) result++;
            while ( result>0 && table.getXTagDouble(result,units) < x ) result--;            
        }
        // result should now point to the guy to the left of x, unless x>the last guy or <the first guy
        if ( result<len-1 ) {
            double alpha= ( x - table.getXTagDouble( result, units ) ) /
                    ( table.getXTagDouble( result+1, units ) - table.getXTagDouble( result, units ) );
            result= ( alpha < 0.5 ? result : result+1 );
        }
        return result;
    }
    
    /**
     * returns the first column that is before the given datum.  Note the
     * if the datum identifies (==) an xtag, then the previous column is
     * returned.
     */
    public static int getPreviousColumn( DataSet ds, Datum datum ) {
        int i= closestColumn( ds, datum );
        // TODO: consider the virtue of ge
        if ( i>0 && ds.getXTagDatum(i).ge(datum) ) {
            return i-1;
        } else {
            return i;
        }
    }
    
    /**
     * returns the first column that is after the given datum.  Note the
     * if the datum identifies (==) an xtag, then the previous column is
     * returned.
     */
    public static int getNextColumn( DataSet ds, Datum datum ) {
        int i= closestColumn( ds, datum );
        // TODO: consider the virtue of le
        if ( i<ds.getXLength()-1 && ds.getXTagDatum(i).le(datum) ) {
            return i+1;
        } else {
            return i;
        }
    }
    
    public static double[] getXTagArrayDouble( DataSet vds, Units units ) {
        int ixmax= vds.getXLength();
        double[] xx= new double[ixmax];
        for ( int i=0; i<ixmax; i++ ) {
            xx[i]= vds.getXTagDouble(i,units);
        }
        return xx;
    }
    
    
    public static DatumVector getXTags( DataSet ds ) {
        double[] data= VectorUtil.getXTagArrayDouble(ds,ds.getXUnits());
        return DatumVector.newDatumVector(data,ds.getXUnits());
    }
    
    /**
     * guess a range that characterizes the data.  The DataSet property name
     * DataSet.PROPERTY_Z_RANGE can be used by the dataset to explicitly 
     * set this.
     * 
     * @return a DatumRange useful for setting axis range.
     * @throws UnsupportedOperationException if the dataset is not a TableDataSet.
     */
    public static DatumRange zRange( DataSet ds ) {
        if ( !( ds instanceof TableDataSet ) ) {
            throw new UnsupportedOperationException("only TableDataSets supported");
        }
        
        if ( ds.getProperty(DataSet.PROPERTY_Z_RANGE)!=null ) {
            return (DatumRange)ds.getProperty(DataSet.PROPERTY_Z_RANGE);
        }
        
        TableDataSet tds= (TableDataSet)ds;
        
        double max= Double.NEGATIVE_INFINITY;
        double min= Double.POSITIVE_INFINITY;
        
        double fill= tds.getZUnits().getFillDouble();
        Units zunits= tds.getZUnits();
        
        for ( int itable=0; itable<tds.tableCount(); itable++ ) {
            int ny= tds.getYLength(itable);
            for (int i=tds.tableStart(itable); i<tds.tableEnd(itable); i++) {
                for (int j=0; j<ny; j++) {
                    double d= tds.getDouble(i,j,zunits);
                    if ( zunits.isValid(d) ) {
                        max= Math.max( d, max );
                        min= Math.min( d, min );
                    }
                }
            }
        }
        if ( max==Double.NEGATIVE_INFINITY ) {
            max= fill;
            min= fill;
        }
        return DatumRange.newDatumRange( min, max, zunits );
        
    }

            
    /** Give an estimate of the size of the data set, or PROPERTY_SIZE_BYTES if available.
     * Generally this would be used as a penalty against the dataset, and it's probably
     * better to overestimate the dataset size.
     */
    public static long guessSizeBytes( DataSet ds ) {
        Long o= (Long)ds.getProperty( DataSet.PROPERTY_SIZE_BYTES );
        if ( o != null ) {
            return o.longValue();
        } else {
            long planeCount= ds.getPlaneIds().length;
            int datumBytes= 8; // storage size for each datum
            long sizeXTags= ds.getXLength() * datumBytes;
            if ( ds instanceof TableDataSet ) {
                TableDataSet tds= (TableDataSet)ds;
                long sizeBytes=0;
                for ( int i=0; i<tds.tableCount(); i++ ) {
                    sizeBytes+= ( tds.tableEnd(i)-tds.tableStart(i) ) * tds.getYLength(i) * planeCount * datumBytes;
                }
                return sizeBytes + sizeXTags;
            } else {
                return ds.getXLength() * planeCount * datumBytes + sizeXTags;
            }
        }
    }
    
    /**
     * provides convenient method for appending datasets together.  The first
     * dataset may be null, in which case the second is trivially returned.
     * Presently a builder is used to create the new dataset, but in the future
     * more efficient methods will be used.  If both ds1 and ds2 are null, then
     * null is returned.
     *
     * @param ds1 the first data set.  May be null.
     * @param ds2 the second data set.  This data set should be after ds1, and
     *    may be null.
     * @param ct the cache tag for the second dataset.  May be null, in which
     *    case guessCacheTag is used to identify the dataset cache tag.
     */
    public static DataSet append( DataSet ds1, DataSet ds2, CacheTag ct ) {
        CacheTag resultTag=null;
        if ( ct!=null ) {
            resultTag= ct;
        } else {
            if ( ds2!=null ) resultTag= DataSetUtil.guessCacheTag( ds2 );
        }
        if ( ds1!=null ) {
            if ( resultTag==null ) {
                resultTag= DataSetUtil.guessCacheTag( ds1 );
            } else {
                resultTag= CacheTag.append( DataSetUtil.guessCacheTag( ds1 ), resultTag );
            }
        }
        if ( ds2==null ) {
            return ds1;
        } else {
            if ( ds2 instanceof TableDataSet ) {
                TableDataSetBuilder builder= new TableDataSetBuilder( ds2.getXUnits(), ds2.getYUnits(), ((TableDataSet)ds2).getZUnits() );
                if ( ds1!=null ) builder.append( (TableDataSet) ds1 );
                builder.append( (TableDataSet) ds2 );
                builder.setProperty( DataSet.PROPERTY_CACHE_TAG, resultTag );
                return builder.toTableDataSet();
            } else {
                VectorDataSetBuilder builder= new VectorDataSetBuilder( ds2.getXUnits(), ds2.getYUnits() );
                if ( ds1!=null ) builder.append( (VectorDataSet) ds1 );
                builder.append( (VectorDataSet) ds2 );
                builder.setProperty( DataSet.PROPERTY_CACHE_TAG, resultTag );
                return builder.toVectorDataSet();
            }
        }
    }
    
    /**
     * provides convenient method for appending datasets together.  The first
     * dataset may be null, in which case the second is trivially returned.
     * Presently a builder is used to create the new dataset, but in the future
     * more efficient methods will be used.  If both ds1 and ds2 are null, then
     * null is returned.
     *
     * @param ds1 the first data set.  May be null.
     * @param ds2 the second data set.  This data set should be after ds1, and
     *    may be null.
     */
    public static DataSet append( DataSet ds1, DataSet ds2 ) {
        return append( ds1, ds2, null );
    }
    
    public static VectorDataSet log10( VectorDataSet ds ) {
        VectorDataSetBuilder builder= new VectorDataSetBuilder( ds.getXUnits(), Units.dimensionless );
        Units yunits= ds.getYUnits();
        Units xunits= ds.getXUnits();
        for ( int i=0; i<ds.getXLength(); i++ ) {
            builder.insertY( ds.getXTagDouble(i,xunits), Math.log10(ds.getDouble(i,yunits)) );
        }
        return builder.toVectorDataSet();
    }
    
    /**
     * returns all planes, including the default plane "".  This is to take care of
     * inconsistent behavior of the ds.getPlaneIds() implementations.
     */
    public static String[] getAllPlaneIds( DataSet ds ) {
        String[] planes= ds.getPlaneIds();
        boolean haveDefault=false;
        for ( int i=0; i<planes.length; i++ ) {
            if ( planes[i].equals("") ) haveDefault=true;
        }
        if ( haveDefault ) {
            return planes;
        } else {
            String[] newPlanes= new String[ planes.length+1] ;
            newPlanes[0]= "";
            for ( int i=0; i<planes.length; i++ ) {
                newPlanes[i+1]= planes[i];
            }
            return newPlanes;
        }
    }

    public static DataSet getWeightsDataSet( DataSet ds ) {
        DataSet wds= ds.getPlanarView( DataSet.PROPERTY_PLANE_WEIGHTS );
        if ( wds!=null ) {
            return wds;
        }
        if ( ds instanceof TableDataSet ) {
            return WeightsTableDataSet.create(  (TableDataSet)ds );
        } else {
            return WeightsVectorDataSet.create( (VectorDataSet)ds );
        }
    }
}
