/*
 * DataSetAdapter.java
 *
 * Created on April 2, 2007, 8:49 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package org.das2.dataset;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.das2.datum.Datum;
import org.das2.datum.DatumRange;
import org.das2.datum.DatumRangeUtil;
import org.das2.datum.InconvertibleUnitsException;
import org.das2.datum.Units;
import org.das2.system.DasLogger;
import org.das2.qds.AbstractDataSet;
import org.das2.qds.BundleDataSet;
import org.das2.qds.DDataSet;
import org.das2.qds.DRank0DataSet;
import org.das2.qds.MutablePropertyDataSet;
import org.das2.qds.QDataSet;
import org.das2.qds.SemanticOps;
import org.das2.qds.ops.Ops;

/**
 * Presents legacy das2 datasets as QDataSets. See also TableDataSetAdapter,VectorDataSetAdapter
 *
 * @author jbf
 */
public class DataSetAdapter {

    private static final Logger logger = DasLogger.getLogger(DasLogger.DATA_TRANSFER_LOG);

    public static final String PROPERTY_SOURCE = "adapterSource";

    ///////////////////////////////////////////////////////////////////////////////////
    // Helper for conversion such as %{xCacheRange} -> %{USER_PROPERTIES.xCacheRange}
    protected static Map<String, Object> adaptSubstitutions(Map<String, Object> das2props) {
        // Defines a pattern with three subgroups
        Pattern ptrn = Pattern.compile("(%\\{)(.+?)(\\})");

        for (Map.Entry<String, Object> e : das2props.entrySet()) {
            Object o = e.getValue();
            if (!(o instanceof String)) {
                continue;
            }
            String s = (String) o;
            Matcher m = ptrn.matcher(s);
            while (m.find()) {
                // Group indices are not as expected, 0 = entire match, 1 = 1st group, etc.
                if (!m.group(2).contains("USER_PROPERTIES")) {
                    s = String.format("%sUSER_PROPERTIES.%s%s", s.substring(0, m.end(1)),
                            s.substring(m.start(2), m.end(2)),
                            s.substring(m.start(3), s.length()));
                    m = ptrn.matcher(s);
                }
            }
            e.setValue(s);
        }
        return das2props;
    }

