package gov.nasa.gsfc.spdf.cdfj;
import java.io.*;
import java.nio.*;
import java.util.*;
import java.util.zip.*;
import java.lang.reflect.*;
public class TSExtractor extends Extractor {
    static {
        try {
            Class variableClass =
                Class.forName("gov.nasa.gsfc.spdf.cdfj.Variable");
            Class rdrClass = 
                Class.forName("gov.nasa.gsfc.spdf.cdfj.MetaData");
            Class cl = 
                Class.forName("gov.nasa.gsfc.spdf.cdfj.TSExtractor");
            Class timeSpecClass =
                Class.forName("gov.nasa.gsfc.spdf.cdfj.TimeInstantModel");
            double[] da = new double[0];
            int[] ia = new int[0];
            Class[][] arglist;
            arglist = new Class[][]{
                new Class[] {rdrClass, variableClass, Boolean.class,
                     da.getClass()},
                new Class[] {rdrClass, variableClass, Integer.class,
                     Boolean.class, da.getClass()},
                null,
                null
            };
            addFunction("TimeSeries", cl, arglist);
            arglist = new Class[][]{
                new Class[] {rdrClass, variableClass, Boolean.class,
                     da.getClass(), ia.getClass()},
                new Class[] {rdrClass, variableClass, Integer.class,
                     Boolean.class, da.getClass(), ia.getClass()},
                null,
                null
            };
            addFunction("SampledTimeSeries", cl, arglist);

            arglist = new Class[][]{
                new Class[] {rdrClass, variableClass, Boolean.class,
                     da.getClass(), timeSpecClass},
                new Class[] {rdrClass, variableClass, Integer.class,
                     Boolean.class, da.getClass(), timeSpecClass},
                null,
                null
            };
            addFunction("TimeSeriesObject", cl, arglist);

        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }
    public static double [][] getTimeSeries0(MetaData rdr, Variable var,
        Boolean ignoreFill, double[] timeRange) throws Throwable {
        return getTimeSeries(rdr, var, null, ignoreFill, timeRange);
    }
    public static double [][] getTimeSeries1(MetaData rdr, Variable var,
        Integer which, Boolean ignoreFill, double[] timeRange)
        throws Throwable {
        return getTimeSeries(rdr, var, which, ignoreFill, timeRange);
    }
    public static double [][] getTimeSeries(MetaData rdr, Variable var,
        Integer which, Boolean ignoreFill, double[] timeRange) throws Throwable
        {
        if (var.getNumberOfValues() == 0) return null;
        boolean ignore = ignoreFill.booleanValue();
        double [] vdata;
        int [] recordRange = null;
        TimeVariable tv = TimeVariableFactory.getTimeVariable(rdr,
            var.getName());
        double [] times = tv.getTimes();
        if (times == null) return null;
        boolean longType = false;
        int type = var.getType();
        int element = (which == null)?0:which.intValue();
        Number pad;
        if (DataTypes.typeCategory[type] == DataTypes.LONG) {
            longType = true;
            pad = new Long(((long[])getPadValue(rdr.thisCDF, var))[element]);
        } else {
            pad =
                new Double(((double[])getPadValue(rdr.thisCDF, var))[element]);
        }
        double[] stimes;
        Object o = null;
        Object[] oa = null;
        if (timeRange == null) {
            o = (which == null)?Extractor.getSeries0(rdr.thisCDF, var):
                                Extractor.getElement1(rdr.thisCDF, var, which);
            if (var.isMissingRecords()) {
                long[][] locations = var.getLocator().getLocations();
                oa = filterPad(o, times, pad, locations, 0);
            } else {
                oa = new Object[]{times, (double[])o};
            }
        } else {
            recordRange = getRecordRange(rdr, var, timeRange);
            if (recordRange == null) return null;
            if (which == null) {
                o = getRange0(rdr.thisCDF, var, new Integer(recordRange[0]),
                                  new Integer(recordRange[1]));
            } else {
                o = getRangeForElement1(rdr.thisCDF, var,
                    new Integer(recordRange[0]), new Integer(recordRange[1]),
                    which);
            }
            stimes = new double[Array.getLength(o)];
            int index = recordRange[0];
            for (int i = 0; i < stimes.length; i++) {
                stimes[i] = times[index++];
            }
            if (var.isMissingRecords()) {
                long[][] locations = var.getLocator().getLocations();
                oa = filterPad(o, stimes, pad, locations, recordRange[0]);
            } else {
                oa = new Object[]{stimes, (double[])o};
            }
        }
        stimes = (double[])oa[0];
        if (!ignore) {
            vdata = castToDouble(oa[1], longType);
            return new double [][] {stimes, vdata};
        }
        // fill values need to be filtered
        Object fill = Extractor.getFillValue(rdr.thisCDF, var);
        boolean fillDefined = true;
        Number fillValue = null;
        if (fill.getClass().getComponentType() == Double.TYPE) {
            fillDefined =  (((double[])fill)[0] == 0);
            if (fillDefined) fillValue = new Double(((double[])fill)[1]);
        } else {
            fillDefined =  (((long[])fill)[0] == 0);
            if (fillDefined) fillValue = new Long(((long[])fill)[1]);
        }
        if (!fillDefined) {
            vdata = castToDouble(oa[1], longType);
            return new double [][] {stimes, vdata};
        }
        return filterFill(stimes, oa[1], fillValue);
    }
    static int[] getRecordRange(MetaData rdr,Variable var,
        double[] timeRange) {
        return getRecordRange(rdr, var, timeRange, null);
    }

    static int[] getRecordRange(MetaData rdr, Variable var,
        double[] timeRange, TimeInstantModel ts) {
        try {
            TimeVariableX tvx = (TimeVariableX)
                TimeVariableFactory.getTimeVariable(rdr, var.getName());
            return tvx.getRecordRange(timeRange);
        } catch (Throwable t) {
        }
        return null;
    }
    public static double [][] filterFill(double[] times, double [] vdata,
        double fill, int first) {
        double [][] series;
        int count = 0;
        for (int i = 0; i < vdata.length; i++) {
            if (vdata[i] != fill) count++;
        }
        series = new double[2][count];
        int n = 0;
        for (int i = 0; i < vdata.length; i++) {
            if (vdata[i] == fill) continue;
            series[0][n] = times[i + first];
            series[1][n] = vdata[i];
            n++;
        }
        return series;
    }
    public static double [][] filterFill(double[] times, Object o,
        Number fillValue) {
        double [][] series;
        int count = 0;
        if (o.getClass().getComponentType() == Long.TYPE) {
            long fill = fillValue.longValue();
            long[] ldata = (long[])o;
            for (int i = 0; i < ldata.length; i++) {
                if (ldata[i] != fill) count++;
            }
            series = new double[2][count];
            int n = 0;
            for (int i = 0; i < ldata.length; i++) {
                if (ldata[i] == fill) continue;
                series[0][n] = times[i];
                series[1][n] = (double)ldata[i];
                n++;
            }
        } else {
            if (o.getClass().getComponentType() != Double.TYPE) return null;
            double fill = fillValue.doubleValue();
            double[] data = (double[])o;
            for (int i = 0; i < data.length; i++) {
                if (data[i] != fill) count++;
            }
            series = new double[2][count];
            int n = 0;
            for (int i = 0; i < data.length; i++) {
                if (data[i] == fill) continue;
                series[0][n] = times[i];
                series[1][n] = data[i];
                n++;
            }
        }
        return series;
    }
    static Object[] filterPad(Object o, double[] times, Number pad,
        long[][] locations, int first) {
        RecordSensor sensor = new RecordSensor(locations);
        if (o.getClass().getComponentType() == Double.TYPE) {
            double dpad = pad.doubleValue();
            double[] vdata = (double[])o;
            int npad = 0;
            for (int i = 0; i < vdata.length; i++) {
                if (sensor.hasRecord(first + i)) continue;
                if (vdata[i] == dpad) npad++;
            }
            if (npad == 0) return new Object[] {times, vdata};
            double[] _data = new double[vdata.length - npad];
            double[] _times = new double[vdata.length - npad];
            int index = 0;
            for (int i = 0; i < vdata.length; i++) {
                if (sensor.hasRecord(first + i) || (vdata[i] != dpad)) {
                    _data[index] = vdata[i];
                    _times[index] = times[i];
                    index++;
                }
            }
            return new Object[] {_times, _data};
        } else {
            if (!(o.getClass().getComponentType() == Long.TYPE)) return null;
            long lpad = pad.longValue();
            long[] ldata = (long[])o;
            int npad = 0;
            for (int i = 0; i < ldata.length; i++) {
                if (sensor.hasRecord(first + i)) continue;
                if (ldata[i] == lpad) npad++;
            }
            if (npad == 0) {
                return new Object[] {times, ldata};
            }
            long[] _data = new long[ldata.length - npad];
            double[] _times = new double[ldata.length - npad];
            int index = 0;
            for (int i = 0; i < ldata.length; i++) {
                if (sensor.hasRecord(first + i) || (ldata[i] != lpad)) {
                    _data[index] = ldata[i];
                    _times[index] = times[i];
                    index++;
                }
            }
            return new Object[] {_times, _data};
        }
    }
    public static double [][] getSampledTimeSeries0(MetaData rdr,
        Variable var,
        Boolean ignoreFill, double[] timeRange, int[] stride) throws Throwable
        {
        return getSampledTimeSeries(rdr, var, null, ignoreFill, timeRange,
            stride);
    }
    public static double [][] getSampledTimeSeries1(MetaData rdr,
        Variable var,
        Integer which, Boolean ignoreFill, double[] timeRange, int[] stride)
        throws Throwable {
        return getSampledTimeSeries(rdr, var, which, ignoreFill, timeRange,
            stride);
    }
    public static double [][] getSampledTimeSeries(MetaData rdr,
        Variable var,
        Integer which, Boolean ignoreFill, double[] timeRange, int[] stride)
        throws Throwable {
        if (var.getNumberOfValues() == 0) return null;
        boolean ignore = ignoreFill.booleanValue();
        double [] vdata;
        int [] recordRange = null;
        TimeVariable tv = TimeVariableFactory.getTimeVariable(rdr,
            var.getName());
        double [] times = tv.getTimes();
        if (times == null) return null;
        double[] stimes;
        Stride strideObject = new Stride(stride);
        if (timeRange == null) {
            vdata = (which == null)?
                   (double[])getSeries0(rdr.thisCDF, var, strideObject):
                   (double[])getElement1(rdr.thisCDF, var, which, strideObject);
        } else {
            recordRange = getRecordRange(rdr, var, timeRange);
            if (recordRange == null) return null;
            if (which == null) {
                vdata = (double[])getRange0(rdr.thisCDF, var,
                        new Integer(recordRange[0]),
                                  new Integer(recordRange[1]), strideObject);
            } else {
                vdata = (double[])getRangeForElement1(rdr.thisCDF, var,
                    new Integer(recordRange[0]), new Integer(recordRange[1]),
                    which, strideObject);
            }
        }
        int _stride = strideObject.getStride();
        double [] fill = (double[])getFillValue(rdr.thisCDF, var);
        if ((!ignore) || (fill[0] != 0)) {
            if (timeRange == null) {
                if (_stride == 1) {
                    return new double [][] {times, vdata};
                } else {
                    stimes = new double[vdata.length];
                    for (int i = 0; i < vdata.length; i ++) {
                        stimes[i] = times[i*_stride];
                    }
                    return new double [][] {stimes, vdata};
                }
            } else {
                stimes = new double[vdata.length];
                if (_stride == 1) {
                    System.arraycopy(times, recordRange[0], stimes, 0,
                        vdata.length);
                    return new double [][] {stimes, vdata};
                } else {
                    int srec = recordRange[0];
                    for (int i = 0; i < vdata.length; i ++) {
                        stimes[i] = times[srec + i*_stride];
                    }
                    return new double [][] {stimes, vdata};
                }
            }
        }
        // fill values need to be filtered
        if (timeRange == null) {
            if (_stride == 1) {
                return filterFill(times, vdata, fill[1], 0);
            } else {
                stimes = new double[vdata.length];
                for (int i = 0; i < vdata.length; i ++) {
                    stimes[i] = times[i*_stride];
                }
                return filterFill(stimes, vdata, fill[1], 0);
            }
        } else {
            stimes = new double[vdata.length];
            if (_stride == 1) {
                System.arraycopy(times, recordRange[0], stimes, 0,
                    vdata.length);
                return filterFill(stimes, vdata, fill[1], 0);
            } else {
                int srec = recordRange[0];
                for (int i = 0; i < vdata.length; i ++) {
                    stimes[i] = times[srec + i*_stride];
                }
                return filterFill(stimes, vdata, fill[1], 0);
            }
        }
    }
    public static TimeSeries getTimeSeriesObject0(MetaData rdr, Variable var,
        Boolean ignoreFill, double[] timeRange, TimeInstantModel ts) throws
        Throwable {
        return new GeneralTimeSeries(rdr, var, null, ignoreFill, timeRange,
           ts);
    }

    public static TimeSeries getTimeSeriesObject1(MetaData rdr, Variable var,
        Integer which, Boolean ignoreFill, double[] timeRange,
        TimeInstantModel ts) throws Throwable {
        return new GeneralTimeSeries(rdr, var, which, ignoreFill, timeRange,
           ts);
    }
    /**
     * Loss of precision may occur if type of var is LONG
     * times obtained are millisecond since 1970 regardless of the
     * precision of time variable corresponding to variable var
     */
    public static class GeneralTimeSeries implements TimeSeries {
        double [] vdata;
        double [] times;
        TimeInstantModel tspec;
        double[][] filtered = null;
        public GeneralTimeSeries(MetaData rdr, Variable var, Integer which,
            Boolean ignoreFill, double[] timeRange, TimeInstantModel ts) throws
            Throwable {
            boolean ignore = ignoreFill.booleanValue();
            int [] recordRange = null;
            if (ts != null) {
                synchronized (ts) {
                    tspec = (TimeInstantModel)ts.clone();
                }
            }
            TimeVariable tv = TimeVariableFactory.getTimeVariable(rdr,
                var.getName());
            times = tv.getTimes(tspec);
            if (times == null) throw new Throwable("times not available for " +
                var.getName());
            double[] stimes;
            boolean longType = false;
            int type = var.getType();
            if (DataTypes.typeCategory[type] == DataTypes.LONG) {
                longType = true;
            }
            Object o = null;
            if (timeRange == null) {
                o = (which == null)?getSeries0(rdr.thisCDF, var):
                    getElement1(rdr.thisCDF, var, which);
            } else {
                recordRange = getRecordRange(rdr, var, timeRange, ts);
                if (recordRange == null) throw new Throwable("no record range");
                if (which == null) {
                    o = getRange0(rdr.thisCDF, var, new Integer(recordRange[0]),
                                  new Integer(recordRange[1]));
                } else {
                    o = getRangeForElement1(rdr.thisCDF, var,
                    new Integer(recordRange[0]), new Integer(recordRange[1]),
                    which);
                }
            }
            vdata = castToDouble(o, longType);
            if (!ignore) {
                if (timeRange != null) {
                    stimes = new double[vdata.length];
                    System.arraycopy(times, recordRange[0], stimes, 0,
                    vdata.length);
                    times = stimes;
                }
            } else {
                // fill values need to be filtered
                double [] fill = (double[])getFillValue(rdr.thisCDF, var);
                int first = (timeRange != null)?recordRange[0]:0;
                if (fill[0] != 0) { // there is no fill value
                    stimes = new double[vdata.length];
                    System.arraycopy(times, first, stimes, 0, vdata.length);
                    times = stimes;
                } else {
                    filtered = filterFill(times, vdata, fill[1], first);
                }
            }
        }
        public double[] getTimes() {
            return (filtered != null)?filtered[0]:times;
        }
        public double[] getValues() {
            return (filtered != null)?filtered[1]:vdata;
        }
        public TimeInstantModel getTimeInstantModel() {return tspec;}
    }

    public static class GeneralTimeSeriesX implements TimeSeriesX {
        final TimeInstantModel tspec;
        final TimeVariableX tv;
        final String vname;
        final CDFImpl thisCDF;
        final  double[] timeRange;
        final boolean oned;
        final boolean columnMajor;
        public GeneralTimeSeriesX(MetaData rdr, Variable var,
            Boolean ignoreFill, final double[] timeRange, TimeInstantModel ts,
            boolean oned, boolean columnMajor) throws Throwable {
            boolean ignore = ignoreFill.booleanValue();
            if (ts != null) {
                synchronized (ts) {
                    tspec = (TimeInstantModel)ts.clone();
                }
            } else {
                tspec = null;
            }
            vname = var.getName();
            tv = (TimeVariableX)
                TimeVariableFactory.getTimeVariable(rdr, vname);
            thisCDF = rdr.thisCDF;
            this.timeRange = timeRange;
            this.oned = oned;
            this.columnMajor = columnMajor;
        }

        public double[] getTimes() throws CDFException.ReaderError {
            try {
                if (timeRange == null)  return tv.getTimes(tspec);
                return tv.getTimes(timeRange, tspec);
            } catch (Throwable th) {
                throw new CDFException.ReaderError(th);
            }
        }

        public Object getValues() throws CDFException.ReaderError {
            try {
                if (timeRange == null)  {
                    return (oned)?thisCDF.getOneD(vname,columnMajor):
                           thisCDF.get(vname);
                }
                int[] recordRange = tv.getRecordRange(timeRange);
                if (recordRange == null) {
                    throw new CDFException.ReaderError("no data");
                }
                if (!oned) {
                    return thisCDF.getRange(vname, recordRange[0],
                    recordRange[1]);
                } else {
                    return thisCDF.getRangeOneD(vname, recordRange[0],
                    recordRange[1], columnMajor);
                }
            } catch (Throwable th) {
                throw new CDFException.ReaderError(th);
            }
        }

        public TimeInstantModel getTimeInstantModel() {return tspec;}

        public boolean isOneD() {return oned;}

        public boolean isColumnMajor() {return columnMajor;}
    }
      
    public static String identifier() {return "TSExtractor";}

    static class RecordSensor {
        long[][] locations;
        int last = 0;
        RecordSensor (long[][] locations) {
            this.locations = locations;
        }
        boolean hasRecord(int number) {
            for (int i = last; i < locations.length; i++) {
                if ((number >= locations[i][0]) &&
                    (number <= locations[i][1])) {
                    last = i;
                    return true;
                }
            }
            return false;
        }
    }
    public static Method getMethod(Variable var, String name, int rank) throws
        Throwable {
        return getMethod(var, name, rank, false);
    }

    static Method getMethod(Variable var, String name, int rank,
        boolean checkMissing) throws Throwable {
        if (var == null) throw new Throwable("Internal error. Null variable " +
            "encountered in call to TSExtractor.getMethod()");
        int _rank = var.getEffectiveRank();
        if (_rank != rank) throw new
            Throwable("Called method is not appropriate for variables of " +
           "effective rank " + _rank);
        if (checkMissing && (var.isMissingRecords())) {
            System.out.println("Variable " + var.getName() +
            " has gaps." +
            " Sampled time series code is being tested. Feature is not " +
            " currently available if the variable has gaps.");
            return null;
        }
        Method method = getMethod(var, name);
        if (method == null) throw new Throwable("get" + name + " not " +
           "implemented for " + var.getName());
        return method;
    }
    static Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    public static long getTime(int[] time) {
        int[] t = new int[6];
        for (int i = 0; i < 3; i++) t[i] = time[i];
        t[1]--;
        for (int i = 3; i < 6; i++) t[i] = 0;
        int n = time.length;
        if (n >= 4) {
            t[3] = time[3];
            if (n >= 5) {
                t[4] = time[4];
                if (n >= 6) {
                    t[5] = time[5];
                }
            }
        }
        cal.clear();
        cal.set(t[0], t[1], t[2], t[3], t[4], t[5]);
        cal.set(Calendar.MILLISECOND,(n > 6)?time[6]:0);
        return cal.getTimeInMillis();
    }

    public static double getTime(MetaData rdr, String vname, int[] time)
        throws Throwable {
        boolean isTT2000 =
            TimeVariableFactory.getTimeVariable(rdr, vname).isTT2000();
        long t = getTime(time);
        return (isTT2000)?TimeUtil.milliSecondSince1970(t):(double)t;
    }

    public static double[] getOverlap(MetaData rdr, double[] trange,
        String varName,
        int[] startTime, int[] stopTime) throws Throwable {
        double[] overlap = new double[] {Double.MIN_VALUE, Double.MAX_VALUE};;
        if (startTime != null) {
            if (startTime.length < 3) throw new Throwable("incomplete start" +
                " time " + "definition.");
            double _start = getTime(rdr, varName, startTime);
            if (_start > trange[1]) throw new Throwable("Start time is " +
                "beyond end of data");
            overlap[0] = (_start < trange[0])?trange[0]:_start;
        } else {
            overlap[0] = trange[0];
        }
        if (stopTime != null) {
            if (stopTime.length < 3) throw new Throwable("incomplete stop" +
                " time " + "definition.");
            double _stop = getTime(rdr, varName, stopTime);
            if (_stop < trange[0]) throw new Throwable("Stop time is " +
                "before start of data");
            if (_stop < overlap[0]) throw new Throwable("Stop time is " +
                "before start time");
            overlap[1] = /*(_stop > trange[1])?trange[1]:*/_stop;
        } else {
            overlap[1] = trange[1];
        }
        return overlap;
    }
}