    /**
     * Created a new QDataSet given a Das2 DataSet
     *
     * This function and createLegacyDataSet() are inverses, though a round trip conversion is not guaranteed to preserve all
     * properties
     *
     * @param ds A Das2 Dataset
     * @return A new QDataSet
     */
    public static AbstractDataSet create(DataSet ds) {
        if (ds == null) {
            throw new NullPointerException("dataset is null");
        }

        // X-Y Datasets
        if (ds instanceof VectorDataSet) {
            boolean newCodeWhichSupportsBinMinPlanes = false;
            if (newCodeWhichSupportsBinMinPlanes) {
                VectorDataSet vds = (VectorDataSet) ds;
                return createVectorQds(vds);
            } else {
                if (ds.getPlaneIds().length <= 1) {
                    //Handle x single y as a simple vector
                    Vector v = new Vector((VectorDataSet) ds);
                    String sname = (String) ds.getProperty("name");
                    if (sname == null) {
                        sname = "y";
                    }
                    v.putProperty(QDataSet.NAME, sname);
                    return v;

                } else {
                    //Handle x multi y as a bundle
                    VectorDataSet vds = (VectorDataSet) ds;
                    Vector v = new Vector(vds);
                    String sname = (String) ds.getProperty("name");
                    if (sname == null) {
                        sname = "y";
                    }
                    v.putProperty(QDataSet.NAME, sname);
                    
                    AbstractDataSet bds = (AbstractDataSet) Ops.bundle(null, v);
                    String[] planes = ds.getPlaneIds();
                    Units unitsY = null;
                    boolean bCommonYUnits = false;
                    
                    HashMap<String,Integer> names= new HashMap<>();
                    names.put("x",0);
                    for (int i = 1; i < planes.length; i++) {
                        names.put(planes[i],i);
                    }
                    
                    for (int i = 1; i < planes.length; i++) {
                        // Arg, everything we want to get at is hidden behind 7 levels of
                        // interfaces.  As a bonus, class names repeat in different packages from
                        // the same dev group.
                        org.das2.dataset.AbstractDataSet.ViewDataSet view
                                = (org.das2.dataset.AbstractDataSet.ViewDataSet) vds.getPlanarView(planes[i]);
                        if (unitsY == null) {
                            unitsY = view.getYUnits();
                        } else {
                            bCommonYUnits = (unitsY == view.getYUnits());
                        }
                        
                        AbstractDataSet v0;
                        int i0; // location of the data

                        v = new Vector((VectorDataSet) vds.getPlanarView(planes[i]), planes[i]);
                        
                        int iext= planes[i].lastIndexOf(".");
                        String ext= planes[i].substring(iext+1);
                        String bas;
                        if ( ext.length()==planes[i].length() ) {
                            bas= ext;
                            ext= "";
                            v0= null;
                            i0= -1;
                        } else {
                            bas= planes[i].substring(0,planes[i].length()-ext.length()-1);
                            v0= (AbstractDataSet)Ops.unbundle( bds, bas );
                            Integer ii0= names.get(bas);
                            if (ii0==null) {
                                logger.fine("assume we can't find the name because it is the X or Y value");
                                i0= 0;
                            } else {
                                i0= ii0;
                            }
                        }
                        
                        MutablePropertyDataSet bundleDescriptor= (MutablePropertyDataSet)bds.property(QDataSet.BUNDLE_1);
                                
                        v.putProperty(QDataSet.NAME, planes[i]);
                        if ( ext.equals("min") && v0!=null ) {
                            v0.putProperty(QDataSet.BIN_MIN, v);
                            bundleDescriptor.putProperty( QDataSet.BIN_MIN_NAME, i0, planes[i]);
                        } else if ( ext.equals("max") && v0!=null ) {
                            v0.putProperty(QDataSet.BIN_MAX, v);
                            bundleDescriptor.putProperty( QDataSet.BIN_MAX_NAME, i0, planes[i]);
                        } else if ( ext.equals("stddev") && v0!=null ) {
                            v0.putProperty(QDataSet.DELTA_MINUS, v);
                            v0.putProperty(QDataSet.DELTA_PLUS, v);
                            bundleDescriptor.putProperty( QDataSet.DELTA_MINUS_NAME, i0, planes[i]);
                            bundleDescriptor.putProperty( QDataSet.DELTA_PLUS_NAME, i0, planes[i]);
                        }
                        
                        Ops.bundle(bds, v);
                        
                    }

                    // Convert Das2 property substitutions to USER_PROPERTIES substitutions
                    Map<String, Object> dasProps = adaptSubstitutions(vds.getProperties());
                    bds.putProperty(QDataSet.USER_PROPERTIES, dasProps);

                    bds.putProperty(QDataSet.DEPEND_0, new XTagsDataSet(vds));
                    bds.putProperty(QDataSet.TITLE, dasProps.get(DataSet.PROPERTY_TITLE));

                    // If all Y elements of the bundle have the same units, put those units
                    // on the Y axis, that way something identifies Y.
                    if (bCommonYUnits) {
                        bds.putProperty(QDataSet.UNITS, unitsY);
                        bds.putProperty(QDataSet.LABEL, unitsY.toString());
                    }

                    // Copy more properties into the overall bundle dataset, wow this really
                    // needs to be refactored.
                    bds.putProperty(QDataSet.SCALE_TYPE, vds.getProperty(DataSet.PROPERTY_Y_SCALETYPE));
                    DatumRange yRng = (DatumRange) vds.getProperty(DataSet.PROPERTY_Y_RANGE);
                    if (yRng != null) {
                        bds.putProperty(QDataSet.TYPICAL_MIN, yRng.min().value());
                        bds.putProperty(QDataSet.TYPICAL_MAX, yRng.max().value());
                    }

                    if ("xyzScatter".equals(ds.getProperty("schema"))) {
                        BundleDataSet bbds = (BundleDataSet) bds;
                        return (BundleDataSet) Ops.bundle(new XTagsDataSet(vds), bbds.unbundle(0), bbds.unbundle(1));
                    }
                            
                    if (bds.length(0) == 1) {
                        return DDataSet.copy(Ops.unbundle(bds, 0));
                    } else {
                        return DDataSet.copy(bds);
                    }
                }
            }
        }

        // X-YScan Datasets
        if (ds instanceof TableDataSet) {
            TableDataSet tds = (TableDataSet) ds;
            if (tds.tableCount() <= 1) {
                // Handling table datasets that may come with statistics planes.  These are
                // denoted by having a "source" and "operations" string properties.
                return createSimpleTableDS(tds);
            } else {
                if (tds instanceof DefaultTableDataSet && tds.tableCount() > tds.getXLength() / 2) {
                    return ((DefaultTableDataSet) tds).toQDataSet();
                } else {
                    return new MultipleTable(tds);
                }
            }
        }

        throw new IllegalArgumentException("unsupported dataset type: " + ds.getClass().getName());
    }

    ///////////////////////////////////////////////////////////////////////////////////
	// Helper for setting up Vector QDataSet's that have statistics
	
	private static class AdapterPDim {

        String sId;
        Map<String, String> vars = new HashMap<>();

        AdapterPDim(String _sId) {
            sId = _sId;
        }
    }

    private static AbstractDataSet createVectorQds(VectorDataSet vds) {
        // The thing we care about are the number of datasets (or phys-dims in das2 speak) 
        // in this X-Y set, not really the number of variables.  If the number of physdims 
        // is greater than one, we need a to make a bundle.  Count phys-dims here.
        // The das2.2 stream format did not group variables into related sets with a natural
        // enclosing tag, instead property values were used to do this, which is... messy.
        // das2.3 streams are much better in this regard.

        String[] lPlanes = vds.getPlaneIds();

        Map<String, AdapterPDim> lPDims = new HashMap<>();

        // 1st pass, find top level planes, side effect, see if have common Y units
        int nStashed = 0;
        Units commonY = null;
        boolean bCommonYUnits = false;
        for (String sId : lPlanes) {
            DataSet ds = vds.getPlanarView(sId);
            // If this has an operation property other than avg, it's not top level
            String sOp = (String) ds.getProperty(DataSet.PROPERTY_OPERATION);
            if ((sOp != null) && (!sOp.equals("BIN_AVG"))) {
                continue;
            }

            if (lPDims.containsKey(sId)) {
                throw new IllegalArgumentException("Non-unique plane IDs in das2 X-multi-Y packet");
            }

            String sSource = (String) ds.getProperty(DataSet.PROPERTY_SOURCE);
            lPDims.put(sSource, new AdapterPDim(sId));

            if (commonY == null) {
                commonY = ds.getYUnits();
            } else {
                bCommonYUnits = (commonY == ds.getYUnits());
            }

            nStashed += 1;
        }

        // 2nd pass, find statistics planes.
        for (String sId : lPlanes) {
            if (sId == null) {
                continue;
            }

            DataSet ds = vds.getPlanarView(sId);
            String sOp = (String) ds.getProperty(DataSet.PROPERTY_OPERATION);
            if ((sOp == null) || sOp.equals("BIN_AVG")) {
                continue;
            }

            String sSource = (String) ds.getProperty(DataSet.PROPERTY_SOURCE);

            AdapterPDim pdim = lPDims.get(sSource);
            if (pdim == null) {
                throw new IllegalArgumentException("Statistics plane has no parent in x-multi-y packet");
            }
            pdim.vars.put(sOp, sId);
            nStashed += 1;
        }

        if (nStashed != lPlanes.length) {
            throw new IllegalArgumentException("Not all plane purposes understood in x-multi-y packet");
        }

        // Now build a QDataSet, if multiple physicsal dimensions present, will need a bundle ds.
        AbstractDataSet bds = null;
        Vector v = null;
        for (Entry<String, AdapterPDim> e : lPDims.entrySet()) {
            AdapterPDim pdim = e.getValue();

            org.das2.dataset.AbstractDataSet.ViewDataSet view
                    = (org.das2.dataset.AbstractDataSet.ViewDataSet) vds.getPlanarView(pdim.sId);

            v = new Vector((VectorDataSet) view, pdim.sId);
            String sName = (String) view.getProperty("name");
            if (sName == null) {
                sName = "y";
            }
            v.putProperty(QDataSet.NAME, sName);

            // Attach stats planes if present.
            for (String sOp : pdim.vars.keySet()) {
                if (sOp == null) {
                    continue; // TODO: Complain to logger here
                }
                String sId = pdim.vars.get(sOp);

                QDataSet qdsStats = new Vector((VectorDataSet) vds.getPlanarView(sId), sId);

                if (sOp.equals("BIN_MAX")) {
                    v.putProperty(QDataSet.BIN_MAX, qdsStats);
                }
                if (sOp.equals("BIN_MIN")) {
                    v.putProperty(QDataSet.BIN_MIN, qdsStats);
                }

                if (sOp.equals("DELTA_PLUS")) {
                    v.putProperty(QDataSet.DELTA_PLUS, qdsStats);
                }
                if (sOp.equals("DELTA_MINUS")) {
                    v.putProperty(QDataSet.DELTA_MINUS, qdsStats);
                }
            }

            // The bundle decision.  If this physical dimension is part of a bundle, the  X-tags, and 
            // properties have to be copied over.
            if (lPDims.size() > 1) {
                if (bds == null) {
                    bds = (AbstractDataSet) Ops.bundle(null, v);

                    // If all top-level elements have the same units, put those units on the Y axis, 
                    // that way something identifies Y.
                    if ((bCommonYUnits) && (commonY != null)) {
                        bds.putProperty(QDataSet.UNITS, commonY);
                        bds.putProperty(QDataSet.LABEL, commonY.toString());
                    }
                    // Convert Das2 property substitutions to USER_PROPERTIES substitutions
                    Map<String, Object> dasProps = adaptSubstitutions(vds.getProperties());
                    bds.putProperty(QDataSet.USER_PROPERTIES, dasProps);

                    bds.putProperty(QDataSet.DEPEND_0, new XTagsDataSet(vds));
                    bds.putProperty(QDataSet.TITLE, dasProps.get(DataSet.PROPERTY_TITLE));

                    // Copy more properties into the overall bundle dataset, wow this really
                    // needs to be refactored.
                    bds.putProperty(QDataSet.SCALE_TYPE, vds.getProperty(DataSet.PROPERTY_Y_SCALETYPE));
                    DatumRange yRng = (DatumRange) vds.getProperty(DataSet.PROPERTY_Y_RANGE);
                    if (yRng != null) {
                        bds.putProperty(QDataSet.TYPICAL_MIN, yRng.min().value());
                        bds.putProperty(QDataSet.TYPICAL_MAX, yRng.max().value());
                    }
                } else {
                    Ops.bundle(bds, v);
                }
            }
        }

        // Output either the bundle or the single dataset
        if (bds != null) {
            return DDataSet.copy(bds);
        } else {
            return v;
        }
    }

    /**
     * Created a new Das2 DataSet given a QDataSet
     *
     * This function and create() are inverses, though a round trip conversion is not guaranteed to preserve all properties. Note
     * that not all QDataSets can be represented as Das2 DataSets. If the given QDataSet has no Das2 analog, an
     * IllegalArgumentException is thrown.
     *
     * @param ds A QDataSet
     * @return A new Das2 DataSet
     *
     * DON'T USE THIS CODE! Use org.das2.qstream.QdsToD2sStream IN THE QStream project!
     */
    @Deprecated
    public static DataSet createLegacyDataSet(org.das2.qds.QDataSet ds) {
        if (ds.rank() == 1) {
            return VectorDataSetAdapter.create(ds);
        } else if (SemanticOps.isBundle(ds)) {
            return VectorDataSetAdapter.createFromBundle(ds);
        } else if (ds.rank() == 2) {
            return TableDataSetAdapter.create(ds);
        } else if (ds.rank() == 3) {
            return TableDataSetAdapter.create(ds);
        } else {
            throw new IllegalArgumentException("unsupported rank: " + ds.rank());
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////
    // Helper dataset holds DEPEND_0 for MultipleTable QDataSets
    static class MultiTableXTagsDataSet extends AbstractDataSet {

        DataSet source;
        int offset;
        int length;

        MultiTableXTagsDataSet(DataSet source, int offset, int length) {
            this.source = source;
            this.offset = offset;
            this.length = length;
            properties.put(QDataSet.UNITS, source.getXUnits());
            properties.put(QDataSet.LABEL, source.getProperty(DataSet.PROPERTY_X_LABEL));
            Object o = source.getProperty(DataSet.PROPERTY_X_MONOTONIC);
            if (o != null) {
                properties.put(QDataSet.MONOTONIC, o);
            }
            Datum xTagWidth = (Datum) source.getProperty(DataSet.PROPERTY_X_TAG_WIDTH);
            if (xTagWidth != null) {
                properties.put(QDataSet.CADENCE, org.das2.qds.DataSetUtil.asDataSet(xTagWidth));
            }
        }

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

        @Override
        public double value(int i) {
            return source.getXTagDouble(i + offset, source.getXUnits());
        }

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

    ///////////////////////////////////////////////////////////////////////////////////
    // Helper dataset, holds DEPEND_0 for Vector & SimpleTable QDataSets
    static class XTagsDataSet extends AbstractDataSet {

        org.das2.dataset.DataSet source;

        XTagsDataSet(org.das2.dataset.DataSet source) {
            this.source = source;
            properties.put(QDataSet.UNITS, source.getXUnits());
            properties.put(QDataSet.LABEL, source.getProperty(DataSet.PROPERTY_X_LABEL));

            // QDataSet Cadences are a rank 0 dataset
            Datum d = (Datum) source.getProperty(DataSet.PROPERTY_X_TAG_WIDTH);
            if (d != null) {
                properties.put(QDataSet.CADENCE, DRank0DataSet.create(d));
            }

            Object o = source.getProperty(org.das2.dataset.DataSet.PROPERTY_X_MONOTONIC);
            if (o != null) {
                properties.put(QDataSet.MONOTONIC, o);
            }
        }

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

        @Override
        public double value(int i) {
            return source.getXTagDouble(i, source.getXUnits());
        }

        @Override
        public int length() {
            return source.getXLength();
        }

    }

    ///////////////////////////////////////////////////////////////////////////////////
	// Top Level QDataSet for X vs Y data
	static class Vector extends AbstractDataSet {

        VectorDataSet source;

        // Time for more ugly hacks:  TODO: Convert straight to QDataSet and get
        //                                  rid of stupid crap like this.
        Vector(VectorDataSet source) {
            this(source, null);
        }

        private static Object hack(Map<String, Object> m, String k, String id) {
            if ((id == null) || (id.isEmpty())) {
                return m.get(k);
            } else {
                return m.get(id + "." + k);
            }
        }

        // This constructor takes a plane ID so that property values can be gathered.
        // It's a dumb hack to get around:
        //   1. Das2 DataSet objects are immutable
        //   2. We are not converting straght to QDataSet
        Vector(VectorDataSet source, String sPlaneID) {
            super();
            this.source = source;

            //Throw everything including the well-known stuff into user properties
            Map<String, Object> dasProps = adaptSubstitutions(source.getProperties());
            properties.put(QDataSet.USER_PROPERTIES, dasProps);

            properties.put(QDataSet.TITLE, hack(dasProps, DataSet.PROPERTY_TITLE, sPlaneID));
            properties.put(QDataSet.UNITS, source.getYUnits());
            //properties.put(QDataSet.FILL_VALUE, source.getYUnits().getFillDouble() );
            //Object ofill= hack(dasProps, DataSet.PROPERTY_Y_FILL, sPlaneID);
            //if ( ofill!=null && ( ofill instanceof Number ) ) properties.put(QDataSet.FILL_VALUE, ofill );
            properties.put(QDataSet.LABEL, hack(dasProps, DataSet.PROPERTY_Y_LABEL, sPlaneID));
            properties.put(QDataSet.FORMAT, hack(dasProps, DataSet.PROPERTY_Y_FORMAT, sPlaneID));
            AbstractDataSet xds = new XTagsDataSet(source);
            xds.putProperty(QDataSet.CACHE_TAG, hack(dasProps, DataSet.PROPERTY_CACHE_TAG, sPlaneID));
            properties.put(QDataSet.DEPEND_0, xds);
            properties.put(PROPERTY_SOURCE, source);

            // http://www.sarahandjeremy.net/~jbf/1wire/data/2007/0B000800408DD710.20071201.d2s uses property "valid_range"
            String syValid = (String) dasProps.get("valid_range");
            if (syValid != null) {
                try {
                    DatumRange yValid = DatumRangeUtil.parseDatumRange(syValid, source.getYUnits());
                    double val = yValid.min().doubleValue(source.getYUnits());
                    properties.put(QDataSet.VALID_MIN, val);
                    val = yValid.max().doubleValue(source.getYUnits());
                    properties.put(QDataSet.VALID_MAX, val);
                } catch (ParseException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }

            //New properties after 2014-05-28 Das2 Dev meeting
            Object obj = hack(dasProps, DataSet.PROPERTY_Y_VALID_MIN, sPlaneID);
            if (obj != null) {
                if (obj instanceof Double) {
                    properties.put(QDataSet.VALID_MIN, (Double) obj);
                } else if (obj instanceof Datum) {
                    properties.put(QDataSet.VALID_MIN, ((Datum) obj).doubleValue(source.getYUnits()));
                } else {
                    logger.warning("property " + DataSet.PROPERTY_Y_VALID_MIN + " should be type Double");
                }
            }

            obj = hack(dasProps, DataSet.PROPERTY_Y_VALID_MAX, sPlaneID);
            if (obj != null) {
                if (obj instanceof Double) {
                    properties.put(QDataSet.VALID_MAX, (Double) obj);
                } else if (obj instanceof Datum) {
                    properties.put(QDataSet.VALID_MAX, ((Datum) obj).doubleValue(source.getYUnits()));
                } else {
                    logger.warning("property " + DataSet.PROPERTY_Y_VALID_MAX + " should be type Double");
                }
            }

            properties.put(QDataSet.FILL_VALUE, hack(dasProps, DataSet.PROPERTY_Y_FILL, sPlaneID));
            properties.put(QDataSet.SCALE_TYPE, hack(dasProps, DataSet.PROPERTY_Y_SCALETYPE, sPlaneID));
            properties.put(QDataSet.MONOTONIC, hack(dasProps, DataSet.PROPERTY_Y_MONOTONIC, sPlaneID));

            //Add this in after next autoplot update
            properties.put(QDataSet.DESCRIPTION, hack(dasProps, DataSet.PROPERTY_Y_SUMMARY, sPlaneID));

            //Let Das2 Streams set a Y-Axis range
            DatumRange yRng = (DatumRange) hack(dasProps, DataSet.PROPERTY_Y_RANGE, sPlaneID);
            if (yRng != null) {
                try {
                    properties.put(QDataSet.TYPICAL_MIN, yRng.min().doubleValue(source.getYUnits()));
                    properties.put(QDataSet.TYPICAL_MAX, yRng.max().doubleValue(source.getYUnits()));
                } catch (InconvertibleUnitsException ex) {
                    logger.info("yRange has inconvertible units");
                }
            }

            Datum d = (Datum) hack(dasProps, DataSet.PROPERTY_Y_TAG_WIDTH, sPlaneID);
            if (d != null) {
                properties.put(QDataSet.CADENCE, DRank0DataSet.create(d));
            }
        }

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

        @Override
        public double value(int i) {
            return source.getDouble(i, source.getYUnits());
        }

        @Override
        public int length() {
            return source.getXLength();
        }

    }

    ///////////////////////////////////////////////////////////////////////////////////
    // Helper Dataset, holds DEPEND_1 for SimpleTable QDataSets
    static class YTagsDataSet extends AbstractDataSet {

        TableDataSet source;
        int table;

        YTagsDataSet(TableDataSet source, int table) {
            this.source = source;
            this.table = table;
            properties.put(QDataSet.UNITS, source.getYUnits());
            properties.put(QDataSet.LABEL, source.getProperty(DataSet.PROPERTY_Y_LABEL));
            properties.put(QDataSet.SCALE_TYPE, source.getProperty(DataSet.PROPERTY_Y_SCALETYPE));

            Datum d = (Datum) source.getProperty(DataSet.PROPERTY_Y_TAG_WIDTH);
            if (d != null) {
                properties.put(QDataSet.CADENCE, DRank0DataSet.create(d));
            }

            DatumRange yRng = (DatumRange) source.getProperty(DataSet.PROPERTY_Y_RANGE);
            if (yRng != null) {
                properties.put(QDataSet.TYPICAL_MIN, yRng.min().value());
                properties.put(QDataSet.TYPICAL_MAX, yRng.max().value());
            }

        }

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

        @Override
        public double value(int i) {
            return source.getYTagDouble(table, i, source.getYUnits());
        }

        @Override
        public int length() {
            return source.tableCount() > 0 ? source.getYLength(table) : 99;
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////
	// Helper for setting up Table QDataSet's that have statistics
	 
	private static AbstractDataSet createSimpleTableDS(TableDataSet tds) {

        // Find the top level plane, expect there to be only one since this isn't a bundle
        // dataset handler.
        String sTopDs = null;
        String sTopSrc = null;
        String[] lPlanes = tds.getPlaneIds();
        if (lPlanes.length == 1) {
            sTopDs = lPlanes[0];
        } else {
            // Take the first plane that meets the criteria as the top-level dataset, the
            // criteria is, (1) Don't have a source property or (2) be the average
            for (String sPlane : lPlanes) {
                DataSet ds = tds.getPlanarView(sPlane);
                sTopSrc = (String) ds.getProperty(DataSet.PROPERTY_SOURCE);
                if (sTopSrc == null) {
                    sTopDs = sPlane;
                    break;
                } else {
                    String sOp = (String) ds.getProperty(DataSet.PROPERTY_OPERATION);
                    if ((sOp != null) && (sOp.equals("BIN_AVG"))) {
                        sTopDs = sPlane;
                        break;
                    }
                }
            }
        }

        // TODO: Use some sort of conversion exception instead of this
        if (sTopDs == null) {
            throw new IllegalArgumentException(
                    "Couldn't locate the top-level <yscan> in the set of <yscans>.  "
                    + "HINT: If this is simple bundle then you've hit a missing feature in Autoplot."
                    + " If this is a peak and averages dataset, use the 'source' and 'operation'"
                    + " properties to clairify the <yscan> relationships. "
            );
        }

        AbstractDataSet qds = new SimpleTable((TableDataSet) tds.getPlanarView(sTopDs));

        // Create every other dataset and bind it to the top level one based on it's 
        // operation property.  If that's not specified OR it's source disagrees ignore it
        for (String sPlane : lPlanes) {
            if (sPlane.equals(sTopDs)) {
                continue;
            }
            TableDataSet dsPlane = (TableDataSet) tds.getPlanarView(sPlane);
            String sPlaneSrc = (String) dsPlane.getProperty(DataSet.PROPERTY_SOURCE);
            if (((sPlaneSrc == null) && (sTopSrc == null)) || sTopSrc.equals(sPlaneSrc)) {

                // Okay, they have the same source see if we understand the operation
                String sOp = (String) dsPlane.getProperty(DataSet.PROPERTY_OPERATION);

                if (sOp == null) {
                    continue; // TODO: Complain to logger here
                }
                QDataSet qdsAncillary = new SimpleTable(dsPlane);

                if (sOp.equals("BIN_MAX")) {
                    qds.putProperty(QDataSet.BIN_MAX, qdsAncillary);
                }
                if (sOp.equals("BIN_MIN")) {
                    qds.putProperty(QDataSet.BIN_MIN, qdsAncillary);
                }

                if (sOp.equals(QDataSet.DELTA_PLUS)) {
                    qds.putProperty(sOp, qdsAncillary);
                }
                if (sOp.equals(QDataSet.DELTA_MINUS)) {
                    qds.putProperty(sOp, qdsAncillary);
                }

                // TODO: Complain to logger here
            }

        }
        return qds;
    }

    ///////////////////////////////////////////////////////////////////////////////////
    // Toplevel QDataSet for X,Y,Z "grid" data
    static class SimpleTable extends AbstractDataSet {

        TableDataSet source;

        SimpleTable(TableDataSet source) {
            super();
            if (source.tableCount() > 1) {
                throw new IllegalArgumentException("only simple tables are supported");
            }

            this.source = source;

            Map<String, Object> dasProps = adaptSubstitutions(source.getProperties());

            // Save properterties with value substitution strings in Autoplot Stlye
            properties.put(QDataSet.USER_PROPERTIES, dasProps);

            properties.put(QDataSet.UNITS, source.getZUnits());
            properties.put(QDataSet.LABEL, dasProps.get(DataSet.PROPERTY_Z_LABEL));
            properties.put(QDataSet.TITLE, dasProps.get(DataSet.PROPERTY_TITLE));
            QDataSet xtags = new XTagsDataSet(source);
            properties.put(QDataSet.DEPEND_0, xtags);
            QDataSet ytags = new YTagsDataSet(source, 0);
            properties.put(QDataSet.DEPEND_1, ytags);
            properties.put(QDataSet.QUBE, Boolean.TRUE);
            properties.put(PROPERTY_SOURCE, source);

            //Let Das2 Streams set a Z-Axis range
            DatumRange zRng = (DatumRange) dasProps.get(DataSet.PROPERTY_Z_RANGE);
            if (zRng != null) {
                properties.put(QDataSet.TYPICAL_MIN, zRng.min().value());
                properties.put(QDataSet.TYPICAL_MAX, zRng.max().value());
            }
            properties.put(QDataSet.RENDER_TYPE, dasProps.get(DataSet.PROPERTY_RENDERER));
            //properties.put(QDataSet.MONOTONIC, dasProps.get(DataSet.PROPERTY_X_MONOTONIC));
            properties.put(QDataSet.FILL_VALUE, dasProps.get(DataSet.PROPERTY_Z_FILL));

            properties.put(QDataSet.VALID_MIN, dasProps.get(DataSet.PROPERTY_Z_VALID_MIN));
            properties.put(QDataSet.VALID_MAX, dasProps.get(DataSet.PROPERTY_Z_VALID_MAX));
            properties.put(QDataSet.SCALE_TYPE, dasProps.get(DataSet.PROPERTY_Z_SCALETYPE));
            properties.put(QDataSet.LABEL, dasProps.get(DataSet.PROPERTY_Z_LABEL));
        }

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

        @Override
        public int length(int i) {
            return source.getYLength(0);
        }

        @Override
        public double value(int i, int j) {
            return source.getDouble(i, j, source.getZUnits());
        }

        @Override
        public int length() {
            return source.getXLength();
        }

    }

    ///////////////////////////////////////////////////////////////////////////////////
    // Toplevel QDataSet for multiple sets of Z data on an X,Y "grid"
    static class MultipleTable extends AbstractDataSet {

        TableDataSet source;

        MultipleTable(TableDataSet source) {
            super();

            this.source = source;

            Map<String, Object> dasProps = adaptSubstitutions(source.getProperties());

            // Save properterties with value substitution strings in Autoplot Stlye
            properties.put(QDataSet.USER_PROPERTIES, dasProps);

            properties.put(QDataSet.JOIN_0, DDataSet.create(new int[0]));
            properties.put(QDataSet.UNITS, source.getZUnits());
            properties.put(PROPERTY_SOURCE, source);
            properties.put(QDataSet.TITLE, dasProps.get(DataSet.PROPERTY_TITLE));

            //Let Das2 Streams set Z-Axis properties
            DatumRange zRng = (DatumRange) dasProps.get(DataSet.PROPERTY_Z_RANGE);
            if (zRng != null) {
                properties.put(QDataSet.TYPICAL_MIN, zRng.min().value());
                properties.put(QDataSet.TYPICAL_MAX, zRng.max().value());
            }
            properties.put(QDataSet.RENDER_TYPE, dasProps.get(DataSet.PROPERTY_RENDERER));
            //properties.put(QDataSet.MONOTONIC, dasProps.get(DataSet.PROPERTY_X_MONOTONIC));
            properties.put(QDataSet.FILL_VALUE, dasProps.get(DataSet.PROPERTY_Z_FILL));

            properties.put(QDataSet.VALID_MIN, dasProps.get(DataSet.PROPERTY_Z_VALID_MIN));
            properties.put(QDataSet.VALID_MAX, dasProps.get(DataSet.PROPERTY_Z_VALID_MAX));
            properties.put(QDataSet.SCALE_TYPE, dasProps.get(DataSet.PROPERTY_Z_SCALETYPE));
            properties.put(QDataSet.LABEL, dasProps.get(DataSet.PROPERTY_Z_LABEL));
        }

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

        @Override
        public int length() {
            return source.tableCount();
        }

        @Override
        public int length(int i) {
            return source.tableEnd(i) - source.tableStart(i);
        }

        @Override
        public int length(int i, int j) {
            try {
                return source.getYLength(i);
            } catch (IndexOutOfBoundsException ex) {
                throw ex;
            }
        }

        @Override
        public double value(int i, int j, int k) {
            int ts = source.tableStart(i);
            try {
                return source.getDouble(ts + j, k, source.getZUnits());
            } catch (IndexOutOfBoundsException ex) {
                throw ex;
            }
        }

        @Override
        public Object property(String name, int i) {
            if (name.equals(QDataSet.DEPEND_0)) {
                return new MultiTableXTagsDataSet(source, source.tableStart(i), source.tableEnd(i) - source.tableStart(i));
            } else if (name.equals(QDataSet.DEPEND_1)) {
                return new YTagsDataSet(source, i);
            } else {
                return super.property(name, i);
            }
        }
    }
}
